Trabalhadores da Web sem um arquivo Javascript separado?


291

Tanto quanto eu sei, os profissionais da Web precisam ser escritos em um arquivo JavaScript separado e chamados assim:

new Worker('longrunning.js')

Estou usando o compilador de fechamento para combinar e minificar todo o meu código-fonte JavaScript, e prefiro não ter meus trabalhadores em arquivos separados para distribuição. Existe alguma maneira de fazer isso?

new Worker(function() {
    //Long-running work here
});

Dado que as funções de primeira classe são tão cruciais para o JavaScript, por que a maneira padrão de executar o trabalho em segundo plano precisa carregar um arquivo JavaScript inteiro do servidor da Web?


7
É porque manter um contexto de execução puramente threadsafe é ainda mais crucial do que funções de primeira classe :-)
Pointy

1
Estou trabalhando nisso (ou melhor, na minimização do problema): DynWorker . Você pode fazer: var worker = new DynWorker(); worker.inject("foo", function(){...});...
Félix Saparelli


1
O OP excluiu a pergunta "Trabalhador responsável pelo ensino para aceitar a função em vez do arquivo de origem JavaScript". A resposta também for anunciado aqui
Rob W

Eu desenvolvi o task.js para tornar isso muito mais fácil. Na maioria das vezes, você está apenas tentando descarregar pequenas tarefas de bloqueio.
Chad Scira

Respostas:


225

http://www.html5rocks.com/en/tutorials/workers/basics/#toc-inlineworkers

E se você quiser criar seu script de trabalho em tempo real ou criar uma página independente sem precisar criar arquivos de trabalho separados? Com Blob (), você pode "incorporar" seu trabalhador no mesmo arquivo HTML que sua lógica principal, criando um identificador de URL para o código do trabalhador como uma string


Exemplo completo de trabalhador embutido BLOB:

<!DOCTYPE html>
<script id="worker1" type="javascript/worker">
  // This script won't be parsed by JS engines because its type is javascript/worker.
  self.onmessage = function(e) {
    self.postMessage('msg from worker');
  };
  // Rest of your worker code goes here.
</script>
<script>
  var blob = new Blob([
    document.querySelector('#worker1').textContent
  ], { type: "text/javascript" })

  // Note: window.webkitURL.createObjectURL() in Chrome 10+.
  var worker = new Worker(window.URL.createObjectURL(blob));
  worker.onmessage = function(e) {
    console.log("Received: " + e.data);
  }
  worker.postMessage("hello"); // Start the worker.
</script>


Solução única do Google Chrome, parece que o Firefox 10 irá suportá-lo, não sei sobre outros navegadores
4esn0k

2
BlobBuiler agora está obsoleto . Use Blob . Atualmente suportado no Firefox / WebKit / Opera e IE10 mais recentes, consulte as tabelas de compatibilidade para navegadores mais antigos.
Félix Saparelli

3
O construtor de blob pode ser suportado no IE10, mas você ainda não pode passar o javascript para o trabalhador da Web por meio dele (nem mesmo no IE11): connect.microsoft.com/IE/feedback/details/801810/… .
Jayarjo # 7/14

1
@albanx -que testes? já existem bilhões de páginas de demonstração on-line, o que mostra que a segmentação não desliga o navegador por anos.
vsync 21/10

2
@ albanx - você gostaria de dizer pelo menos qual navegador esotérico você usa e que trava? esta demo trava para você? ie.microsoft.com/testdrive/Graphics/WorkerFountains/…
vsync

162

A solução html5rocks de incorporar o código de trabalho da Web em HTML é bastante horrível.
E um blob de JavaScript escapado como uma string não é melhor, principalmente porque complica o fluxo de trabalho (o compilador Closure não pode operar em strings).

Pessoalmente, eu realmente gosto dos métodos toString, mas @ dan-man ISSO regex!

Minha abordagem preferida:

// Build a worker from an anonymous function body
var blobURL = URL.createObjectURL( new Blob([ '(',

function(){
    //Long-running work here
}.toString(),

')()' ], { type: 'application/javascript' } ) ),

worker = new Worker( blobURL );

