¿Trabajadores web sin un archivo Javascript separado?


291

Por lo que puedo decir, los trabajadores web deben escribirse en un archivo JavaScript separado y llamarse así:

new Worker('longrunning.js')

Estoy usando el compilador de cierre para combinar y minimizar todo mi código fuente JavaScript, y prefiero no tener que tener a mis trabajadores en archivos separados para su distribución. Hay alguna manera de hacer esto?

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

Dado que las funciones de primera clase son tan cruciales para JavaScript, ¿por qué la forma estándar de trabajo en segundo plano tiene que cargar otro archivo JavaScript completo desde el servidor web?


77
Es porque mantener un contexto de ejecución puramente seguro es incluso más crucial que las funciones de primera clase :-)
Pointy

1
Estoy trabajando en ello (o más bien en minimizar el problema): DynWorker . Puedes hacer: var worker = new DynWorker(); worker.inject("foo", function(){...});...
Félix Saparelli


1
El OP eliminó la pregunta "Trabajador de enseñanza para aceptar la función en lugar del archivo fuente JavaScript". La respuesta se vuelve a publicar aquí
Rob W

Desarrollé task.js para hacer esto mucho más fácil de hacer. La mayoría de las veces solo intentas descargar pequeñas tareas de bloqueo.
Chad Scira

Respuestas:


225

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

¿Qué sucede si desea crear su script de trabajo sobre la marcha o crear una página independiente sin tener que crear archivos de trabajo separados? Con Blob (), puede "alinear" a su trabajador en el mismo archivo HTML que su lógica principal creando un identificador de URL para el código del trabajador como una cadena


Ejemplo completo de trabajador en línea 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>


Solución única de Google Chrome, parece que Firefox 10 lo admitirá, no sé sobre otros navegadores
4esn0k

2
BlobBuiler ahora está en desuso . Use Blob en su lugar. Actualmente compatible con los últimos Firefox / WebKit / Opera e IE10, consulte las tablas de compatibilidad para navegadores antiguos.
Félix Saparelli

3
El constructor de blobs puede ser compatible con IE10, pero aún no puede pasar javascript al trabajador web a través de él (ni siquiera en IE11): connect.microsoft.com/IE/feedback/details/801810/… .
jayarjo

1
@albanx -¿qué pruebas? Ya hay miles de millones de páginas de demostración en línea que muestran que los subprocesos no cuelgan el navegador durante años.
vsync

2
@albanx: ¿le gustaría decir al menos qué navegador esotérico utiliza y qué cuelga? ¿Esta demo te cuelga? ie.microsoft.com/testdrive/Graphics/WorkerFountains/…
vsync

162

La solución html5rocks de incrustar el código del trabajador web en HTML es bastante horrible.
Y una gota de JavaScript escapado como cadena no es mejor, sobre todo porque complica el flujo de trabajo (el compilador de cierre no puede funcionar en cadenas).

Personalmente, me gustan mucho los métodos toString, pero @ dan-man ¡ ESA expresión regular!

Mi enfoque preferido:

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

El soporte es la intersección de estas tres tablas:

Sin embargo, esto no funcionará para un SharedWorker , porque la URL debe ser una coincidencia exacta, incluso si coincide el parámetro opcional 'nombre'. Para un SharedWorker, necesitará un archivo JavaScript separado.


Actualización 2015: llega la singularidad ServiceWorker

Ahora hay una forma aún más poderosa de resolver este problema. Nuevamente, almacene el código de trabajo como una función (en lugar de una cadena estática) y convierta usando .toString (), luego inserte el código en CacheStorage bajo una URL estática de su elección.

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

Hay dos posibles retrocesos. ObjectURL como arriba, o más fácilmente, coloque un archivo JavaScript real en /my_workers/worker1.js

Las ventajas de este enfoque son:

  1. SharedWorkers también puede ser compatible.
  2. Las pestañas pueden compartir una sola copia en caché en una dirección fija. El enfoque de blob prolifera objectURL aleatorios para cada pestaña.

44
¿Cómo sería la compatibilidad del navegador en esta solución?
Ben Dilts

¿Puedes dar más detalles sobre esta solución, cómo funciona? ¿Cuál es el trabajador1.js? ¿Es un archivo js separado? Estoy tratando de usar esto pero no puedo hacer que funcione. Específicamente estoy tratando de que funcione para un Trabajador Compartido
Yehuda

