Lavoratori Web senza un file Javascript separato?


291

Per quanto ne so, gli operatori web devono essere scritti in un file JavaScript separato e chiamati in questo modo:

new Worker('longrunning.js')

Sto usando il compilatore di chiusura per combinare e minimizzare tutto il mio codice sorgente JavaScript e preferirei non avere i miei lavoratori in file separati per la distribuzione. C'è un modo per farlo?

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

Dato che le funzioni di prima classe sono così cruciali per JavaScript, perché il modo standard di eseguire il lavoro in background deve caricare un intero altro file JavaScript dal server Web?


7
È perché mantenere un contesto di esecuzione puramente sicuro per i thread è ancora più cruciale delle funzioni di prima classe :-)
Pointy

1
Ci sto lavorando (o piuttosto sulla minimizzazione del problema): DynWorker . Puoi fare: var worker = new DynWorker(); worker.inject("foo", function(){...});...
Félix Saparelli l'


1
L'OP ha eliminato la domanda "Insegnante per accettare la funzione anziché il file di origine JavaScript". La risposta è ripubblicata qui
Rob W

Ho sviluppato task.js per renderlo molto più facile da fare. Il più delle volte stai solo provando a scaricare piccole attività di blocco.
Chad Scira,

Risposte:


225

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

Cosa succede se si desidera creare al volo lo script di lavoro o creare una pagina autonoma senza dover creare file di lavoro separati? Con Blob (), puoi "incorporare" il tuo lavoratore nello stesso file HTML della tua logica principale creando un handle URL per il codice di lavoro come una stringa


Esempio completo di lavoratore inline 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>


L'unica soluzione di Google Chrome, sembra che Firefox 10 la supporterà, non conosco altri browser
4esn0k

2
BlobBuiler è ora obsoleto . Utilizzare invece BLOB . Attualmente supportato negli ultimi Firefox / WebKit / Opera e IE10, consultare le tabelle di compatibilità per i browser meno recenti.
Félix Saparelli,

3
Il costruttore BLOB potrebbe essere supportato in IE10, ma non è ancora possibile passare javascript al Web worker attraverso di esso (nemmeno in IE11): connect.microsoft.com/IE/feedback/details/801810/… .
jayarjo,

1
@albanx -che test? ci sono già miliardi di pagine dimostrative online che mostrano che il threading non blocca il browser da anni.
vsync,

2
@albanx - ti andrebbe almeno di dire quale browser esoterico usi quale blocco? questa demo si blocca per te? ie.microsoft.com/testdrive/Graphics/WorkerFountains/…
vsync

162

La soluzione html5rocks di incorporare il codice Web worker in HTML è abbastanza orribile.
E un blocco di JavaScript sfuggito a una stringa non è migliore, anche perché complica il flusso di lavoro (il compilatore di chiusura non può operare su stringhe).

Personalmente mi piacciono molto i metodi toString, ma @ dan-man CHE regex!

Il mio approccio preferito:

// 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 );

Il supporto è l'intersezione di queste tre tabelle:

Tuttavia, ciò non funzionerà per un SharedWorker , poiché l'URL deve corrispondere esattamente, anche se il parametro facoltativo "name" corrisponde. Per un SharedWorker, avrai bisogno di un file JavaScript separato.


Aggiornamento 2015 - Arriva la singolarità di ServiceWorker

Ora c'è un modo ancora più potente per risolvere questo problema. Ancora una volta, memorizza il codice di lavoro come una funzione (anziché una stringa statica) e converti utilizzando .toString (), quindi inserisci il codice in CacheStorage sotto un URL statico di tua scelta.

// 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'}})
 );
});

Esistono due possibili fallback. ObjectURL come sopra, o più facilmente, metti un vero file JavaScript in /my_workers/worker1.js

I vantaggi di questo approccio sono:

  1. SharedWorkers può anche essere supportato.
  2. Le schede possono condividere una singola copia memorizzata nella cache a un indirizzo fisso. L'approccio BLOB prolifera oggettiURL casuali per ogni scheda.

4
Come sarebbe la compatibilità del browser su questa soluzione?
Ben Dilts,

Puoi approfondire questa soluzione, come funziona? Che cos'è worker1.js? È un file js separato? Sto cercando di usare questo, ma non riesco a farlo funzionare. In particolare, sto cercando di farlo funzionare per un SharedWorker
Yehuda il

Se solo potessi avvolgerlo in una funzione utile!
mmm,

