Este artigo usará o método DIRTY, inseguro, "muleta", assustador etc. Os fracos de coração não leem!
Devo dizer imediatamente que não foi possível resolver alguns problemas de usabilidade: você não pode usar o fechamento no código que será passado para o worker.

Todos nós gostamos de novas tecnologias e gostamos quando é conveniente usá-las. Mas no caso do trabalhador, isso não é inteiramente verdade. O Worker trabalha com um arquivo ou link de arquivo, mas isso é inconveniente. Eu gostaria de poder colocar qualquer tarefa no trabalhador, e não apenas o código especialmente planejado.
O que é necessário para tornar o trabalho com o trabalhador mais conveniente? Na minha opinião, o seguinte:
- Capacidade de executar código arbitrário no trabalhador a qualquer momento
- Capacidade de passar dados complexos para o trabalhador (instâncias de classe, funções)
- Capacidade de obter Promise com uma resposta do trabalhador.
Primeiro, precisamos de um protocolo de comunicação entre o trabalhador e a janela principal. Em geral, um protocolo é simplesmente a estrutura e os tipos de dados com os quais a janela e o trabalhador do navegador se comunicarão. Não há nada complicado. Você pode usar algo assim ou escrever sua própria versão. Em cada mensagem, teremos um ID e dados específicos para um tipo específico de mensagem. Para começar, teremos dois tipos de mensagens para o trabalhador:
- adicionando bibliotecas / arquivos ao trabalhador
- lançamento do trabalho
Arquivo dentro do trabalhador
Antes de começar a criar um trabalhador, é necessário descrever um arquivo que funcione no trabalhador e suporte o protocolo descrito por nós. Eu amo OOP , então será uma classe chamada WorkerBody. Esta classe deve se inscrever no evento na janela pai.
self.onmessage = (message) => { this.onMessage(message.data); };
Agora podemos ouvir eventos na janela principal. Temos dois tipos de eventos: aqueles aos quais a resposta está implícita e todos os outros. Processamos eventos.
A adição de bibliotecas e arquivos ao trabalhador é feita usando a API importScripts .
E o pior: usaremos eval para executar uma função arbitrária.
... onMessage(message) { switch (message.type) { case MESSAGE_TYPE.ADD_LIBS: this.addLibs(message.libs); break; case MESSAGE_TYPE.WORK: this.doWork(message); break; } } doWork(message) { try { const processor = eval(message.job); const params = this._parser.parse(message.params); const result = processor(params); if (result && result.then && typeof result.then === 'function') { result.then((data) => { this.send({ id: message.id, state: true, body: data }); }, (error) => { if (error instanceof Error) { error = String(error); } this.send({ id: message.id, state: false, body: error }); }); } else { this.send({ id: message.id, state: true, body: result }); } } catch (e) { this.send({ id: message.id, state: false, body: String(e) }); } } send(data) { data.body = this._serializer.serialize(data.body); try { self.postMessage(data); } catch (e) { const toSet = { id: data.id, state: false, body: String(e) }; self.postMessage(toSet); } }
O método onMessage
responsável por receber a mensagem e selecionar o manipulador, doWork
- inicia a função passada e envia a resposta para a janela pai.
Analisador e serializador
Agora que temos o conteúdo de worker, precisamos aprender a serializar e analisar todos os dados para transmiti-los ao worker. Vamos começar com o serializador. Queremos poder passar quaisquer dados para o trabalhador, incluindo instâncias de classe, classes e funções. Mas com os recursos nativos do worker, só podemos transmitir dados do tipo JSON. Para contornar essa proibição, precisamos avaliar . Tudo o que não pode aceitar JSON, envolveremos as construções de string correspondentes e executaremos do outro lado. Para preservar a imutabilidade, os dados obtidos são clonados em tempo real e os que não podem ser serializados da maneira usual são substituídos por objetos de serviço e, por sua vez, são substituídos pelo analisador do outro lado. À primeira vista, pode parecer que essa tarefa não seja difícil, mas há muitas armadilhas. A pior limitação dessa abordagem é a incapacidade de usar o fechamento, que carrega um estilo ligeiramente diferente de escrever código. Vamos começar com o mais simples, com a função Primeiro, você precisa aprender como distinguir uma função de um construtor de classe.
Vamos tentar distinguir:
static isFunction(Factory){ if (!Factory.prototype) {
Primeiro, descobriremos se a função possui um protótipo. Caso contrário, essa é definitivamente uma função. Em seguida, examinamos o número de propriedades no protótipo e, se no protótipo apenas o construtor e a função não são os herdeiros de outra classe, consideramos que essa é uma função.
Após encontrar uma função, simplesmente a substituímos por um objeto de serviço pelos campos __type = "serialized-function"
e template
, que é igual ao modelo dessa função ( func.toString()
).
Por enquanto, pule a classe e analise a instância da classe. Além disso, nos dados, precisamos distinguir objetos comuns de instâncias de classe.
static isInstance(some) { const constructor = some.constructor; if (!constructor) { return false; } return !Serializer.isNative(constructor); } static isNative(data) { return /function .*?\(\) \{ \[native code\] \}/.test(data.toString()); }
Consideramos que um objeto é comum se não tiver um construtor ou se for uma função nativa. Após identificar a instância da classe, a substituímos por um objeto de serviço por campos:
__type
- 'instância serializada'data
- dados que estavam na instânciaindex
- o índice de classe desta instância na lista de serviços de classe.
Para transferir dados, precisamos criar um campo adicional: nele armazenaremos uma lista de todas as classes únicas que passarmos. A parte mais difícil é que, quando uma classe é detectada, pegue não apenas o modelo, mas também o modelo de todas as classes-pai e salve-as como classes separadas - para que cada "pai" seja passado não mais de uma vez - e salve a verificação na instância de. Definir uma classe é fácil: esta é uma função que não passou no teste Serializer.isFunction. Ao adicionar uma classe, verificamos a presença de uma classe na lista de dados serializados e adicionamos apenas dados únicos. O código que coleta a classe em um modelo é bastante grande e fica aqui .
No analisador, primeiro analisamos todas as classes passadas para nós e as compilamos se elas não foram aprovadas antes. Em seguida, percorremos recursivamente cada campo de dados e substituímos os objetos de serviço por dados compilados. A coisa mais interessante está na instância da classe. Temos uma classe e há dados que estavam em sua instância, mas não podemos simplesmente instancia-los, porque a chamada ao construtor pode ter parâmetros que não temos. Um método Object.create quase esquecido vem em nosso auxílio, que retorna um objeto com um determinado protótipo. Portanto, evitamos chamar o construtor e obter uma instância da classe e apenas reescrever as propriedades na instância.
Trabalhador de criação
Para que o trabalhador trabalhe com sucesso, precisamos ter um analisador e serializador dentro e fora do trabalhador; portanto, pegamos o serializador e transformamos o serializador, analisador e corpo do trabalhador em um modelo. Fazemos um blob a partir do modelo e criamos um link de download via URL.createObjectURL (esse método pode não funcionar com algumas "Políticas de segurança de conteúdo"). Este método também é adequado para executar código arbitrário a partir de uma string.
_createWorker(customWorker) { const template = `var MyWorker = ${this._createTemplate(customWorker)};`; const blob = new Blob([template], { type: 'application/javascript' }); return new Worker(URL.createObjectURL(blob)); } _createTemplate(WorkerBody) { const Name = Serializer.getFnName(WorkerBody); if (!Name) { throw new Error('Unnamed Worker Body class! Please add name to Worker Body class!'); } return [ '(function () {', this._getFullClassTemplate(Serializer, 'Serializer'), this._getFullClassTemplate(Parser, 'Parser'), this._getFullClassTemplate(WorkerBody, 'WorkerBody'), `return new WorkerBody(Serializer, Parser)})();` ].join('\n'); }
Resultado
Assim, obtivemos uma biblioteca fácil de usar que pode executar código arbitrário no worker. Ele suporta classes do TypeScript. Por exemplo:
const wrapper = workerWrapper.create(); wrapper.process((params) => {
Planos adicionais
Infelizmente, esta biblioteca está longe de ser ideal. É necessário adicionar suporte para setters e getters em classes, objetos, protótipos, propriedades estáticas. Também gostaríamos de adicionar armazenamento em cache, executar um script alternativo sem avaliação via URL.createObjectURL e adicionar um arquivo com o conteúdo do worker ao assembly (se a criação não estiver disponível em tempo real). Venha para o repositório !