Web Worker
Web?Worker
https://www.zoo.team/article/web-worker

前言
眾所周知,JavaScript 是單線程的語言。當(dāng)我們面臨需要大量計(jì)算的場(chǎng)景時(shí)(比如視頻解碼等),UI 線程就會(huì)被阻塞,甚至瀏覽器直接卡死。現(xiàn)在前端遇到大量計(jì)算的場(chǎng)景越來越多,為了有更好的體驗(yàn),HTML5 中提出了 Web Worker 的概念。Web Worker 可以使腳本運(yùn)行在新的線程中,它們獨(dú)立于主線程,可以進(jìn)行大量的計(jì)算活動(dòng),而不會(huì)影響主線程的 UI 渲染。當(dāng)計(jì)算結(jié)束之后,它們可以把結(jié)果發(fā)送給主線程,從而形成了高效、良好的用戶體驗(yàn)。Web Worker 是一個(gè)統(tǒng)稱,具體可以細(xì)分為普通的 Worker、SharedWorker 和 ServiceWorker 等,接下來我們一一介紹其使用方法和適合的場(chǎng)景。
普通 Worker
- 創(chuàng)建 Worker 通過 new 的方式來生成一個(gè)實(shí)例,參數(shù)為 url 地址,該地址必須和其創(chuàng)建者是同源的。
const?worker?=?new?Worker('./worker.js');?//?參數(shù)是?url,這個(gè)?url?必須與創(chuàng)建者同源?
- Worker 的方法
onmessage 主線程中可以在 Worker 上添加 onmessage 方法,用于監(jiān)聽 Worker 的信息。
示例:
const?worker?=?new?Worker('./worker.js');
worker.onmessage?=?function?(messageEvent)?{
?console.log(messageEvent)
}?
onmessageerror 主線程中可以在 Worker 上添加 onmessageerror 方法,用于監(jiān)聽 Worker 的錯(cuò)誤信息。
示例:
const?worker?=?new?Worker('./worker.js');
worker.onmessageerror?=?function?(messageEvent)?{
?console.log(messageEvent)
}?
postMessage() 主線程通過此方法給 Worker 發(fā)送消息,發(fā)送參數(shù)的格式不限(可以是數(shù)組、對(duì)象、字符串等),可以根據(jù)自己的業(yè)務(wù)選擇。
示例:
const?worker?=?new?Worker('./worker.js');
worker.postMessage({?type:?'start',?payload:?{?count:?666?}?});?//?發(fā)送信息給worker
terminate() 主線程通過此方法終止 Worker 的運(yùn)行。
示例:
const?worker?=?new?Worker('./worker.js');
worker.terminate();
通信
Worker 的作用域跟主線程中的 Window 是相互獨(dú)立的,并且 Worker 中是獲取不到 DOM 元素的。所以在 Worker 中你無法使用 Window 變量。取而代之的是可以用 self 來表示全局對(duì)象。self 上有哪些方法和屬性,感興趣的小伙伴可以自行輸出查看。比較常用的方法是 onmessage、postMessage,主要用來跟主線程進(jìn)行通信。
示例:
//?監(jiān)聽事件,主線程可以通過?postMessage?發(fā)送信息過來
self.onmessage?=?(messageEvent)?=>?{
?const?{?type,?payload?}?=?messageEvent.data;
??switch?(type)?{
????case?'start':
??????//?通過?type?去區(qū)分不同的業(yè)務(wù)邏輯,payload?是傳過來的數(shù)據(jù)
??????const?result?=?0;
??????//?....,通過一系列處理之后,把最終的結(jié)果發(fā)送給主線程
??????this.postMessage(result);
??????break;
??}
};
這里我們從 messageEvent.data 中獲取從主線程傳遞過來的數(shù)據(jù)。為了業(yè)務(wù)的擴(kuò)展性,這邊是以 type 去區(qū)分不同的業(yè)務(wù),payload 承載數(shù)據(jù)源,通過處理之后把結(jié)果發(fā)給主線程。主線程的 onmessage 回調(diào)函數(shù)中就能收到這個(gè)結(jié)果了。
- Worker 中引用其他腳本的方式
跟常用的 JavaScript 一樣,Worker 中也是可以引入其他的模塊的。但是方式不太一樣,是通過 importScripts 來引入。這邊我為了演示,新建了一個(gè) constant.js。在 constant.js 定義了一些變量和函數(shù)。
示例:
//?Worker.js
importScripts('constant.js');
//?下面就可以獲取到?constant.js?中的所有變量了
//?constant.js
//?可以在?Worker?中使用
const?a?=?111;
//?不可以在 Worker 中使用,原因未知
const?b?=?function?()?{
??console.log('test');
};
//?可以在?Worker?中使用
function?c()?{
??console.log('test');
}
調(diào)試方法
寫代碼難免要進(jìn)行調(diào)試。Worker 的調(diào)試在瀏覽器控制臺(tái)中有專門展示的地方,見下圖。

