Основы веб-работников

  1. Проблема: параллелизм JavaScript
  2. Знакомство с веб-работниками: привнесение потоков в JavaScript
  3. Типы веб-работников
  4. Начиная
  5. Общение с работником через передачу сообщений
  6. Переносимые объекты
  7. Рабочая среда
  8. Возможности, доступные для работников
  9. Загрузка внешних скриптов
  10. Subworkers
  11. Встроенные работники
  12. URL-адреса BLOB-объектов
  13. Полный пример
  14. Загрузка внешних скриптов
  15. Обработка ошибок
  16. Слово о безопасности
  17. Те же соображения происхождения
  18. Случаи применения

Проблема: параллелизм JavaScript

Существует ряд узких мест, препятствующих переносу интересных приложений (скажем, из серверных реализаций) в клиентский JavaScript. Некоторые из них включают совместимость браузера, статическую типизацию, доступность и производительность. К счастью, последнее быстро уходит в прошлое, поскольку производители браузеров быстро повышают скорость своих движков JavaScript.

Одна вещь, которая осталась помехой для JavaScript, это на самом деле сам язык. JavaScript - это однопоточная среда, что означает, что несколько сценариев не могут выполняться одновременно. В качестве примера представим сайт, который должен обрабатывать события пользовательского интерфейса, запрашивать и обрабатывать большие объемы данных API и манипулировать DOM. Довольно часто, верно? К сожалению, все это не может быть одновременным из-за ограничений времени выполнения JavaScript в браузерах. Выполнение скрипта происходит в одном потоке.

Разработчики имитируют «параллелизм», используя такие методы, как setTimeout (), setInterval (), XMLHttpRequest и обработчики событий. Да, все эти функции работают асинхронно, но неблокирование не обязательно означает параллелизм. Асинхронные события обрабатываются после того, как текущий исполняемый скрипт уступил. Хорошей новостью является то, что HTML5 дает нам что-то лучшее, чем эти хаки!

Знакомство с веб-работниками: привнесение потоков в JavaScript

Веб-работники Спецификация определяет API для порождения фоновых скриптов в вашем веб-приложении. Веб-работники позволяют вам выполнять такие вещи, как запуск длительных сценариев для обработки вычислительных задач, но без блокировки пользовательского интерфейса или других сценариев для обработки взаимодействия с пользователем. Они помогут положить конец этому отвратительному диалогу «сценарий без ответа», который мы все полюбили:

Они помогут положить конец этому отвратительному диалогу «сценарий без ответа», который мы все полюбили:

Обычный не отвечающий скрипт скрипта.

Рабочие используют потоковую передачу сообщений для достижения параллелизма. Они идеально подходят для обновления вашего пользовательского интерфейса, производительности и отзывчивости для пользователей.

Типы веб-работников

Стоит отметить, что Спецификация обсуждает два вида веб-работников, Выделенные работники а также Общие работники , Эта статья будет охватывать только преданных работников, и я буду называть их «веб-работниками» или «работниками».

Начиная

Web Workers работают в изолированном потоке. В результате код, который они выполняют, должен содержаться в отдельном файле. Но прежде чем мы это сделаем, первое, что нужно сделать, это создать новый объект Worker на главной странице. Конструктор берет имя рабочего скрипта:

var worker = new Worker ('task.js');

Если указанный файл существует, браузер создаст новый рабочий поток, который загружается асинхронно. Рабочий не начнется, пока файл не будет полностью загружен и выполнен. Если путь к вашему работнику возвращает 404, работник молча провалится.

После создания работника запустите его, вызвав метод postMessage ():

worker.postMessage (); // Запуск работника.

Общение с работником через передачу сообщений

Связь между работой и ее родительской страницей осуществляется с использованием модели событий и метода postMessage (). В зависимости от вашего браузера / версии, postMessage () может принимать либо строку, либо объект JSON в качестве единственного аргумента. Последние версии современных браузеров поддерживают передачу объекта JSON.

Ниже приведен пример использования строки для передачи «Hello World» работнику в doWork.js. Рабочий просто возвращает сообщение, которое ему передано.

Основной скрипт:

var worker = new Worker ('doWork.js'); worker.addEventListener ('message', function (e) {console.log ('Worker сказал:', e.data);}, false); worker.postMessage ('Hello World'); // Отправить данные нашему работнику.

doWork.js (рабочий):

self.addEventListener ('message', function (e) {self.postMessage (e.data);}, false);

Когда postMessage () вызывается с главной страницы, наш работник обрабатывает это сообщение, определяя обработчик onmessage для события сообщения. Полезная нагрузка сообщения (в данном случае «Hello World») доступна в Event.data. Хотя этот конкретный пример не очень интересен, он демонстрирует, что postMessage () также является вашим средством для передачи данных обратно в основной поток. Удобный!

