使用網頁工作站中的 JavaScript 模組,現在可以更輕鬆地將繁重的工作移至背景執行緒。
JavaScript 採用單一執行緒,因此一次只能執行一項作業。這直觀且適用於許多網路案例,但如果我們需要處理資料處理、剖析、運算或分析等繁重工作,就可能產生問題。隨著網路上推送的應用程式愈來愈複雜,多執行緒處理的需求也隨之增加。
在網路平台,執行緒和平行處理的主要基本功能是 Web Workers API。工作站是作業系統執行緒上的輕量抽象化機制,可公開用於跨執行緒通訊的訊息傳遞 API。在執行昂貴的運算或操作大型資料集時,這項功能非常實用,可在一或多個背景執行緒執行成本較高的作業時,讓主執行緒順暢執行。
以下是工作站的常見使用情況範例,工作站指令碼會監聽主執行緒中的訊息,並自行傳回訊息來回應:
page.js:
const worker = new Worker('worker.js');
worker.addEventListener('message', e => {
console.log(e.data);
});
worker.postMessage('hello');
worker.js:
addEventListener('message', e => {
if (e.data === 'hello') {
postMessage('world');
}
});
十年來,Web Worker API 已在大多數瀏覽器中推出。雖然這意味著工作站擁有絕佳的瀏覽器支援,並且經過優化,但這也代表工作站具有較長的預先 JavaScript 模組。由於在設計工作站時並沒有模組系統,用於將程式碼載入工作站和撰寫指令碼的 API 仍與 2009 年常見的同步指令碼載入方法相似。
記錄:傳統版 worker
工作站建構函式會採用「傳統指令碼」網址,這是相對於文件網址。它會立即回傳對新的工作站執行個體的參照,進而公開訊息介面以及可立即停止並刪除 worker 的 terminate()
方法。
const worker = new Worker('worker.js');
importScripts()
函式可在網路工作站中使用,以便載入其他程式碼,但會暫停工作站執行,以便擷取和評估每個指令碼。它也會執行全域範圍的指令碼,例如傳統版 <script>
標記,表示某個指令碼中的變數可被另一個指令碼中的變數覆寫。
worker.js:
importScripts('greet.js');
// ^ could block for seconds
addEventListener('message', e => {
postMessage(sayHello());
});
greet.js:
// global to the whole worker
function sayHello() {
return 'world';
}
因此,網路工作者向來對應用程式的架構造成了巨大影響。開發人員必須建立巧妙的工具和解決方法,才能在不放棄現代開發做法的情況下,使用網路工作站。舉例來說,Webpack 等套裝組合工具會將小型模組載入器實作項目嵌入產生的程式碼,而該程式碼會使用 importScripts()
載入程式碼,但會將模組納入函式中,以避免變數衝突,並模擬依附元件匯入和匯出作業。
進入模組 worker
Chrome 80 版推出名為「模組工作站」,為網路工作人員提供符合人體工學和效能優勢的新模式。Worker
建構函式現在接受新的 {type:"module"}
選項,可變更指令碼載入和執行作業以符合 <script type="module">
。
const worker = new Worker('worker.js', {
type: 'module'
});
由於模組工作站為標準的 JavaScript 模組,因此可以使用匯入和匯出陳述式。和所有 JavaScript 模組一樣,依附元件只會在指定結構定義 (主執行緒、工作站等) 中執行一次,而且日後的所有匯入作業都會參照已經執行的模組執行個體。瀏覽器也會最佳化 JavaScript 模組的載入及執行。您可以在執行模組之前載入模組的依附元件,進而平行載入整個模組樹狀結構。模組載入也會快取剖析的程式碼,也就是說,在主執行緒及工作站中使用的模組只需剖析一次即可。
遷移至 JavaScript 模組也可以讓動態匯入用於延遲載入程式碼,而不會阻斷 worker 的執行作業。動態匯入功能比使用 importScripts()
載入依附元件更明確,因為會傳回已匯入模組的匯出內容,而非依賴全域變數。
worker.js:
import { sayHello } from './greet.js';
addEventListener('message', e => {
postMessage(sayHello());
});
greet.js:
import greetings from './data.js';
export function sayHello() {
return greetings.hello;
}
為了確保效能,舊的 importScripts()
方法無法在模組 worker 中使用。將工作站切換為使用 JavaScript 模組,表示所有程式碼都會以嚴格模式載入。另一個值得注意的變更是 JavaScript 模組的頂層範圍內的 this
值為 undefined
,在傳統版工作站中則值為 worker 的全域範圍。幸運的是,我們始終有一個提供全域範圍的 self
全域參照。適用於所有類型的工作站,包括 Service Worker 及 DOM。
使用 modulepreload
預先載入工作站
模組工作站可提供一項重大的效能改善,就是預先載入工作站及其依附元件。使用模組工作站時,系統會以標準 JavaScript 模組載入及執行指令碼,因此可以使用 modulepreload
預先載入,甚至預先剖析這些模組:
<!-- preloads worker.js and its dependencies: -->
<link rel="modulepreload" href="worker.js">
<script>
addEventListener('load', () => {
// our worker code is likely already parsed and ready to execute!
const worker = new Worker('worker.js', { type: 'module' });
});
</script>
預先載入的模組也可以由主執行緒和模組工作站使用。如果模組同時匯入兩種結構定義,或者無法事先得知模組將用於主執行緒或工作站中,這就非常實用。
之前,預先載入網路工作站指令碼的選項有限,不一定可靠。傳統版工作站具有專屬的「工作站」資源類型進行預先載入,但沒有瀏覽器實作 <link rel="preload" as="worker">
。因此,預先載入網路工作站的主要技術是使用 <link rel="prefetch">
,完全仰賴 HTTP 快取。與正確的快取標頭搭配使用時,就能避免工作站執行個體化,就不用等候下載工作站指令碼。不過,這項技巧與 modulepreload
不同,不支援預先載入依附元件或預先剖析。
共用員工呢?
共用工作站已在 Chrome 第 83 版中更新,支援 JavaScript 模組。和專用工作站一樣,透過 {type:"module"}
選項建構共用工作站現在會將工作站指令碼載入為模組,而非傳統指令碼:
const worker = new SharedWorker('/worker.js', {
type: 'module'
});
開始支援 JavaScript 模組之前,SharedWorker()
建構函式只接受網址和選用的 name
引數。這將持續適用於傳統版共用工作站,但建立模組共用工作站時需要使用新的 options
引數。可用選項與專屬工作站相同,包括取代先前的 name
引數的 name
選項。
Service Worker 呢?
Service Worker 規格已經更新,現支援接受 JavaScript 模組做為進入點 (使用與模組工作站相同的 {type:"module"}
選項),但這項變更尚未導入瀏覽器。完成後,就能使用下列程式碼,使用 JavaScript 模組將 Service Worker 執行個體化:
navigator.serviceWorker.register('/sw.js', {
type: 'module'
});
由於規格已更新,瀏覽器現在可以開始實作新行為。這需要時間,因為將 JavaScript 模組提供給 Service Worker 時,有一些額外複雜問題。系統在判斷是否要觸發更新時,需要在服務工作站註冊作業中比較匯入的指令碼與先前的快取版本,因此用於服務工作站時,必須針對 JavaScript 模組實作這項設定。此外,在檢查更新時,服務工作站必須能夠在特定情況下略過指令碼的快取。
其他資源和延伸閱讀
- 功能狀態、瀏覽器共識和標準化
- 新增原始模組 worker 規格
- 共用工作站的 JavaScript 模組
- 服務工作處理程序的 JavaScript 模組:Chrome 實作狀態