怎麼在 ES6+Webpack 下使用 Web Worker

你們都知道 HTML 5 新增了不少 API,其中就包括 Web Worker,在普通的 js 文件上使用 ES5 編寫相關代碼應該是徹底沒有問題了,只須要在支持 H5 的瀏覽器上就能跑起來。css

那若是咱們須要在 ES6+Webpack 的組合環境下使用 Web Worker呢?其實也很方便,只須要注意一下個別點,接下來記錄一下我踩過的坑。webpack

至於 Web Worker 的基礎知識和基本 api 我就放到最後面當給還不瞭解或者沒有系統使用過的讀者們去簡單閱讀一下。git

1. 快速建立工程環境

假設你已經有一份 ES6+Webpack 的代碼工程環境,並且是能夠順利跑起來的;若是沒有,能夠 clone 個人 github 倉庫:github.com/irm-github/…github

2. 安裝及使用 worker-loader

2.1 安裝依賴:

$ npm install -D worker-loader
# 或
$ yarn add worker-loader --dev
複製代碼

2.2 代碼中直接使用 worker-loader

// main.js
var MyWorker = require("worker-loader!./file.js");
// var MyWorker = require("worker-loader?inline=true&fallback=false!./file.js");

var worker = new MyWorker();
worker.postMessage({a: 1});
worker.onmessage = function(event) { /* 操做 */ };
worker.addEventListener("message", function(event) { /* 操做 */ });
複製代碼

優勢:寫 worker 邏輯的腳本文件能夠任意命名,只要傳進 worker-loader 中處理便可; 缺點:每引入一次 worker 邏輯的腳本文件,就須要寫一次如上所示的代碼,須要多寫 N(N>=1) 次的 "worker-loader!"web

2.3 在 webpack 的配置文件中引入 worker-loader

{
  module: {
    rules: [
      {
        // 匹配 *.worker.js
        test: /\.worker\.js$/,
        use: {
          loader: 'worker-loader',
          options: {
            name: '[name]:[hash:8].js',
            // inline: true,
            // fallback: false
            // publicPath: '/scripts/workers/'
          }
        }
      }
    ]
  }
}
複製代碼

其中配置,能夠設置 inline 屬性爲 true 將 worker 做爲 blob 進行內聯; 要注意,內聯模式將額外爲瀏覽器建立 chunk,即便對於不支持內聯 worker 的瀏覽器也是如此;若這種瀏覽器想要禁用這種行爲,只須要將 fallback 參數設置爲 false 便可。算法

3. 同源策略

Web Worker 嚴格遵照同源策略,若是 webpack 的靜態資源與應用代碼不是同源的,那麼頗有可能就被瀏覽器給牆掉了,並且這種場景也常常發生。對於 Web Worker 遇到這種狀況,有兩種解決方案。npm

3.1 第一種

經過設置 worker-loader 的選項參數 inline 把 worker 內聯成 blob 數據格式,而再也不是經過下載腳本文件的方式來使用 worker:編程

App.jsapi

import Worker from './file.worker.js';
複製代碼

webpack.config.jspromise

{
  loader: 'worker-loader'
  options: { inline: true }
}
複製代碼

3.2 第二種

經過設置 worker-loader 的選項參數 publicPath 來重寫掉 worker 腳本的下載 url,固然腳本也要存放到一樣的位置:

App.js

// This will cause the worker to be downloaded from `/workers/file.worker.js`
import Worker from './file.worker.js';
複製代碼

webpack.config.js

{
  loader: 'worker-loader'
  options: { publicPath: '/workers/' }
}
複製代碼

4. devServer 模式下報錯 "window is not defined"

若使用了 webpack-dev-server 啓動了本地調試服務器,則有可能會在控制檯報錯: "Uncaught ReferenceError: window is not defined"

反正我是遇到了,找了好久未果,當時仍是洗了把臉冷靜下來排查問題,嘗試着前後在 worker-loaderwebpack-dev-serverwebpack 的 github 倉庫的 issues 裏面去找,最後果真在 webpack 的 github 倉庫裏找到了碼友的提問,官方給出了答案:

只須要在 webpack 的配置文件下的 output 下,加一個屬性對:globalObject: 'this'

output: {
  path: DIST_PATH,
  publicPath: '/dist/',
  filename: '[name].bundle.[hash:8].js',
  chunkFilename: "[name].chunk.[chunkhash:8].js",
  globalObject: 'this',
},
複製代碼

5. Web Worker 出現的背景

JavaScript 引擎是單線程運行的,JavaScript 中耗時的 I/O 操做都被處理爲異步操做,它們包括鍵盤、鼠標 I/O 輸入輸出事件、窗口大小的 resize 事件、定時器(setTimeoutsetInterval)事件、Ajax 請求網絡 I/O 回調等。當這些異步任務發生的時候,它們將會被放入瀏覽器的事件任務隊列中去,等到 JavaScript 運行時執行線程空閒時候纔會按照隊列先進先出的原則被一一執行,但終究仍是單線程。

平時看似夠用的異步編程(promiseasync/await),在遇到很複雜的運算,好比說圖像的識別優化或轉換、H5遊戲引擎的實現,加解密算法操做等等,它們的不足就將逐漸體現出來。長時間運行的 js 進程會致使瀏覽器凍結用戶界面,下降用戶體驗。那有沒有什麼辦法能夠將複雜的計算從業務邏輯代碼抽離出來,讓計算運行的同時不阻塞用戶操做界面得到反饋呢?

