大家都知道 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()
才會返回。