@ Ben Dilts: la compatibilità del browser sembrerebbe semplicemente come eseguire il tuo codice tramite babel: babeljs.io/repl
Jack Giffin

Lo standard non garantisce che Function.prototype.toString () restituisce il corpo della funzione come stringa. Probabilmente dovresti aggiungere un avviso alla risposta.
RD

37

È possibile creare un singolo file JavaScript che sia a conoscenza del suo contesto di esecuzione e possa fungere sia da script padre che da lavoratore. Cominciamo con una struttura di base per un file come questo:

(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);

Come puoi vedere, lo script contiene tutto il codice sia per il punto di vista del genitore che del lavoratore, verificando se la sua singola istanza è un lavoratore con !document. Il script_pathcalcolo un po 'ingombrante viene utilizzato per calcolare con precisione il percorso dello script relativo alla pagina principale, poiché il percorso fornito new Workerè relativo alla pagina principale, non allo script.


4
Il tuo sito sembra essere svanito; hai un nuovo URL?
BrianFreud,

1
Questo è un approccio interessante. FWIW, rilevo i Web Worker controllando la presenza di "self" (l'oggetto globale di Web Worker) rispetto a "window".
pwnall,

Ho esaminato come PapaParse gestisce i Web Worker e sembrano adottare questo approccio github.com/mholt/PapaParse
JP DeVries,

Penso che il test usando 'typeof importScripts! == null' possa dire se lo script è in esecuzione in ambito worker.
MeTTeO,

1
Non capisco quale sia il precedente fratello dall'elemento script. Qualcuno può spiegarmi?
Teemoh,

28

Utilizzando il Blobmetodo, che ne dici di questo per una fabbrica di lavoratori:

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'})));
}

Quindi potresti usarlo in questo modo ...

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

MODIFICARE:

Ho appena ampliato ulteriormente questa idea per rendere più semplice la comunicazione cross-thread: bridged-worker.js .

MODIFICA 2:

Il link qui sopra è per una sintesi che ho creato. Qualcun altro in seguito lo ha trasformato in un vero repository .


11

I web worker operano in contesti completamente separati come singoli programmi.

Ciò significa che il codice non può essere spostato da un contesto a un altro in forma di oggetto, poiché sarebbero quindi in grado di fare riferimento a oggetti tramite chiusure appartenenti all'altro contesto.
Ciò è particolarmente cruciale in quanto ECMAScript è progettato per essere un linguaggio a thread singolo e poiché i web worker operano in thread separati, si rischia quindi di eseguire operazioni non thread-safe.

Ciò significa che gli operatori Web devono essere inizializzati con il codice in formato sorgente.

Le specifiche di WHATWG dicono

Se l'origine dell'URL assoluto risultante non è la stessa dell'origine dello script di immissione, genera un'eccezione SECURITY_ERR.

Pertanto, gli script devono essere file esterni con lo stesso schema della pagina originale: non è possibile caricare uno script da un dato: URL o javascript: URL e una pagina https: non può avviare i lavoratori che utilizzano script con http: URL.

ma sfortunatamente non spiega davvero perché non si possa permettere al costruttore di passare una stringa con codice sorgente.


6

un modo migliore per leggere un lavoratore in linea ..

    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"); 

Quello che ho fatto è stato creare una funzione con tutto il codice di lavoro, passare quella funzione toString(), estrapolare il corpo e quindi inserirlo in un BLOB. Controlla l'ultima risposta, ho un esempio
Fernando Carvajal,

5

Prendere la risposta di Adria e metterla in una funzione copia-pasta che funziona con Chrome e FF attuali ma non con IE10 (il lavoratore dal BLOB provoca un errore di sicurezza ).

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;
}

Ed ecco un esempio funzionante http://jsfiddle.net/ubershmekel/YYzvr/


5

Risposta recente (2018)

Puoi usare Greenlet :

Sposta una funzione asincrona nel proprio thread. Una versione semplificata a funzione singola di Workerize .

Esempio:

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

A seconda del tuo caso d'uso puoi usare qualcosa del genere

task.js Interfaccia semplificata per l'esecuzione del codice intensivo della CPU su tutti i core (node.js e web)

Un esempio sarebbe

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

Dai un'occhiata al plugin vkThread. Con il plugin htis puoi prendere qualsiasi funzione nel tuo codice principale ed eseguirla in un thread (web worker). Pertanto, non è necessario creare uno speciale "file di lavoro sul Web".

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

