Веб-работники без отдельного файла Javascript?
Насколько я могу судить, веб-работники должны быть написаны в отдельном файле JavaScript и называться так:
new Worker('longrunning.js')
Я использую компилятор закрытия для объединения и минимизации всего моего исходного кода JavaScript, и я бы предпочел не иметь своих работников в отдельных файлах для распространения. Есть ли способ сделать это?
new Worker(function() {
//Long-running work here
});
Учитывая, что первоклассные функции так важны для JavaScript, почему стандартный способ выполнения фоновой работы должен загружать целый "другой" файл JavaScript с сервера?
31 ответ
http://www.html5rocks.com/en/tutorials/workers/basics/
Что, если вы хотите создать рабочий сценарий на лету или создать автономную страницу без необходимости создания отдельных рабочих файлов? С помощью Blob() вы можете "встроить" своего работника в тот же HTML-файл, что и основная логика, создав дескриптор URL для рабочего кода в виде строки
Полный пример работника 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>
Решение html5rocks по встраиванию кода веб-работника в HTML довольно ужасно.
К тому же бланк экранированного JavaScript-as-a-string не лучше, не в последнюю очередь потому, что он усложняет рабочий процесс (компилятор Closure не может работать со строками).
Лично мне очень нравятся методы toString, но @ dan-man ЭТО регулярное выражение!
Мой предпочтительный подход:
// 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 );
Поддержка - это пересечение этих трех таблиц:
Однако это не сработает для SharedWorker, поскольку URL-адрес должен точно соответствовать, даже если необязательный параметр name совпадает. Для SharedWorker вам понадобится отдельный файл JavaScript.
Обновление 2015 года - наступает особенность ServiceWorker
Теперь есть еще более мощный способ решения этой проблемы. Опять же, сохраните рабочий код как функцию (а не статическую строку) и преобразуйте ее с помощью.toString(), затем вставьте код в CacheStorage под выбранным статическим URL-адресом.
// 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'}})
);
});
Есть два возможных отступления. ObjectURL, как описано выше, или, что более удобно, поместите настоящий файл JavaScript в /my_workers/worker1.js
Преимущества такого подхода:
- SharedWorkers также могут поддерживаться.
- Вкладки могут совместно использовать одну кэшированную копию по фиксированному адресу. Подход BLOB-объектов распространяет случайные объектные URL для каждой вкладки.
Вы можете создать один файл JavaScript, который знает его контекст выполнения и может действовать как родительский сценарий и как рабочий. Давайте начнем с базовой структуры для файла, подобного этому:
(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);
Как видите, скрипт содержит весь код как для точки зрения родителей, так и для работника, проверяя, является ли его собственный отдельный экземпляр рабочим с !document
, Несколько громоздкий script_path
вычисление используется для точного расчета пути сценария относительно родительской страницы, как путь, предоставленный для new Worker
относится к родительской странице, а не к сценарию.
С использованием Blob
Метод, как насчет этого для рабочего завода:
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'})));
}
Таким образом, вы можете использовать это так...
var myWorker = BuildWorker(function(){
//first line of worker
self.onmessage(){....};
//last line of worker
});
РЕДАКТИРОВАТЬ:
Я только что расширил эту идею, чтобы упростить межпотоковое взаимодействие: bridged-worker.js.
РЕДАКТИРОВАТЬ 2:
Приведенная выше ссылка относится к сущности, которую я создал. Кто-то позже превратил это в реальный репо.
Веб-работники работают в совершенно разных контекстах, как отдельные программы.
Это означает, что код не может быть перемещен из одного контекста в другой в форме объекта, поскольку тогда они смогут ссылаться на объекты через замыкания, принадлежащие другому контексту.
Это особенно важно, так как ECMAScript разработан как однопоточный язык, и поскольку веб-работники работают в отдельных потоках, вы рискуете выполнить не поточнобезопасные операции.
Это снова означает, что веб-работники должны быть инициализированы с кодом в исходной форме.
Спецификация от WHATWG говорит
Если источник результирующего абсолютного URL-адреса не совпадает с источником сценария ввода, то генерируется исключение SECURITY_ERR.
Таким образом, сценарии должны быть внешними файлами по той же схеме, что и исходная страница: вы не можете загрузить сценарий из data: URL или javascript: URL, а страница https: не может запустить рабочих, использующих сценарии с http: URL.
но, к сожалению, на самом деле это не объясняет, почему нельзя было разрешить передачу строки с исходным кодом в конструктор.
Недавний ответ (2018)
Вы можете использовать Гринлет:
Переместите асинхронную функцию в ее собственный поток. Упрощенная однофункциональная версия Workerize.
Пример:
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'))
Лучше читать способ для встроенного работника..
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");
Простая обещанная версия, Function#callAsWorker
, который принимает thisArg и аргументы (так же, как call
) и возвращает обещание:
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.onerror = e => reject(e.message);
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);
});
Возьмите ответ Адрии и поместите его в копируемую функцию, которая работает с текущими Chrome и FF, но не с IE10 (работник из blob вызывает ошибку безопасности).
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;
}
И вот рабочий пример http://jsfiddle.net/ubershmekel/YYzvr/
В зависимости от вашего варианта использования вы можете использовать что-то вроде
task.js Упрощенный интерфейс для запуска кода, интенсивно использующего процессор, для всех ядер (node.js и web)
Примером будет
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
});
Так что я думаю, что у нас есть еще один классный вариант для этого, благодаря шаблонным литералам в ES6. Это позволяет нам обойтись без дополнительной рабочей функции (и ее странной области видимости) и просто написать код, предназначенный для рабочего, в виде многострочного текста, во многом как в случае, когда мы использовали для хранения текста, но без необходимости в документе или DOM сделать это в. Пример:
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);
`;
Вот суть этого подхода.
Обратите внимание, что мы можем добавить любые дополнительные зависимости функций, которые мы хотим, в работника, просто собрав их в массив и запустив.toString для каждого из них, чтобы также сократить их до строк (должно работать, пока они являются объявлениями функций) и затем просто добавив это к строке сценария. Таким образом, нам не нужно импортировать скрипты, которые мы, возможно, уже включили в область написанного нами кода.
Единственный реальный недостаток этой конкретной версии заключается в том, что линтеры не смогут привязывать код работника службы (поскольку это всего лишь строка), что является преимуществом для "подхода с использованием отдельных рабочих функций".
Взгляните на плагин vkThread. С помощью плагина htis вы можете взять любую функцию в своем основном коде и выполнить ее в потоке (веб-работник). Таким образом, вам не нужно создавать специальный "файл веб-работника".
http://www.eslinstructor.net/vkthread/
--Vadim
@Trincot's кажется лучшим на данный момент. Тем не менее, возможно, мы можем развить его немного дальше. Итак, идея в том,
- Давайте не будем изменять
Function.prototype
. - Получите обещанную/потоковую версию функции для многопотоковой операции.
- Убедитесь, что функция все еще может быть вызвана синхронно, если это необходимо.
Таким образом, мы определяемThreadable
класс сspawn
метод. Как только мы сделаем нашу функцию членом этого класса, она станет потоковой :)
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
});
};
})();
Попробуйте использовать jThread. https://github.com/cheprasov/jThread
// You can use simple calling like this
jThread(
function(arr){
//... some code for Worker
return arr;
}
,function(arr){
//... done code
}
)( [1,2,3,4,5,6,7] ); // some params
Мне понравился ответ, который дал ifbamoq, но не смог прокомментировать из-за глупой политики переполнения стека. Поэтому я приведу пример, который показывает, как выполняется интенсивная работа, и как она не блокирует основной поток.
Все это без проблем с CORS с нулевым происхождением - если вы похожи на меня и любите дважды щелкать html-файлы и обрабатывать их как маленькие программы. :-)
Используйте мой крошечный плагин https://github.com/zevero/worker-create
var worker_url = Worker.createURL(function(e){
self.postMessage('Example post from Worker'); //your code here
});
var worker = new Worker(worker_url);
Для реализации Node.js можно использовать следующую адаптацию ответа Тринкота .Обратите внимание еще раз, чтоFunction.prototype.callAsWorker()
беретthisArg
и аргументы, какFunction.prototype.call()
и возвращает обещание.
const { Worker } = require ( 'worker_threads' );
Function.prototype.callAsWorker = function ( ...args ) {
return new Promise( ( resolve, reject ) => {
const code = `
const { parentPort, workerData } = require ( 'worker_threads' );
parentPort.postMessage( ( ${this.toString()} ).call( ...workerData ) )
`;
const worker = new Worker( code, { eval: true, workerData: args } );
worker.on('message', ( msg ) => { resolve( msg ), worker.terminate() } );
worker.on('error', ( err ) => { reject( err ), worker.terminate() } );
worker.on('exit', ( code ) => {
if ( code !== 0 ) {
reject( new Error( `Worker stopped with exit code ${code}.` ) );
}
});
});
}
// Demo
function add( ...nums ) {
return nums.reduce( ( a, b ) => a + b );
}
// Let the worker execute the above function, with the specified arguments
let result = await add.callAsWorker( null, 1, 2, 3 );
console.log( 'result: ', result );
Я обнаружил, что CodePen в настоящее время не выделяет синтаксис встроенным <script>
теги, которые не являются type="text/javascript"
(или которые не имеют атрибута типа).
Поэтому я разработал похожее, но немного другое решение, используя помеченные блоки с break
, который является единственным способом освобождения под залог <script>
тег без создания функции-оболочки (что не нужно).
<!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>
Это всего лишь дополнение к вышеупомянутому - у меня есть хорошие шаблоны для тестирования веб-работников в jsFiddle. Вместо Blob он использует jsFiddles ?js
апи:
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);
}
})
Доступны обычные веб-рабочие и общие рабочие шаблоны.
Однострочный для запуска функций в воркерах:
const FunctionalWorker = fn => new Worker(window.URL.createObjectURL(new Blob(["(" + fn.toString() + ")()"], {type: "text/javascript"})));
Пример использования:
let fn = FunctionalWorker(() => {
self.postMessage("hi");
});
fn.onmessage = msg => {
console.log(msg);
};
Здесь консоль:
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);
});
Я думаю, что лучший способ сделать это - использовать объект Blob, ниже вы можете увидеть простой пример.
// 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");
мой взгляд на это:
function BuildWorker(fn){
var str = fn.toString().match(/^[^{]+{([\s\S]+)}\s*$/m)[1];
return new Worker(window.URL.createObjectURL(
new Blob([str],{type:'text/javascript'})));
}
function createAsyncWorker(fn){
// asyncworker=createAsyncWorker(function(){
// importScripts('my_otherscript.js');
// self.onmessage = function([arg1,arg2]) {
// self.postMessage('msg from worker');
// };
// })
// await asyncworker.postMessage('arg1','value')
// await asyncworker.postMessage('arg1','value')
// asyncworker.worker.terminate()
var worker = BuildWorker(fn);
function postMessage(...message){
let external={}, promise= new Promise((resolve,reject)=>{external.resolve=resolve;external.reject=reject;})
worker.onmessage = function(message){ external.resolve(message.data)};
worker.postMessage(message); // Start the worker.
return promise;
}
return {worker,postMessage};
}
пример использования:
autoarima = createAsyncWorker(function(){
importScripts("https://127.0.0.1:11000/arima.js")
self.onmessage=(message)=>{
let [action,arg1,arg2]=message.data
if(action=='load')
{
ARIMAPromise.then(ARIMA1 => {
ARIMA=ARIMA1
autoarima = new ARIMA({ auto: true });
// const ts = Array(10).fill(0).map((_, i) => i + Math.random() / 5)
// const arima = new ARIMA({ p: 2, d: 1, q: 2, P: 0, D: 0, Q: 0, S: 0, verbose: false }).train(ts)
// const [pred, errors] = arima.predict(10)
postMessage('ok')
});
}
if(action=='fit')
{
autoarima.fit(arg1)
postMessage('ok')
}
if(action=='predict')
{
postMessage(autoarima.predict(arg1,arg2))
}
};
})
autoarima.terminate=function(){ this.worker.terminate(); }
autoarima.load=async function(...args){return await this.postMessage('load',...args)}
autoarima.fit=async function(...args){return await this.postMessage('fit',...args)}
autoarima.predict=async function(...args){return await this.postMessage('predict',...args)}
await autoarima.load()
await autoarima.fit(b_values)
await autoarima.predict(1)
Вы можете использовать веб-работников в одном и том же JavaScript, используя встроенных веб-работников.
Следующая статья поможет вам лучше понять веб-работников, их ограничения и отладку.
Вы можете поместить содержимое вашего файла worker.js внутри обратных галочек (что позволяет использовать многострочную строковую константу) и создать работника из большого двоичного объекта, например:
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" })));
Это удобно, если по какой-либо причине вы не хотите иметь отдельные теги сценария для работника.
Я использую такой код, вы можете определить свое сообщение как функцию, отличную от обычного текста, чтобы редактор мог выделить ваш код, и 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;
}
было несколько ответов, но вот еще одна встроенная версия.
примечание: аргумент "self" является чисто косметическим для целей линтинга, фактический рабочий код начинается после первой скобки, self как обычно
Да, это возможно, я сделал это, используя файлы Blob и передавая обратный вызов
Я покажу вам, что делает класс, который я написал, и как он управляет выполнением обратных вызовов в фоновом режиме.
Сначала вы создаете экземпляр GenericWebWorker
с любыми данными, которые вы хотели бы передать обратному вызову, который будет выполняться в Web Worker
, который включает в себя функции, которые вы хотите использовать, в этом случае число, дату и функцию с именем blocker
var worker = new GenericWebWorker(100, new Date(), blocker)
Эта блокирующая функция будет выполнять бесконечное время в течение n миллисекунд
function blocker (ms) {
var now = new Date().getTime();
while(true) {
if (new Date().getTime() > now +ms)
return;
}
}
а затем вы используете это так
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
Теперь пришло время увидеть магию в примере ниже
/*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
Другое решение - просто обернуть Worker в функцию, а затем создать BLOB-объект, вызывающий эту функцию следующим образом:
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.