// Won't be needing this anymore
URL.revokeObjectURL( blobURL );

Suporte é a interseção dessas três tabelas:

No entanto, isso não funcionará para um SharedWorker , porque o URL deve ser uma correspondência exata, mesmo se o parâmetro opcional 'name' corresponder. Para um SharedWorker, você precisará de um arquivo JavaScript separado.


Atualização de 2015 - chega a singularidade do ServiceWorker

Agora, há uma maneira ainda mais poderosa de resolver esse problema. Novamente, armazene o código do trabalhador como uma função (em vez de uma sequência estática) e converta usando .toString () e insira o código no CacheStorage em uma URL estática de sua escolha.

// Post code from window to ServiceWorker...
navigator.serviceWorker.controller.postMessage(
 [ '/my_workers/worker1.js', '(' + workerFunction1.toString() + ')()' ]
);

// Insert via ServiceWorker.onmessage. Or directly once window.caches is exposed
caches.open( 'myCache' ).then( function( cache )
{
 cache.put( '/my_workers/worker1.js',
  new Response( workerScript, { headers: {'content-type':'application/javascript'}})
 );
});

Existem dois possíveis substitutos. ObjectURL como acima, ou mais perfeitamente, coloque um arquivo JavaScript real em /my_workers/worker1.js

As vantagens dessa abordagem são:

  1. SharedWorkers também podem ser suportados.
  2. As guias podem compartilhar uma única cópia em cache em um endereço fixo. A abordagem de blob prolifera objectURLs aleatórios para cada guia.

4
Como seria a compatibilidade do navegador nesta solução?
quer

Você pode elaborar essa solução, como ela funciona? O que é o worker1.js? É um arquivo js separado? Estou tentando usar isso, mas incapaz de fazê-lo funcionar. Especificamente, estou tentando fazê-lo funcionar para um SharedWorker
Yehuda

Se você pudesse envolvê-lo em uma função útil!
mmm

@ Ben Dilts: Compatibilidade do navegador seria parecido com apenas executar o código através de babel: babeljs.io/repl
Jack Giffin

O padrão não garante que Function.prototype.toString () retorne o corpo da função como string. Você provavelmente deve adicionar um aviso à resposta.
RD

37

Você pode criar um único arquivo JavaScript que esteja ciente de seu contexto de execução e possa atuar como um script pai e um trabalhador. Vamos começar com uma estrutura básica para um arquivo como este:

(function(global) {
    var is_worker = !this.document;
    var script_path = is_worker ? null : (function() {
        // append random number and time to ID
        var id = (Math.random()+''+(+new Date)).substring(2);
        document.write('<script id="wts' + id + '"></script>');
        return document.getElementById('wts' + id).
            previousSibling.src;
    })();
    function msg_parent(e) {
        // event handler for parent -> worker messages
    }
    function msg_worker(e) {
        // event handler for worker -> parent messages
    }
    function new_worker() {
        var w = new Worker(script_path);
        w.addEventListener('message', msg_worker, false);
        return w;
    }
    if (is_worker)
        global.addEventListener('message', msg_parent, false);

    // put the rest of your library here
    // to spawn a worker, use new_worker()
})(this);

Como você pode ver, o script contém todo o código do ponto de vista do pai e do trabalhador, verificando se sua própria instância individual é um trabalhador !document. O script_pathcálculo um tanto pesado é usado para calcular com precisão o caminho do script em relação à página pai, pois o caminho fornecido new Workeré relativo à página pai, não ao script.


4
Seu site parece ter desaparecido; você tem um novo URL?
27412 BrianFreud

1
Esta é uma abordagem interessante. FWIW, detecto os Trabalhadores da Web por meio de verificação da presença de "self" (o objeto global do Trabalhador da Web) vs "janela".
Pwnall

Eu estive examinando como o PapaParse lida com Web Workers e eles parecem adotar essa abordagem github.com/mholt/PapaParse
JP DeVries

Eu acho que o teste usando 'typeof importScripts! == null' pode dizer se o script está sendo executado no escopo do trabalhador.
MeTTeO 15/09/2015

1
Eu não entendo o que o previousSibling é do elemento de script. Alguém pode me explicar?
Teemoh #