常見使用場(chǎng)景
一般的視頻網(wǎng)站 以優(yōu)酷為例,當(dāng)我們開始播放優(yōu)酷視頻的時(shí)候,就能看到它會(huì)調(diào)用 Worker,解碼的代碼應(yīng)該寫在 Worker 里面。

需要大量計(jì)算的網(wǎng)站 比如 imgcook 這個(gè)網(wǎng)站,它能在前端解析 sketch 文件,這部分解析的邏輯就寫在 Worker 里。

SharedWorker
SharedWorker 是一種特定的 Worker。從它的命名就能知道,它是一種共享數(shù)據(jù)的 Worker。它可以同時(shí)被多個(gè)瀏覽器環(huán)境訪問。這些瀏覽器環(huán)境可以是多個(gè) window, iframes 或者甚至是多個(gè) Worker,只要這些 Workers 處于同一主域。為跨瀏覽器 tab 共享數(shù)據(jù)提供了一種解決方案。
創(chuàng)建 SharedWorker
創(chuàng)建的方法跟上面普通 Worker 完全一模一樣。
const?worker?=?new?SharedWorker("./shareWorker.js");?//?參數(shù)是?url,這個(gè)?url?必須與創(chuàng)建者同源?
SharedWorker 的方法
SharedWorker 的方法都在 port 上,這是它與普通 Worker 不同的地方。
port.onmessage
主線程中可以在 worker 上添加 onmessage 方法,用于監(jiān)聽 SharedWorker 的信息
示例:
const?sharedWorker?=?new?SharedWorker('./shareWorker.js');
sharedWorker.port.onmessage?=?function?(messageEvent)?{
??console.log(messageEvent)
}?
port.postMessage()
主線程通過此方法給 SharedWorker 發(fā)送消息,發(fā)送參數(shù)的格式不限
示例:
const?sharedWorker?=?new?SharedWorker('./shareWorker.js');
sharedWorker.port.postMessage({?type:?'increase',?payload:?{?count:?666?}?});
port.start()
主線程通過此方法開啟 SharedWorker 之間的通信
示例:
const?sharedWorker?=?new?SharedWorker('./shareWorker.js');
sharedWorker.port.start()
port.close()
主線程通過此方法關(guān)閉 SharedWorker
示例:
const?sharedWorker?=?new?SharedWorker('./shareWorker.js');
sharedWorker.port.close()
通信
SharedWorker 跟普通的 Worker 一樣,可以用 self 來表示全局對(duì)象。不同之處是,它需要等 port 連接成功之后,利用 port 的onmessage、postMessage,來跟主線程進(jìn)行通信。當(dāng)你打開多個(gè)窗口的時(shí)候,SharedWorker 的作用域是公用的,這也是其特點(diǎn)。
示例:
//?index.js
const?worker?=?new?SharedWorker('./shareWorker.js');
worker.port.start();?//?開啟端口
//?發(fā)送信息給?shareWorker
worker.port.postMessage({?type:?'increase',?payload:?{?count:?666?}?});?
//?接受?shareWorker?發(fā)過來的數(shù)據(jù)
worker.port.onmessage?=?function?(val)?{
??console.log(val.data)
};
//?shareWorker.js
let?count?=?666;
port.onmessage?=?(messageEvent)?=>?{
??const?{?type,?payload?}?=?messageEvent.data;
??switch?(type)?{
????case?'increase':
??????port.postMessage(++count);
??????break;
????case?'decrease':
??????port.postMessage(--count);
??????break;
??}
};
Worker 中引用其他腳本
這個(gè)與普通的 Worker 方法一樣,使用 importScripts
調(diào)試方法
在瀏覽器中查看和調(diào)試 SharedWorker 的代碼,需要輸入 chrome://inspect/

