Веб-працівники без окремого файлу Javascript?


291

Наскільки я можу сказати, веб-працівників потрібно записати в окремий файл JavaScript і викликати так:

new Worker('longrunning.js')

Я використовую компілятор закриття, щоб комбінувати та мінімізувати весь мій вихідний код JavaScript, і я вважаю за краще не мати своїх працівників в окремих файлах для розповсюдження. Чи є спосіб це зробити?

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

Зважаючи на те, що функції першого класу настільки важливі для JavaScript, чому стандартний спосіб фонової роботи повинен завантажувати цілий інший файл JavaScript з веб-сервера?


7
Причиною цього є те, що збереження чистого контексту виконання є навіть більш важливим, ніж функції першого класу :-)
Pointy

1
Я працюю над цим (а точніше над мінімізацією проблеми): DynWorker . Можна зробити: var worker = new DynWorker(); worker.inject("foo", function(){...});...
Фелікс Сапареллі


1
ОП видалила питання "Навчаючий працівник приймати функцію замість вихідного файлу JavaScript". Відповідь перенесена тут
Rob W

Я розробив task.js, щоб зробити це набагато простіше. Більшу частину часу ви намагаєтеся лише завантажити невеликі завдання блокування.
Чад Счіра

Відповіді:


225

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

Що робити, якщо ви хочете створити свій робочий скрипт на льоту або створити автономну сторінку, не створюючи окремих робочих файлів? За допомогою 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>


Рішення лише для Google Chrome, здається, Firefox 10 підтримуватиме його, я не знаю про інші браузери
4esn0k

2
BlobBuiler тепер застарілий . Використовуйте замість Blob . На даний момент підтримується в останніх Firefox / WebKit / Opera та IE10, див. Таблиці сумісності для старих браузерів.
Фелікс Сапареллі

3
Конструктор Blob може підтримуватися в IE10, але ви все ще не можете передавати JavaScript веб-працівнику через нього (навіть не в IE11): connect.microsoft.com/IE/feedback/details/801810/… .
jayarjo

1
@albanx -що тести? В Інтернеті вже є мільярд демо-сторінок, на яких видно, що нитка не затримує браузер роками.
vsync

2
@albanx - чи хотіли б ви хоча б сказати, який езотеричний браузер ви використовуєте, який висить? це демо висить для вас? ie.microsoft.com/testdrive/Graphics/WorkerFountains/…
vsync

162

Рішення html5rocks вбудовування коду веб-робочого в HTML є досить жахливим.
І крапка втеченого JavaScript-as-a-string не краща, не в останню чергу, тому що це ускладнює робочий потік (компілятор закриття не може працювати на рядках).

Особисто мені дуже подобаються методи 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'}})
 );
});

Можливі дві можливі падіння. Об'єктURL, як зазначено вище, або більш плавно, розмістіть справжній файл JavaScript за адресою /my_workers/worker1.js

Перевагами такого підходу є:

  1. SharedWorkers також можна підтримувати.
  2. Вкладки можуть ділитися однією кешованою копією за фіксованою адресою. Підхід блобу поширює випадкові об'єктиURL для кожної вкладки.

4
Як виглядатиме сумісність браузера у цьому рішенні?
Ben Dilts

Чи можете ви детальніше розглянути це рішення, як воно працює? Що таке work1.js? Це окремий js-файл? Я намагаюся використовувати це, але не можу змусити його працювати. Зокрема, я намагаюся зробити так, щоб це працювало для SharedWorker
Yehuda

Якби тільки ти міг перетворити його на корисну функцію!
ммм

@ Ben Dilts: Сумісність веб-переглядача виглядатиме як просто запуск вашого коду через babel: babeljs.io/repl
Jack Giffin

Стандарт не гарантує, що Function.prototype.toString () повертає тіло функції у вигляді рядка. Напевно, слід додати попередження до відповіді.
РД

37

Ви можете створити один файл 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є відносно батьківської сторінки, а не сценарію.


4
Здається, ваш сайт зник; у вас є нова URL-адреса?
BrianFreud