28

Usando o Blobmétodo, que tal isso para uma fábrica de trabalhadores:

var BuildWorker = function(foo){
   var str = foo.toString()
             .match(/^\s*function\s*\(\s*\)\s*\{(([\s\S](?!\}$))*[\s\S])/)[1];
   return  new Worker(window.URL.createObjectURL(
                      new Blob([str],{type:'text/javascript'})));
}

Então você poderia usá-lo assim ...

var myWorker = BuildWorker(function(){
   //first line of worker
   self.onmessage(){....};
   //last line of worker
});

EDITAR:

Acabei de estender essa idéia ainda mais para facilitar a comunicação entre threads: bridged-worker.js .

EDIT 2:

O link acima é para uma essência que eu criei. Mais tarde, alguém o transformou em um repo real .


11

Trabalhadores da Web operam em contextos totalmente separados como programas individuais.

Isso significa que o código não pode ser movido de um contexto para outro na forma de objeto, pois eles poderiam fazer referência a objetos por meio de fechamentos pertencentes ao outro contexto.
Isso é especialmente crucial, pois o ECMAScript foi projetado para ser uma linguagem de encadeamento único e, como os trabalhadores da Web operam em encadeamentos separados, você terá o risco de executar operações não seguras para encadeamento.

Isso novamente significa que os trabalhadores da Web precisam ser inicializados com o código no formato de origem.

A especificação do WHATWG diz

Se a origem do URL absoluto resultante não for igual à origem do script de entrada, lance uma exceção SECURITY_ERR.

Portanto, os scripts devem ser arquivos externos com o mesmo esquema da página original: você não pode carregar um script a partir de um dado data: URL ou javascript: URL, e uma página https: não pôde iniciar trabalhadores usando scripts com http: URLs.

mas, infelizmente, isso não explica realmente por que não se pode ter permitido passar uma string com código-fonte para o construtor.


6

uma maneira melhor de ler para um trabalhador em linha ..

    var worker_fn = function(e) 
    {
        self.postMessage('msg from worker');            
    };

    var blob = new Blob(["onmessage ="+worker_fn.toString()], { type: "text/javascript" });

    var worker = new Worker(window.URL.createObjectURL(blob));
    worker.onmessage = function(e) 
    {
       alert(e.data);
    };
    worker.postMessage("start"); 

O que fiz foi criar uma função com todo o código do trabalhador, passar essa função toString(), extrair o corpo do corpo e colocá-lo em um Blob. Verifique na última resposta, eu tenho um exemplo #
Fernando Carvajal

5

Pegar a resposta da Adria e colocá-la em uma função de copiar e colar que funciona com o Chrome e o FF atuais, mas não o IE10 (o trabalhador do blob causa um erro de segurança ).

var newWorker = function (funcObj) {
    // Build a worker from an anonymous function body
    var blobURL = URL.createObjectURL(new Blob(
        ['(', funcObj.toString(), ')()'],
        {type: 'application/javascript'}
     ));

    var worker = new Worker(blobURL);

    // Won't be needing this anymore
    URL.revokeObjectURL(blobURL);

    return worker;
}

E aqui está um exemplo de trabalho http://jsfiddle.net/ubershmekel/YYzvr/


5

Resposta recente (2018)

Você pode usar o Greenlet :

Mova uma função assíncrona para seu próprio encadeamento. Uma versão simplificada de função única do Workerize .

Exemplo:

import greenlet from 'greenlet'

const getName = greenlet(async username => {
  const url = `https://api.github.com/users/${username}`
  const res = await fetch(url)
  const profile = await res.json()
  return profile.name
})

console.log(await getName('developit'))

3

Dependendo do seu caso de uso, você pode usar algo como

task.js Interface simplificada para a execução de código intensivo da CPU em todos os núcleos (node.js e web)

Um exemplo seria

function blocking (exampleArgument) {
    // block thread
}

// turn blocking pure function into a worker task
const blockingAsync = task.wrap(blocking);

// run task on a autoscaling worker pool
blockingAsync('exampleArgumentValue').then(result => {
    // do something with result
});

2