Сообщения, передаваемые между главной страницей и работниками, копируются, а не передаются. Например, в следующем примере свойство msg сообщения JSON доступно в обоих местах. Похоже, что объект передается непосредственно работнику, даже если он работает в отдельном выделенном пространстве. На самом деле происходит то, что объект сериализуется по мере передачи работнику, а затем десериализуется на другом конце. Страница и работник не совместно используют один и тот же экземпляр, поэтому конечным результатом является создание дубликата на каждом проходе. Большинство браузеров реализуют эту функцию, автоматически кодируя / декодируя значение JSON на любом конце.

Ниже приведен более сложный пример передачи сообщений с использованием объектов JSON.

Основной скрипт:

<button onclick = "sayHI ()"> Say HI </ button> <button onclick = "unknownCmd ()"> Отправить неизвестную команду </ button> <button onclick = "stop ()"> Остановить работника </ button> < output id = "result"> </ output> <script> function sayHI () {worker.postMessage ({'cmd': 'start', 'msg': 'Hi'}); } function stop () {// worker.terminate () из этого скрипта также остановит работника. worker.postMessage ({'cmd': 'stop', 'msg': 'Bye'}); } function unknownCmd () {worker.postMessage ({'cmd': 'foobard', 'msg': '???'}); } var worker = new Worker ('doWork2.js'); worker.addEventListener ('message', function (e) {document.getElementById ('result'). textContent = e.data;}, false); </ Скрипт>

doWork2.js:

self.addEventListener ('message', function (e) {var data = e.data; switch (data.cmd) {case 'start': self.postMessage ('WORKER STARTED:' + data.msg); перерыв; case 'stop': self.postMessage ('WORKER STOPPED:' + data.msg + '. (кнопки больше не работают)'); self.close (); // Завершает рабочий. break; default: self.postMessage ( 'Неизвестная команда:' + data.msg);};}, false);

Примечание . Существует два способа остановить работника: с помощью метода worker.terminate () с главной страницы или вызова self.close () внутри самого работника.

Пример : запустить этого работника!

Скажи HI Отправить неизвестную команду Stop рабочий

Переносимые объекты

Большинство браузеров реализуют структурированное клонирование Алгоритм, который позволяет передавать более сложные типы в / из Workers, такие как объекты File, Blob, ArrayBuffer и JSON. Однако при передаче этих типов данных с помощью postMessage () копия все равно создается. Следовательно, если вы передаете большой файл размером 50 МБ (например), то при передаче этого файла между рабочим и основным потоком возникают заметные накладные расходы.

Структурное клонирование - это хорошо, но его копирование может занять сотни миллисекунд. Для борьбы с перфорированным ударом вы можете использовать Переносимые объекты ,

В переносимых объектах данные переносятся из одного контекста в другой. Это нулевое копирование, что значительно повышает производительность отправки данных в Worker. Думайте об этом как о передаче по ссылке, если вы из мира C / C ++. Однако, в отличие от передачи по ссылке, «версия» из вызывающего контекста больше не доступна после переноса в новый контекст. Например, при переносе ArrayBuffer из основного приложения в Worker исходный ArrayBuffer очищается и больше не используется. Его содержимое (буквально тихо) переносится в контекст Worker.

Чтобы использовать переносимые объекты, используйте немного другую подпись postMessage ():

worker.postMessage (arrayBuffer, [arrayBuffer]); window.postMessage (arrayBuffer, targetOrigin, [arrayBuffer]);

Рабочий случай, первый аргумент - это данные, а второй - список элементов, которые должны быть переданы. Кстати, первый аргумент не обязательно должен быть ArrayBuffer. Например, это может быть объект JSON:

worker.postMessage ({data: int8View, moreData: anotherBuffer}, [int8View.buffer, anotherBuffer]);

Важным моментом является то, что второй аргумент должен быть массивом ArrayBuffers. Это ваш список передаваемых предметов.

Чтобы увидеть улучшение скорости передачи, проверьте это DEMO , Для получения дополнительной информации о переводе см. Наш HTML5Rock сообщение ,

Рабочая среда

Рабочая сфера

В контексте работника, и я, и это ссылаются на глобальную область видимости работника. Таким образом, предыдущий пример также может быть записан как:

addEventListener ('message', function (e) {var data = e.data; switch (data.cmd) {case 'start': postMessage ('WORKER STARTED:' + data.msg); break; case 'stop': ... }, ложный);

В качестве альтернативы вы можете установить обработчик события onmessage напрямую (хотя addEventListener всегда поощряется ниндзя JavaScript).

onmessage = function (e) {var data = e.data; ...};

Возможности, доступные для работников

Благодаря многопоточному поведению веб-работники имеют доступ только к подмножеству функций JavaScript:

  • Объект навигатор
  • Местоположение объекта (только для чтения)
  • XMLHttpRequest
  • setTimeout () / clearTimeout () и setInterval () / clearInterval ()
  • Кэш приложения
  • Импорт внешних скриптов с использованием метода importScripts ()
  • Порождение других веб-работников

Работники не имеют доступа к:

  • DOM (это не потокобезопасно)
  • Объект окна
  • Объект документа
  • Родительский объект

Загрузка внешних скриптов

Вы можете загрузить внешние файлы сценариев или библиотеки в работника с помощью функции importScripts (). Метод принимает ноль или более строк, представляющих имена файлов для импортируемых ресурсов.

Этот пример загружает script1.js и script2.js в работника:

worker.js:

importScripts ( 'script1.js'); importScripts ( 'script2.js');

Который также может быть записан как один оператор импорта:

importScripts ('script1.js', 'script2.js');

Subworkers

Работники имеют возможность порождать детей. Это отлично подходит для дальнейшего разбивания больших задач во время выполнения. Тем не менее, субработники идут с несколькими оговорками:

  • Субработники должны быть размещены в том же источнике, что и родительская страница.
  • URI внутри подчиненных разрешаются относительно местоположения их родительского работника (в отличие от главной страницы).

Имейте в виду, что большинство браузеров порождают отдельные процессы для каждого работника. Прежде чем приступить к созданию рабочей фермы, будьте осторожны, если не используете слишком много системных ресурсов пользователя. Одной из причин этого является то, что сообщения, передаваемые между основными страницами и работниками, копируются, а не передаются. Увидеть Общение с работником через передачу сообщений ,

Для примера того, как порождать субработника, см. пример в спецификации.

Встроенные работники

Что, если вы хотите создать свой рабочий сценарий на лету или создать автономную страницу без необходимости создания отдельных рабочих файлов? С помощью Blob () вы можете «встроить» своего работника в тот же HTML-файл, что и основная логика, создав дескриптор URL для рабочего кода в виде строки:

var blob = new Blob (["onmessage = function (e) {postMessage ('msg from worker');}"]); // Получить ссылку на BLOB-ссылку на наш рабочий файл. var blobURL = window.URL.createObjectURL (blob); var worker = новый работник (blobURL); worker.onmessage = function (e) {// e.data == 'msg from worker'}; worker.postMessage (); // Запуск работника.

URL-адреса BLOB-объектов

Магия приходит с призывом window.URL.createObjectURL () , Этот метод создает простую строку URL, которая может использоваться для ссылки на данные, хранящиеся в объекте DOM File или Blob. Например:

блоб: HTTP: // локальный / c745ef73-ece9-46da-8f66-ebes574789b1

URL-адреса BLOB-объектов являются уникальными и действуют в течение всего срока службы вашего приложения (например, до тех пор, пока документ не будет выгружен). Если вы создаете много URL-адресов BLOB-объектов, рекомендуется опубликовать ссылки, которые больше не нужны. Вы можете явно освободить URL-адреса Blob, передав его window.URL.revokeObjectURL () :

window.URL.revokeObjectURL (blobURL);

В Chrome есть хорошая страница для просмотра всех созданных URL-адресов BLOB-объектов: chrome: // blob-internals /.

Полный пример

Сделав еще один шаг вперед, мы сможем понять, как код JS работника встроен на нашей странице. Этот метод использует тег <script> для определения работника:

<! DOCTYPE html> <html> <head> <meta charset = "utf-8" /> </ head> <body> <div id = "log"> </ div> <script id = "worker1" type = "javascript / worker"> // Этот скрипт не будет обрабатываться механизмами JS //, потому что его тип - javascript / worker. self.onmessage = function (e) {self.postMessage ('msg from worker'); }; // Остальная часть вашего рабочего кода идет сюда. </ script> <script> function log (msg) {// Использовать фрагмент: браузер будет отображать / перекомпоновывать только один раз. var фрагмент = документ.createDocumentFragment (); fragment.appendChild (document.createTextNode (MSG)); fragment.appendChild (document.createElement ( 'ш')); document.querySelector ( "# журнал") AppendChild (фрагмент). } var blob = new Blob ([document.querySelector ('# worker1'). textContent]); var worker = new Worker (window.URL.createObjectURL (blob)); worker.onmessage = function (e) {log ("Получено:" + e.data); } worker.postMessage (); // Запуск работника. </ script> </ body> </ html>

На мой взгляд, этот новый подход немного чище и понятнее. Он определяет тег сценария с id = "worker1" и type = 'javascript / worker' (поэтому браузер не анализирует JS). Этот код извлекается в виде строки с использованием document.querySelector ('# worker1'). TextContent и передается в Blob () для создания файла.

Загрузка внешних скриптов

При использовании этих методов для встраивания вашего рабочего кода importScripts () будет работать, только если вы предоставите абсолютный URI. Если вы попытаетесь передать относительный URI, браузер выдаст сообщение об ошибке безопасности. Причина в том, что рабочий (теперь созданный из URL-адреса BLOB-объекта) будет разрешен с префиксом blob :, а ваше приложение будет работать по другой схеме (предположительно http: //). Следовательно, сбой будет из-за ограничений перекрестного происхождения.

Один из способов использования importScripts () во встроенном работнике - это «вставить» текущий URL вашего основного скрипта, из которого он выполняется, передав его встроенному работнику и создав абсолютный URL-адрес вручную. Это обеспечит импорт внешнего скрипта из того же источника. Предполагается, что ваше основное приложение запущено с http://example.com/index.html:

... <script id = "worker2" type = "javascript / worker"> self.onmessage = function (e) {var data = e.data; if (data.url) {var url = data.url.href; var index = url.indexOf ('index.html'); if (index! = -1) {url = url.substring (0, index); } importScripts (url + 'engine.js'); } ...}; </ script> <script> var worker = new Worker (window.URL.createObjectURL (bb.getBlob ())); worker.postMessage ( {url: document.location} ); </ Скрипт>

Обработка ошибок

Как и в любой логике JavaScript, вы захотите обрабатывать любые ошибки, возникающие у ваших веб-работников. Если во время выполнения работника возникает ошибка, запускается ErrorEvent. Интерфейс содержит три полезных свойства для определения того, что пошло не так: filename - имя рабочего скрипта, вызвавшего ошибку, lineno - номер строки, в которой произошла ошибка, и message - содержательное описание ошибки. Вот пример настройки обработчика события onerror для печати свойств ошибки:

<output id = "error" style = "color: red;"> </ output> <output id = "result"> </ output> <script> функция onError (e) {document.getElementById ('error'). textContent = ['ERROR: Line', e.lineno, 'in', e.filename, ':', e.message] .join (''); } function onMsg (e) {document.getElementById ('result'). textContent = e.data; } var worker = new Worker ('workerWithError.js'); worker.addEventListener ('message', onMsg, false); worker.addEventListener ('error', onError, false); worker.postMessage (); // Запуск работника без сообщения. </ Скрипт>

Пример : workerWithError.js пытается выполнить 1 / x, где x не определено.

Запустить его

workerWithError.js:

self.addEventListener ('message', function (e) {postMessage (1 / x); // Преднамеренная ошибка.};

Слово о безопасности

Ограничения с локальным доступом

Из-за ограничений безопасности Google Chrome рабочие не будут работать локально (например, из file: //) в последних версиях браузера. Вместо этого они молча терпят неудачу! Чтобы запустить приложение из схемы file: //, запустите Chrome с установленным флагом --allow-file-access-from-files. ПРИМЕЧАНИЕ . Не рекомендуется запускать основной браузер с этим установленным флагом. Он должен использоваться только для целей тестирования, а не для регулярного просмотра.

Другие браузеры не накладывают такое же ограничение.

Те же соображения происхождения

Рабочие сценарии должны быть внешними файлами по той же схеме, что и их вызывающая страница. Таким образом, вы не можете загрузить скрипт из data: URL или javascript: URL, а страница https: не может запустить рабочие скрипты, которые начинаются с http: URL.

Случаи применения

Так какое приложение будет использовать веб-работников? К сожалению, веб-работники все еще относительно новы, и большинство примеров / учебных пособий касаются вычисления простых чисел. Хотя это не очень интересно, это полезно для понимания концепций веб-работников. Вот еще несколько идей, чтобы заставить ваш мозг взбалтывать:

  • Предварительная выборка и / или кэширование данных для последующего использования
  • Подсветка синтаксиса кода или другое форматирование текста в реальном времени
  • Программа проверки орфографии
  • Анализировать видео или аудио данные
  • Фоновый ввод / вывод или опрос веб-сервисов
  • Обработка больших массивов или огромных JSON-ответов
  • Фильтрация изображений в <canvas>
  • Обновление многих строк локальной веб-базы данных

демос

Рекомендации

Довольно часто, верно?
Cmd': 'foobard', 'msg': '?