¡Ojalá pudieras envolverlo en una función útil!
mmm

@ Ben Dilts: la compatibilidad del navegador se vería como si simplemente ejecutara su código a través de babel: babeljs.io/repl
Jack Giffin

El estándar no garantiza que Function.prototype.toString () devuelva el cuerpo de la función como una cadena. Probablemente deberías agregar una advertencia a la respuesta.
RD

37

Puede crear un único archivo JavaScript que conozca su contexto de ejecución y pueda actuar como un script principal y como un trabajador. Comencemos con una estructura básica para un archivo 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 puede ver, el script contiene todo el código tanto para el punto de vista del padre como del trabajador, verificando si su propia instancia individual es un trabajador con !document. El script_pathcálculo algo difícil de manejar se utiliza para calcular con precisión la ruta del script en relación con la página principal, ya que la ruta suministrada new Workeres relativa a la página principal, no al script.


44
Su sitio parece haber desaparecido; ¿tienes una nueva URL?
BrianFreud

1
Este es un enfoque interesante. FWIW, detecto funciones Web Workers al verificar la presencia de "self" (el objeto global de Web Worker) frente a "window".
pwnall

He estado investigando cómo PapaParse maneja a los trabajadores web y parecen adoptar este enfoque github.com/mholt/PapaParse
JP DeVries

Creo que probar usando 'typeof importScripts! == null' puede decir si el script se está ejecutando en el ámbito de trabajo.
MeTTeO

1
No entiendo cuál es el anteriorSibling del elemento script. Alguien me puede explicar?
Teemoh

28

Usando el Blobmétodo, ¿qué tal esto para una fábrica de trabajadores:

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

Entonces podrías usarlo así ...

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

EDITAR:

Acabo de ampliar esta idea para facilitar la comunicación entre hilos: bridged-worker.js .

EDITAR 2:

El enlace de arriba es a una esencia que creé. Luego, alguien más lo convirtió en un repositorio real .


11

Los trabajadores web operan en contextos completamente separados como programas individuales.

Esto significa que el código no se puede mover de un contexto a otro en forma de objeto, ya que podrían hacer referencia a objetos a través de cierres que pertenecen al otro contexto.
Esto es especialmente crucial ya que ECMAScript está diseñado para ser un lenguaje de subproceso único, y dado que los trabajadores web operan en subprocesos separados, entonces correría el riesgo de que se realicen operaciones no seguras para subprocesos.

Esto nuevamente significa que los trabajadores web deben inicializarse con el código en forma de fuente.

La especificación de WHATWG dice

Si el origen de la URL absoluta resultante no es el mismo que el origen del script de entrada, arroje una excepción SECURITY_ERR.

Por lo tanto, los scripts deben ser archivos externos con el mismo esquema que la página original: no puede cargar un script desde data: URL o javascript: URL, y una página https: no podría iniciar a los trabajadores que usan scripts con http: URL.

pero desafortunadamente no explica realmente por qué uno no podría haber permitido pasar una cadena con código fuente al constructor.


6

Una mejor manera de leer para un trabajador en línea.

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

Lo que hice fue crear una función con todo el código de trabajo, pasar esa función toString(), extraer el cuerpo y luego ponerlo en un Blob. Verifique en la última respuesta, tengo un ejemplo
Fernando Carvajal

5

Tomando la respuesta de Adria y poniéndola en una función copiable que funciona con Chrome y FF actuales pero no con IE10 (el trabajador de blob causa un error de seguridad ).

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

Y aquí hay un ejemplo de trabajo http://jsfiddle.net/ubershmekel/YYzvr/


5

Respuesta reciente (2018)

Puedes usar Greenlet :

Mueva una función asíncrona a su propio hilo. Una versión simplificada de una sola función de Workerize .

Ejemplo:

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

Dependiendo de su caso de uso, puede usar algo como

task.js Interfaz simplificada para que el código intensivo de la CPU se ejecute en todos los núcleos (node.js y web)

Un ejemplo sería

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

Echa un vistazo al complemento vkThread. Con el complemento htis puede tomar cualquier función en su código principal y ejecutarla en un hilo (trabajador web). Por lo tanto, no necesita crear un "archivo de trabajador web" especial.

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