Dê uma olhada no plugin vkThread. Com o htis plugin, você pode pegar qualquer função no código principal e executá-la em um thread (web worker). Portanto, você não precisa criar um "arquivo de trabalho da web" especial.

http://www.eslinstructor.net/vkthread/

--Vadim


1

Você pode usar trabalhadores da Web no mesmo campo javascript usando trabalhadores da Web embutidos.

O artigo abaixo abordará você para entender facilmente os trabalhadores da web e suas limitações e depuração de trabalhadores da web.

Domínio em webworkers


1

Eu acho que a melhor maneira de fazer isso é usando um objeto Blob, abaixo você pode ver um exemplo simples.

// create a Blob object with a worker code
var blob = new Blob(["onmessage = function(e) { postMessage('msg from worker'); }"]);

// Obtain a blob URL reference to our worker 'file'.
var blobURL = window.URL.createObjectURL(blob);

// create a Worker
var worker = new Worker(blobURL);
worker.onmessage = function(e) {
  console.log(e.data);
};
worker.postMessage("Send some Data"); 


1

aqui console:

var worker=new Worker(window.URL.createObjectURL(new Blob([function(){
  //Long-running work here
  postMessage('done');
}.toString().split('\n').slice(1,-1).join('\n')],{type:'text/javascript'})));

worker.addEventListener('message',function(event){
  console.log(event.data);
});

1

https://developer.mozilla.org/es/docs/Web/Guide/Performance/Using_web_workers

    // Syntax: asyncEval(code[, listener])

var asyncEval = (function () {

  var aListeners = [], oParser = new Worker("data:text/javascript;charset=US-ASCII,onmessage%20%3D%20function%20%28oEvent%29%20%7B%0A%09postMessage%28%7B%0A%09%09%22id%22%3A%20oEvent.data.id%2C%0A%09%09%22evaluated%22%3A%20eval%28oEvent.data.code%29%0A%09%7D%29%3B%0A%7D");

  oParser.onmessage = function (oEvent) {
    if (aListeners[oEvent.data.id]) { aListeners[oEvent.data.id](oEvent.data.evaluated); }
    delete aListeners[oEvent.data.id];
  };


  return function (sCode, fListener) {
    aListeners.push(fListener || null);
    oParser.postMessage({
      "id": aListeners.length - 1,
      "code": sCode
    });
  };

})();


1

Então, acho que temos outra opção interessante para isso agora, graças aos literais de modelo no ES6. Isso nos permite dispensar a função extra worker (e seu escopo estranho) e escrever o código que se destina ao trabalhador como texto com várias linhas, como no caso em que estávamos usando para armazenar texto, mas sem precisar de um documento ou DOM. para fazer isso. Exemplo:

const workerScript = `
self.addEventListener('message', function(e) {
  var data = e.data;
  console.log('worker recieved: ',data);
  self.postMessage('worker added! :'+ addOne(data.value));
  self.close();//kills the worker
}, false);
`;

Aqui está um resumo do restante dessa abordagem .

Observe que podemos extrair quaisquer dependências de função extras que desejamos para o trabalhador, coletando-as em uma matriz e executando .toString em cada uma delas para reduzi-las também em strings (deve funcionar desde que sejam declarações de função) e anexando isso à string de script. Dessa forma, não precisamos importar scripts que talvez já incluíssemos no escopo do código que estamos escrevendo.

A única desvantagem real dessa versão em particular é que os linters não serão capazes de desvendar o código do operador de serviço (já que é apenas uma string), o que é uma vantagem para a "abordagem da função de operador separada".


1

Isso é apenas uma adição ao que foi mencionado acima - tenho ótimos modelos para testar os trabalhadores da web no jsFiddle. Em vez de Blob, ele usa a ?jsAPI jsFiddles :

function workerFN() {
  self.onmessage = function(e) {
    switch(e.data.name) {
      case "" : 
      break;
      default:
        console.error("Unknown message:", e.data.name);
    }
  }
}
// This is a trick to generate real worker script that is loaded from server
var url = "/echo/js/?js="+encodeURIComponent("("+workerFN.toString()+")()");
var worker = new Worker(url);
worker.addEventListener("message", function(e) {
  switch(e.data.name) {
    case "" : 
    break;
    default:
      console.error("Unknown message:", e.data.name);
  }
})