ServiceWorker
ServiceWorker 一般作為 Web 應(yīng)用程序、瀏覽器和網(wǎng)絡(luò)之間的代理服務(wù)。他們旨在創(chuàng)建有效的離線體驗(yàn),攔截網(wǎng)絡(luò)請(qǐng)求,以及根據(jù)網(wǎng)絡(luò)是否可用采取合適的行動(dòng),更新駐留在服務(wù)器上的資源。他們還將允許訪問推送通知和后臺(tái)同步 API。
- 創(chuàng)建 ServiceWorker
//?index.js
if?('serviceWorker'?in?navigator)?{
??window.addEventListener('load',?function?()?{
????navigator.serviceWorker
??????.register('./serviceWorker.js',?{?scope:?'/page/'?})
??????.then(
??????function?(registration)?{
????????console.log('ServiceWorker?registration?successful?with?scope:?',
????????????????????registration.scope);
??????},
??????function?(err)?{
????????console.log('ServiceWorker?registration?failed:?',?err);
??????}
????);
??});
}
只要?jiǎng)?chuàng)建了 ServiceWorker,不管這個(gè)創(chuàng)建 ServiceWorker 的 html 是否打開,這個(gè) ServiceWorker 是一直存在的。它會(huì)代理范圍是根據(jù) scope 決定的,如果沒有這個(gè)參數(shù),則其代理范圍是創(chuàng)建目錄同級(jí)別以及子目錄下所有頁面的網(wǎng)絡(luò)請(qǐng)求。代理的范圍可以通過 registration.scope 查看。
- 安裝 ServiceWorker
//?serviceWorker.js
const?CACHE_NAME?=?'cache-v1';
//?需要緩存的文件
const?urlsToCache?=?[
??'/style/main.css',
??'/constant.js',
??'/serviceWorker.html',
??'/page/index.html',
??'/serviceWorker.js',
??'/image/131.png',
];
self.oninstall?=?(event)?=>?{
??event.waitUntil(
????caches
????.open(CACHE_NAME)?//?這返回的是?promise
????.then(function?(cache)?{
??????return?cache.addAll(urlsToCache);?//?這返回的是?promise
????})
??);
};
在上述代碼中,我們可以看到,在 install 事件的回調(diào)中,我們打開了名字為 cache-v1 的緩存,它返回的是一個(gè) promise。在打開緩存之后,我們需要把要緩存的文件 add 進(jìn)去,基本上所有類型的資源都可以進(jìn)行緩存,例子中緩存了 css、js、html、png。如果所有緩存數(shù)據(jù)都成功,就表示 ServiceWorker 安裝成功;如果控制臺(tái)提示 Uncaught (in promise) TypeError: Failed to execute 'Cache' on 'addAll': Request failed,則表示安裝失敗。
- 緩存和返回請(qǐng)求
self.onfetch?=?(event)?=>?{
??event.respondWith(
????caches
????.match(event.request)?//?此方法從服務(wù)工作線程所創(chuàng)建的任何緩存中查找緩存的結(jié)果
????.then(function?(response)?{
??????//?response?為匹配到的緩存資源,如果沒有匹配到則返回?undefined,需要?fetch?資源
??????if?(response)?{
????????return?response;
??????}
??????return?fetch(event.request);
????})
??);
};
在 fetch 事件的回調(diào)中,我們?nèi)テヅ?cache 中的資源。如果匹配到,則使用緩存資源;沒有匹配到則用 fetch 請(qǐng)求。正因?yàn)?ServiceWorker 可以代理網(wǎng)絡(luò)請(qǐng)求,所以為了安全起見,規(guī)范中規(guī)定它只能在 https 和 localhost 下才能開啟。
調(diào)試方法
在瀏覽器中查看和調(diào)試 ServiceWorker 的代碼,需要輸入 chrome://inspect/#service-workers

- 演示效果
上面代碼中,我緩存了 131.png。切換到離線模式,131 圖片還是能顯示,134.png 就獲取不到了。

看到這里,大家可能會(huì)有疑惑了。這個(gè)圖片它存到哪里去了?實(shí)際上它會(huì)把文件自動(dòng)存到瀏覽器的 Cache Storage 中。我們打開瀏覽器可以看到。

常見使用場(chǎng)景
緩存資源文件,加快渲染速度
這個(gè)我們以語雀為例。我們?cè)诖蜷_語雀網(wǎng)站的時(shí)候,可以看到它使用 ServiceWorker 緩存了很多 css、js 文件,從而達(dá)到優(yōu)化的效果。