--Vadim


1

Puede usar trabajadores web en el mismo archivo javascript usando trabajadores web en línea.

El siguiente artículo se dirigirá a usted para comprender fácilmente a los trabajadores web y sus limitaciones y depuración de los trabajadores web.

Masterización en webworkers


1

Creo que la mejor manera de hacer esto es usar un objeto Blob, a continuación puedes ver un ejemplo simple.

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

aquí consola:

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

Así que creo que tenemos otra opción genial para esto ahora, gracias a los literales de plantilla en ES6. Eso nos permite prescindir de la función de trabajador adicional (y su alcance extraño) y simplemente escribir el código que está destinado al trabajador como texto multilínea, muy parecido al caso en el que estábamos usando para almacenar texto, pero sin realmente necesitar un documento o DOM hacer eso en. Ejemplo:

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

Aquí hay una idea del resto de ese enfoque .

Tenga en cuenta que podemos incorporar las dependencias de funciones adicionales que queramos en el trabajador simplemente reuniéndolas en una matriz y ejecutando .toString en cada una de ellas para reducirlas también a cadenas (debería funcionar siempre que sean declaraciones de funciones) y entonces solo anteponiendo eso a la secuencia de comandos. De esa manera, no tenemos que importar scripts que ya podríamos haber incluido en el alcance del código que estamos escribiendo.

El único inconveniente real de esta versión en particular es que los linters no podrán alinear el código del trabajador de servicio (ya que es solo una cadena), lo cual es una ventaja para el "enfoque de la función de trabajador por separado".


1

Esto es solo una adición a lo anterior: tengo unas buenas plantillas para probar trabajadores web en jsFiddle. En lugar de Blob, usa la ?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);
  }
})

Están disponibles las plantillas normales de trabajador web y trabajador compartido .


1

Descubrí que CodePen actualmente no resalta sintaxis <script>etiquetas en línea que no lo son type="text/javascript"(o que no tienen atributo de tipo).

Así que ideé una solución similar pero ligeramente diferente usando bloques etiquetados con break, que es la única forma en que puede rescatar de una <script>etiqueta sin crear una función de envoltura (lo cual es innecesario).

<!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 versión simple promisificada Function#callAsWorker, que toma un thisArg y argumentos (como call), y devuelve una promesa:

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


debe agregar un close()método para cerrar el enlace de vida de su trabajador web. developer.mozilla.org/en-US/docs/Web/API/WorkerGlobalScope/…
Shahar ド ー ン Levi

@Shahar ド ー ン Levi, la closefunción está en desuso. Sin embargo, los trabajadores pueden ser despedidos . He agregado eso ahora.
Trincot

0

Utilizo un código como este, puede definir su mensaje como una función que no sea texto simple, para que el editor pueda resaltar su código y jshint funciona.

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


Mira mi respuesta , acabo de hacer eso, pero escribí una clase completa para abstraer cómo pasar las devoluciones de llamada
Fernando Carvajal

0

Sí, es posible, lo hice usando archivos Blob y pasando una devolución de llamada

Te mostraré lo que hace una clase que escribí y cómo maneja la ejecución de devoluciones de llamada en segundo plano.

Primero, crea una instancia GenericWebWorkercon cualquier dato que desee pasar a la devolución de llamada que se ejecutará en el Web Worker, que incluye funciones que desea usar, en este caso un número, una fecha y una función llamadablocker

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

Esta función de bloqueo ejecutará un tiempo infinito durante n milisegundos

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

y luego lo usas así

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

Ahora, es hora de ver la magia en el siguiente ejemplo.

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

Puede colocar el contenido de su archivo worker.js dentro de los backticks (lo que permite una constante de cadena multilínea) y crear el trabajador a partir de un 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" })));

Esto es útil si por alguna razón no desea tener etiquetas de script separadas para el trabajador.


0

Otra solución es simplemente envolver al trabajador en una función, luego crear un blob que invoque la función de esta manera:

     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 para ejecutar funciones en trabajadores:

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

Ejemplo de uso:

let fn = FunctionalWorker(() => {
    self.postMessage("hi");
});
fn.onmessage = msg => {
    console.log(msg);
};
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.