Modelos de trabalhador da Web normal e de trabalhador compartilhado estão disponíveis.


1

Descobri que o CodePen atualmente não possui <script>tags embutidas de destaque de sintaxe que não são type="text/javascript"(ou que não têm nenhum atributo de tipo).

Então, eu criei uma solução semelhante, mas um pouco diferente, usando blocos rotulados com break, que é a única maneira de recuperar uma <script>marca sem criar uma função de invólucro (o que é desnecessário).

<!DOCTYPE html>
<script id="worker1">
  worker: { // Labeled block wrapper

    if (typeof window === 'object') break worker; // Bail if we're not a Worker

    self.onmessage = function(e) {
      self.postMessage('msg from worker');
    };
    // Rest of your worker code goes here.
  }
</script>
<script>
  var blob = new Blob([
    document.querySelector('#worker1').textContent
  ], { type: "text/javascript" })

  // Note: window.webkitURL.createObjectURL() in Chrome 10+.
  var worker = new Worker(window.URL.createObjectURL(blob));
  worker.onmessage = function(e) {
    console.log("Received: " + e.data);
  }
  worker.postMessage("hello"); // Start the worker.
</script>


1

Uma versão promissificada simples,, Function#callAsWorkerque usa thisArg e argumentos (exatamente como call) e retorna uma promessa:

Function.prototype.callAsWorker = function (...args) {
    return new Promise( (resolve, reject) => {
        const code = `self.onmessage = e => self.postMessage((${this.toString()}).call(...e.data));`,
            blob = new Blob([code], { type: "text/javascript" }),
            worker = new Worker(window.URL.createObjectURL(blob));
        worker.onmessage = e => (resolve(e.data), worker.terminate());
        worker.onerror = e => (reject(e.message), worker.terminate());
        worker.postMessage(args);
    });
}

// Demo
function add(...nums) {
    return nums.reduce( (a,b) => a+b );
}
// Let the worker execute the above function, with the specified arguments
add.callAsWorker(null, 1, 2, 3).then(function (result) {
    console.log('result: ', result);
});


você deve adicionar um close()método para fechar o gancho de vida do trabalhador da web. developer.mozilla.org/pt-BR/docs/Web/API/WorkerGlobalScope/…
Shahar ン ー ン Levi

@ Shahar ン ン i Levi, a closefunção está obsoleta. No entanto, os trabalhadores podem ser demitidos . Eu adicionei isso agora.
trincot

0

Eu uso código como este, você pode definir sua mensagem como uma função que não seja texto simples, para que o editor possa destacar seu código e o jshint funcionar.

const worker = createWorker();

createWorker() {
    const scriptContent = getWorkerScript();
    const blob = new Blob([
        scriptContent,
    ], {
        type: "text/javascipt"
    });
    const worker = new Worker(window.URL.createObjectURL(blob));
    return worker;
}

getWorkerScript() {
    const script = {
        onmessage: function (e) {
            console.log(e);
            let result = "Hello " + e.data
            postMessage(result);
        }
    };
    let content = "";
    for (let prop in script){
        content += `${prop}=${script[prop].toString()}`;
    }
    return content;
}


Olhe para a minha resposta , acabei de fazer isso, mas escrevi uma classe inteira para abstrair como passar chamadas de retorno.
Fernando Carvajal

0

Sim, é possível, eu fiz isso usando arquivos Blob e passando um retorno de chamada

Mostrarei a você o que uma classe que escrevi faz e como ela gerencia a execução de retornos de chamada em segundo plano.

Primeiro você instancia o GenericWebWorkercom os dados que você gostaria de passar para o retorno de chamada que será executado no Web Worker, que inclui as funções que você deseja usar, neste caso, um número, uma data e uma função chamadablocker

var worker = new GenericWebWorker(100, new Date(), blocker)

Essa função bloqueadora executará um tempo infinito por n milissegundos

function blocker (ms) {
    var now = new Date().getTime();
    while(true) {
        if (new Date().getTime() > now +ms)
            return;
    }   
}