總結(jié)
| 類型 | Worker | SharedWorker | ServiceWorker |
|---|---|---|---|
| 通信方式 | postMessage | port.postMessage | 單向通信,通過 addEventListener 監(jiān)聽 serviceWorker 的狀態(tài) |
| 使用場(chǎng)景 | 適合大量計(jì)算的場(chǎng)景 | 適合跨 tab、iframes 之間共享數(shù)據(jù) | 緩存資源、網(wǎng)絡(luò)優(yōu)化 |
| 兼容性 | >= IE 10 >= Chrome 4 | 不支持 IE、Safari、Android、iOS >= Chrome 4 | 不支持 IE >= Chrome 40 |
本文介紹了 3 種 Worker,他們分別適合不同的場(chǎng)景,總結(jié)如上面表格。普通的 Worker 可以在需要大量計(jì)算的時(shí)候使用,創(chuàng)建新的線程可以降低主線程的計(jì)算壓力,不會(huì)導(dǎo)致 UI 卡頓。SharedWorker 主要是為不同的 window、iframes 之間共享數(shù)據(jù)提供了另外一個(gè)解決方案。ServiceWorker 可以緩存資源,提供離線服務(wù)或者是網(wǎng)絡(luò)優(yōu)化,加快 Web 應(yīng)用的開啟速度,更多是優(yōu)化體驗(yàn)方面的。
示例代碼:https://github.com/Pulset/Web-Worker
參考文獻(xiàn)
- 在網(wǎng)絡(luò)應(yīng)用中添加服務(wù)工作線程和離線功能(https://developers.google.com/web/fundamentals/codelabs/offline)
- Service worker overview(https://developer.chrome.com/docs/workbox/service-worker-overview/)
- Workers(https://developer.mozilla.org/zh-CN/docs/Web/API/Worker)
- SharedWorker(https://developer.mozilla.org/zh-CN/docs/Web/API/SharedWorker)
看完兩件事
如果你覺得這篇內(nèi)容對(duì)你挺有啟發(fā),我想邀請(qǐng)你幫我兩件小事
1.點(diǎn)個(gè)「在看」,讓更多人也能看到這篇內(nèi)容(點(diǎn)了「在看」,bug -1 ??)
2.關(guān)注公眾號(hào)「政采云前端團(tuán)隊(duì)」,持續(xù)為你推送精選好文招賢納士
政采云前端團(tuán)隊(duì)(ZooTeam),一個(gè)年輕富有激情和創(chuàng)造力的前端團(tuán)隊(duì),隸屬于政采云產(chǎn)品研發(fā)部,Base 在風(fēng)景如畫的杭州。團(tuán)隊(duì)現(xiàn)有 60 余個(gè)前端小伙伴,平均年齡 27 歲,近 3 成是全棧工程師,妥妥的青年風(fēng)暴團(tuán)。成員構(gòu)成既有來自于阿里、網(wǎng)易的“老”兵,也有浙大、中科大、杭電等校的應(yīng)屆新人。團(tuán)隊(duì)在日常的業(yè)務(wù)對(duì)接之外,還在物料體系、工程平臺(tái)、搭建平臺(tái)、性能體驗(yàn)、云端應(yīng)用、數(shù)據(jù)分析及可視化等方向進(jìn)行技術(shù)探索和實(shí)戰(zhàn),推動(dòng)并落地了一系列的內(nèi)部技術(shù)產(chǎn)品,持續(xù)探索前端技術(shù)體系的新邊界。
如果你想改變一直被事折騰,希望開始能折騰事;如果你想改變一直被告誡需要多些想法,卻無從破局;如果你想改變你有能力去做成那個(gè)結(jié)果,卻不需要你;如果你想改變你想做成的事需要一個(gè)團(tuán)隊(duì)去支撐,但沒你帶人的位置;如果你想改變既定的節(jié)奏,將會(huì)是“5 年工作時(shí)間 3 年工作經(jīng)驗(yàn)”;如果你想改變本來悟性不錯(cuò),但總是有那一層窗戶紙的模糊… 如果你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的自己。如果你希望參與到隨著業(yè)務(wù)騰飛的過程,親手推動(dòng)一個(gè)有著深入的業(yè)務(wù)理解、完善的技術(shù)體系、技術(shù)創(chuàng)造價(jià)值、影響力外溢的前端團(tuán)隊(duì)的成長(zhǎng)歷程,我覺得我們?cè)摿牧摹H魏螘r(shí)間,等著你寫點(diǎn)什么,發(fā)給 [email protected]