--Vadim


1

Puoi usare i web worker nello stesso javascript usando i webworker in linea.

L'articolo di seguito ti indirizzerà per comprendere facilmente i webworker e le loro limitazioni e il debug dei webworker.

Master in webworker


1

Penso che il modo migliore per farlo sia usare un oggetto BLOB, di seguito puoi vedere un semplice esempio.

// 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

qui 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

Quindi penso che ora abbiamo un'altra fantastica opzione per questo, grazie ai template letterali in ES6. Ciò ci consente di rinunciare alla funzione lavoratore aggiuntiva (e al suo strano ambito) e di scrivere semplicemente il codice che è destinato al lavoratore come testo multilinea, proprio come nel caso in cui stavamo usando per archiviare il testo, ma senza realmente bisogno di un documento o DOM per farlo dentro. Esempio:

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);
`;

Ecco una sintesi del resto di questo approccio .

Si noti che possiamo inserire tutte le dipendenze di funzioni extra che vogliamo nel lavoratore semplicemente raccogliendole in un array ed eseguendo .toString su ognuna di esse per ridurle anche in stringhe (dovrebbero funzionare purché siano dichiarazioni di funzione) e quindi basta anteporre quello alla stringa di script. In questo modo non è necessario importare Script che potremmo già aver raggruppato nell'ambito del codice che stiamo scrivendo.

L'unico vero svantaggio di questa particolare versione è che i linter non saranno in grado di sfilare il codice di servizio (dal momento che è solo una stringa), il che è un vantaggio per "l'approccio di funzione lavoratore separato".


1

Questa è solo un'aggiunta a quanto sopra: ho dei bei template per testare i web worker in jsFiddle. Invece di Blob utilizza jsFiddles ?jsapi:

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);
  }
})

Sono disponibili modelli normali di Web worker e di lavoro condiviso .


1

Ho scoperto che CodePen al momento non evidenzia la sintassi evidenziando i <script>tag inline che non sono type="text/javascript"(o che non hanno un attributo di tipo).

Quindi ho escogitato una soluzione simile ma leggermente diversa usando i blocchi etichettati con break, che è l'unico modo in cui puoi salvare da un <script>tag senza creare una funzione wrapper (che non è necessario).

<!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

Una semplice versione promessa Function#callAsWorker, che accetta un thisArg e argomenti (proprio come call) e restituisce una 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);
});


dovresti aggiungere un close()metodo per chiudere il gancio di vita del lavoratore web. developer.mozilla.org/en-US/docs/Web/API/WorkerGlobalScope/…
Shahar ド ー ン Levi

@Shahar ド ー ン Levi, la closefunzione è obsoleta. Tuttavia, i lavoratori possono essere licenziati . L'ho aggiunto ora.
trincot

0

Uso il codice in questo modo, puoi definire il tuo messaggio come una funzione diversa dal testo normale, quindi l'editor può evidenziare il tuo codice e le opere di jshint.

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;
}


Guarda la mia risposta , l'ho appena fatto ma ho scritto un'intera classe per astrarre come superare i callback
Fernando Carvajal,

0

Sì, è possibile, l'ho fatto usando i file BLOB e passando un callback

Ti mostrerò cosa fa una classe che ho scritto e come gestisce l'esecuzione dei callback in background.

Innanzitutto si crea un'istanza GenericWebWorkercon tutti i dati che si desidera passare al callback che verranno eseguiti in Web Worker, che include le funzioni che si desidera utilizzare, in questo caso un numero, una data e una funzione chiamatablocker

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

Questa funzione di blocco eseguirà un infinito mentre per n milisecondi

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

e poi lo usi così

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

Ora, è tempo di vedere la magia nell'esempio qui sotto

/*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

È possibile posizionare il contenuto del file worker.js all'interno di backtick (che consente una costante di stringa su più righe) e creare il worker da un BLOB in questo modo:

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" })));

Ciò è utile se per qualsiasi motivo non si desidera avere tag di script separati per il lavoratore.


0

Un'altra soluzione è semplicemente avvolgere il lavoratore in una funzione, quindi creare un BLOB che invoca la funzione in questo modo:

     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

One-liner per le funzioni di funzionamento nei lavoratori:

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

Esempio di utilizzo:

let fn = FunctionalWorker(() => {
    self.postMessage("hi");
});
fn.onmessage = msg => {
    console.log(msg);
};
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.