e então você usa assim

worker.exec((num, date, fnBlocker) => {
    /*Everithing here does not block the main thread
      and this callback has access to the number, date and the blocker */
    fnBlocker(10000) //All of this run in backgrownd
    return num*10

}).then(d => console.log(d)) //Print 1000

Agora, é hora de ver a mágica no exemplo abaixo

/*https://github.com/fercarvo/GenericWebWorker*/
class GenericWebWorker {
    constructor(...ags) {
        this.args = ags.map(a => (typeof a == 'function') ? {type:'fn', fn:a.toString()} : a)
    }

    async exec(cb) {
        var wk_string = this.worker.toString();
        wk_string = wk_string.substring(wk_string.indexOf('{') + 1, wk_string.lastIndexOf('}'));            
        var wk_link = window.URL.createObjectURL( new Blob([ wk_string ]) );
        var wk = new Worker(wk_link);

        wk.postMessage({ callback: cb.toString(), args: this.args });
 
        var resultado = await new Promise((next, error) => {
            wk.onmessage = e => (e.data && e.data.error) ? error(e.data.error) : next(e.data);
            wk.onerror = e => error(e.message);
        })

        wk.terminate(); window.URL.revokeObjectURL(wk_link);
        return resultado
    }

    async parallel(arr, cb) {
        var res = [...arr].map(it => new GenericWebWorker(it, ...this.args).exec(cb))
        var all = await Promise.all(res)
        return all
    }

    worker() {
        onmessage = async function (e) {
            try {                
                var cb = new Function(`return ${e.data.callback}`)();
                var args = e.data.args.map(p => (p.type == 'fn') ? new Function(`return ${p.fn}`)() : p);

                try {
                    var result = await cb.apply(this, args); //If it is a promise or async function
                    return postMessage(result)

                } catch (e) { throw new Error(`CallbackError: ${e}`) }
            } catch (e) { postMessage({error: e.message}) }
        }
    }
}


function blocker (ms) {
    var now = new Date().getTime();
    while(true) {
        if (new Date().getTime() > now +ms)
            return;
    }   
}

setInterval(()=> console.log("Not blocked " + Math.random()), 1000)

console.log("\n\nstarting blocking code in Worker\n\n")

var worker = new GenericWebWorker(100, new Date(), blocker)

worker.exec((num, date, fnBlocker) => {
    fnBlocker(7000) //All of this run in backgrownd
    return num*10    
})
.then(d => console.log(`\n\nEnd of blocking code: result ${d}\n\n`)) //Print 1000


0

Você pode colocar o conteúdo do seu arquivo worker.js dentro de backticks (que permite uma constante de cadeia de linhas múltiplas) e criar o trabalhador a partir de um blob como este:

var workerScript = `
    self.onmessage = function(e) {
        self.postMessage('message from worker');
    };
    // rest of worker code goes here
`;

var worker =
    new Worker(createObjectURL(new Blob([workerScript], { type: "text/javascript" })));

Isso é útil se, por qualquer motivo, você não quiser ter tags de script separadas para o trabalhador.


0

Outra solução é apenas agrupar o Worker em uma função e criar um blob chamando a função da seguinte maneira:

     function workerCode() {
        self.onmessage = function (e) {
          console.log("Got message from parent", e.data);
        };
        setTimeout(() => {
          self.postMessage("Message From Worker");
        }, 2000);
      }

      let blob = new Blob([
        "(" + workerCode.toString() + ")()"
      ], {type: "text/javascript"});

      // Note: window.webkitURL.createObjectURL() in Chrome 10+.
      let worker = new Worker(window.URL.createObjectURL(blob));
      worker.onmessage = function (e) {
        console.log("Received: " + e.data);
      };
      worker.postMessage("hello"); // Start the worker.

-1

Uma linha para executar funções em trabalhadores:

const FunctionalWorker = fn => new Worker(window.URL.createObjectURL(new Blob(["(" + workerCode.toString() + ")()"], {type: "text/javascript"})));

Exemplo de uso:

let fn = FunctionalWorker(() => {
    self.postMessage("hi");
});
fn.onmessage = msg => {
    console.log(msg);
};
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.