1
Це цікавий підхід. FWIW, я виявляю функцій веб-працівників, перевіряючи наявність "себе" (глобального об'єкта Web Worker) проти "вікна".
pwnall

Я розглядав, як PapaParse поводиться з веб-працівниками, і, здається, вони застосовують такий підхід github.com/mholt/PapaParse
JP DeVries

Я думаю, що тестування за допомогою 'typeof importScripts! == null' може визначити, чи сценарій працює у робочих сферах.
MeTTeO

1
Я не розумію, що попереднійSibling - це з елемента скрипта. Може хтось мені пояснить?
Темох

28

Використовуючи 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 .

EDIT 2:

Наведене вище посилання - на створену мною суть. Хтось пізніше перетворив це на фактичне репо .


11

Працівники Інтернету працюють у абсолютно окремих контекстах як окремі програми.

Це означає, що код не може бути переміщений з одного контексту в інший у вигляді об'єкта, оскільки вони зможуть посилатись на об'єкти через закриття, що належать іншому контексту.
Це особливо важливо, оскільки ECMAScript розроблений як одна різьбова мова, а оскільки веб-працівники працюють в окремих потоках, то ви ризикуєте виконувати операції, не безпечні для потоків.

Це ще раз означає, що веб-працівників потрібно ініціалізувати кодом у вихідній формі.

Специфікація WHATWG говорить

Якщо походження отриманої абсолютної URL-адреси не збігається з початковим сценарієм введення, викиньте виняток SECURITY_ERR.

Таким чином, сценарії повинні бути зовнішніми файлами за тією ж схемою, що і вихідна сторінка: ви не можете завантажити скрипт із даних: URL або javascript: URL, а https: сторінка не може запустити працівників, використовуючи скрипти з http: URL.

але, на жаль, це не зовсім пояснює, чому не можна було дозволити передавати рядок із вихідним кодом конструктору.


6

краще читати спосіб для рядового працівника ..

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

Що я зробив, це те, що я створив функцію з усім робочим кодом, передав цю функцію toString(), витягнути тіло і потім поставити це в Blob. Перевірте останню відповідь, у мене є приклад
Фернандо Карвахаль,

5

Отримайте відповідь Adria і ввімкнувши її в функцію копіювання, яка працює з поточними 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/


5

Остання відповідь (2018)

Ви можете використовувати Greenlet :

Перемістіть функцію асинхронізації у власну нитку. Спрощена однофункціональна версія 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'))

3

Залежно від випадку використання ви можете використовувати щось подібне

Спрощений інтерфейс task.js для отримання інтенсивного коду CPU для роботи на всіх ядрах (node.js та веб)

Прикладом може бути

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

Погляньте на плагін vkThread. За допомогою htis-плагіна ви можете взяти будь-яку функцію в своєму основному коді та виконати її в потоці (веб-працівник). Отже, вам не потрібно створювати спеціальний файл "веб-працівник".

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

--Вадиме


1

Ви можете використовувати веб-працівників у тому ж файлі javascript, використовуючи вбудовані веб-працівники.

Стаття нижче стосуватиметься вас, щоб легко зрозуміти веб-працівників та їх обмеження та налагодження веб-працівників.

Оволодіння веб-роботами


1

Я думаю, що кращий спосіб зробити це за допомогою об’єкта 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"); 


1

тут консоль:

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

Тому я думаю, що у нас є ще один класний варіант для цього, завдяки літературним шаблонам в 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 на кожному з них, щоб також зменшити їх у рядки (повинні працювати до тих пір, поки вони є оголошеннями функції) та то просто попередньо додавши це до рядка сценарію. Таким чином, нам не потрібно імпортувати скрипти, які ми, можливо, вже вкладені в область коду, який ми пишемо.

Єдиним реальним недоліком цієї конкретної версії є те, що лінери не зможуть підкреслити код службового працівника (оскільки це лише рядок), що є перевагою для "окремого підходу до функціонування працівників".


1

Це лише доповнення до вище - у мене є приємні шаблони для тестування веб-працівників у jsFiddle. Замість Blob він використовує 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);
  }
})

Доступні звичайні шаблони веб-службовців та спільних працівників .


1

Я виявив, що 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>


1

Проста обіцяна версія, Function#callAsWorkerяка бере цей аргумент і аргументи (точно так само 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.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);
});


вам слід додати close()метод закриття життєвого гачка веб-працівників. developer.mozilla.org/en-US/docs/Web/API/WorkerGlobalScope/…
Shahar ド ー ン Levi

@Shahar ド ー ン Леві, closeфункція застаріла. Однак працівників можна звільнити . Я це зараз додав.
trincot

0

Я використовую такий код, як ви можете визначити свою програму onmessage як функцію, відмінну від простого тексту, тому редактор може виділити ваш код, а 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;
}


Подивіться на мою відповідь , я щойно це зробив, але я написав цілий клас для абстрагування, як передавати зворотні дзвінки
Фернандо Карвахаль

0

Так, можливо, я це робив за допомогою файлів 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


0

Ви можете розмістити вміст файлу Working.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" })));

Це зручно, якщо з будь-якої причини ви не хочете мати окремі теги сценарію для працівника.


0

Іншим рішенням є просто обернути Worker у функції, а потім створити крапку, яка викликає функцію так:

     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

Один вкладиш для виконання функцій у працівників:

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

Приклад використання:

let fn = FunctionalWorker(() => {
    self.postMessage("hi");
});
fn.onmessage = msg => {
    console.log(msg);
};
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.