HTML5 標準經過了 Web Worker 的規範,該規範定義了一套 api,它容許一段 js 程序運行在主線程以外的另外一個線程中。工做線程容許開發人員編寫可以長時間運行而不被用戶所中斷的後臺程序, 去執行事務或者邏輯,並同時保證頁面對用戶的及時響應,能夠將一些大量計算的代碼交給web worker運行而不凍結用戶界面。

5. Web Worker 的類型

以前一直認爲不就那一種類型嗎,哪裏還會有多類型的 Worker。答案是有的,其可分爲兩種類型:

  1. 專用 Worker, Dedicated Web Worker
  2. 共享 Worker, Shared Web Worker

「專用 Worker」只能被建立它的頁面訪問,而「共享 Worker」能夠在瀏覽器的多個標籤中打開的同一個頁面間共享。

在 js 代碼中,Woker 類表明 Dedicated WorkerShared Worker 類表明 Shared Web Worker

下面的一些示例代碼我就直接用 ES5 去寫了,上面教了你們怎麼使用在 ES6+Webpack 的環境下,遷移這種工做你們就當練習,多動動手。

6. 如何建立 Worker

很簡單

// 應用文件 app.js
var worker = new Worker('./my.worker.js'); // 傳入 worker 腳本文件的路徑便可
複製代碼

7. 如何與 worker 通訊

就經過兩個方法便可完成:

應用文件 app.js

// 建立 worker 實例
var worker = new Worker('./my.worker.js'); // 傳入 worker 腳本文件的路徑便可
// 監聽消息
worker.onmessage = function(evt){
  // 主線程收到工做線程的消息
};
// 主線程向工做線程發送消息
worker.postMessage({
  value: '主線程向工做線程發送消息'
});
複製代碼

worker 文件 my.worker.js

// 監聽消息
this.onmessage = function(evt){
  // 工做線程收到主線程的消息
};
this.postMessage({
  value: '工做線程向主線程發送消息'
});
複製代碼

8. Worker 的全局做用域

使用 Web Worker 最重要的一點是要知道,它所執行的 js 代碼徹底在另外一做用域中,與當前主線程的代碼不共享做用域。在 Web Worker 中,一樣有一個全局對象和其餘對象以及方法,但其代碼沒法訪問 DOM,也不能影響頁面的外觀。

Web Worker 中的全局對象是 worker 對象自己,也即 thisself 引用的都是 worker 對象,說白了,就像上一段在 my.worker.js 的代碼,this 徹底能夠換成 self,甚至能夠省略。

爲便於處理數據,Web Worker 自己也是一個最小化的運行環境,其能夠訪問或使用以下數據:

  • 最小化的 navigator 對象 包括 onLine, appName, appVersion, userAgentplatform 屬性
  • 只讀的 location 對象
  • setTimeout(), setInterval(), clearTimeout(), clearInterval() 方法
  • XMLHttpRequest 構造函數

9. 如何終止工做線程

若是在某個時機不想要 Worker 繼續運行了,那麼咱們須要終止掉這個線程,能夠調用在主線程 Worker 的 terminate 方法 或者在相應的線程中調用 close 方法:

應用文件 app.js

var worker = new Worker('./worker.js');
// ...一些操做
worker.terminate();
複製代碼

Worker 文件 my.worker.js

self.close();
複製代碼

10. Worker 的錯誤處理機制

具體來講,Worker 內部的 js 在執行過程當中只要遇到錯誤,就會觸發 error 事件。發生 error 事件時,事件對象中包含三個屬性:filename, linenomessage,分別表示發生錯誤的文件名、代碼行號和完整的錯誤消息。

worker.addEventListener('error', function (e) {
  console.log('MAIN: ', 'ERROR', e);
  console.log('filename:' + e.filename + '-message:' + e.message + '-lineno:' + e.lineno);
});
複製代碼

11. 引入腳本與庫

Worker 線程可以訪問一個全局函數 importScripts() 來引入腳本,該函數接受 0 個或者多個 URI 做爲參數來引入資源;如下例子都是合法的:

importScripts();                        /* 什麼都不引入 */
importScripts('foo.js');                /* 只引入 "foo.js" */
importScripts('foo.js', 'bar.js');      /* 引入兩個腳本 */
複製代碼

瀏覽器加載並運行每個列出的腳本。每一個腳本中的全局對象都可以被 worker 使用。若是腳本沒法加載,將拋出 NETWORK_ERROR 異常,接下來的代碼也沒法執行。而以前執行的代碼(包括使用 window.setTimeout() 異步執行的代碼)依然可以運行。importScripts() 以後的函數聲明依然會被保留,由於它們始終會在其餘代碼以前運行。

注意: 腳本的下載順序不固定,但執行時會按照傳入 importScripts() 中的文件名順序進行。這個過程是同步完成的;直到全部腳本都下載並運行完畢, importScripts() 纔會返回。

附:相關連接

worker-loader 的 github url: github.com/webpack-con…

webpack 中文文檔(社區): doc.webpack-china.org/loaders/wor…

webpack 中文文檔(第三方): www.css88.com/doc/webpack…

devServer 模式 HMR 下 issue 區:《Webpack 4.0.1 | WebWorker window is not definedgithub.com/webpack/web…

完全解決如上問題的 issue 區:《Add target: "universal"github.com/webpack/web…


微信公衆號

相關文章
相關標籤/搜索