大家都知道 HTML 5 新增了很多 API,其中就包括 Web Worker,在普通的 js 檔案上使用 ES5 編寫相關程式碼應該是完全沒有問題了,只需要在支援 H5 的瀏覽器上就能跑起來。
那如果我們需要在 ES6+Webpack 的組合環境下使用 Web Worker呢?其實也很方便,只需要注意一下個別點,接下來記錄一下我踩過的坑。
至於 Web Worker 的基礎知識和基本 api 我就放到最後面當給還不瞭解或者沒有系統使用過的讀者們去簡單閱讀一下。
1. 快速建立工程環境
假設你已經有一份 ES6+Webpack 的程式碼工程環境,而且是可以順利跑起來的;如果沒有,可以 clone 我的 github 倉庫:github.com/irm-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!"
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 遇到這種情況,有兩種解決方案。
3.1 第一種
通過設定 worker-loader
的選項引數 inline
把 worker 內聯成 blob 資料格式,而不再是通過下載指令碼檔案的方式來使用 worker:
App.js
import Worker from './file.worker.js';
複製程式碼
webpack.config.js
{
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-loader
、webpack-dev-server
和 webpack
的 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
事件、定時器(setTimeout
、setInterval
)事件、Ajax 請求網路 I/O 回撥等。當這些非同步任務發生的時候,它們將會被放入瀏覽器的事件任務佇列中去,等到 JavaScript 執行時執行執行緒空閒時候才會按照佇列先進先出的原則被一一執行,但終究還是單執行緒。
平時看似夠用的非同步程式設計(promise
、async/await
),在遇到很複雜的運算,比如說影像的識別優化或轉換、H5遊戲引擎的實現,加解密演算法操作等等,它們的不足就將逐漸體現出來。長時間執行的 js 程式會導致瀏覽器凍結使用者介面,降低使用者體驗。那有沒有什麼辦法可以將複雜的計算從業務邏輯程式碼抽離出來,讓計算執行的同時不阻塞使用者操作介面獲得反饋呢?
HTML5 標準通過了 Web Worker 的規範,該規範定義了一套 api,它允許一段 js 程式執行在主執行緒之外的另一個執行緒中。工作執行緒允許開發人員編寫能夠長時間執行而不被使用者所中斷的後臺程式, 去執行事務或者邏輯,並同時保證頁面對使用者的及時響應,可以將一些大量計算的程式碼交給web worker執行而不凍結使用者介面。
5. Web Worker 的型別
之前一直認為不就那一種型別嗎,哪裡還會有多型別的 Worker。答案是有的,其可分為兩種型別:
- 專用 Worker, Dedicated Web Worker
- 共享 Worker, Shared Web Worker
「專用 Worker」只能被建立它的頁面訪問,而「共享 Worker」可以在瀏覽器的多個標籤中開啟的同一個頁面間共享。
在 js 程式碼中,Woker
類代表 Dedicated Worker
;Shared 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 物件本身,也即 this
和 self
引用的都是 worker 物件,說白了,就像上一段在 my.worker.js
的程式碼,this
完全可以換成 self
,甚至可以省略。
為便於處理資料,Web Worker 本身也是一個最小化的執行環境,其可以訪問或使用如下資料:
- 最小化的
navigator
物件 包括onLine
,appName
,appVersion
,userAgent
和platform
屬性 - 只讀的
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
, lineno
和 message
,分別表示發生錯誤的檔名、程式碼行號和完整的錯誤訊息。
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 defined
》
github.com/webpack/web…
徹底解決如上問題的 issue 區:《Add target: "universal"
》
github.com/webpack/web…