使用模組工作站串連網路

您現在可以使用網路工作站中的 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 已在大多數瀏覽器中提供超過十年。雖然這表示 worker 有優異的瀏覽器支援,且經過妥善最佳化,但也表示 worker 早在 JavaScript 模組之前就已存在。由於設計 worker 時沒有模組系統,因此用於將程式碼載入 worker 及組合指令碼的 API 仍與 2009 年常見的同步指令碼載入方法相似。

歷史:傳統 worker

Worker 建構函式會採用傳統指令碼網址,該網址會相對於文件網址。它會立即傳回新 worker 例項的參照,該例項會公開訊息介面,以及立即停止並銷毀 worker 的 terminate() 方法。

const worker = new Worker('worker.js');

importScripts() 函式可在網路 worker 中使用,用於載入其他程式碼,但會暫停 worker 的執行,以便擷取及評估每個指令碼。它也會像經典的 <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 推出了一個新的模式,可為網路工作站提供 JavaScript 模組的操作體驗和效能優勢,稱為模組工作站。Worker 建構函式現在接受新的 {type:"module"} 選項,可變更指令碼載入和執行作業,以符合 <script type="module">

const worker = new Worker('worker.js', {
  type: 'module'
});

模組工作站是標準 JavaScript 模組,因此可以使用匯入和匯出陳述式。與所有 JavaScript 模組一樣,依附元件只會在特定情境 (主執行緒、工作站等) 中執行一次,而所有日後的匯入作業都會參照已執行的模組執行個體。瀏覽器也會針對 JavaScript 模組的載入和執行作業進行最佳化。模組的依附元件可在模組執行前載入,這樣就能同時載入整個模組樹狀結構。模組載入也會快取剖析的程式碼,也就是說,在主執行緒和 worker 中使用的模組只需剖析一次。

改用 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;
}

為確保最佳效能,模組 worker 中無法使用舊版 importScripts() 方法。將 worker 切換為使用 JavaScript 模組,表示所有程式碼都會以嚴格模式載入。另一個值得注意的變更是,在 JavaScript 模組的頂層範圍中,this 的值為 undefined,而在傳統 worker 中,這個值則為 worker 的全域範圍。幸運的是,一直都有一個 self 全域變數,可提供全域範圍的參照。這個屬性適用於所有類型的 worker,包括服務 worker 和 DOM。

使用 modulepreload 預先載入工作站

模組 worker 帶來的一大效能改善,就是能夠預先載入 worker 及其依附元件。使用模組 worker 時,指令碼會以標準 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>

預先載入的模組也可供主執行緒和模組工作站使用。這對於在兩種情境下匯入模組,或無法事先得知模組會在主執行緒或 worker 中使用時,非常實用。

先前,可用於預先載入網路 worker 指令碼的選項有限,且不一定可靠。傳統工作站有自己的「worker」資源類型可用於預先載入,但沒有瀏覽器實作 <link rel="preload" as="worker">。因此,用於預先載入網頁工作項的主要技巧是使用 <link rel="prefetch">,這項技巧完全仰賴 HTTP 快取。搭配正確的快取標頭使用時,這可避免工作站例項化必須等待下載工作站指令碼。不過,與 modulepreload 不同的是,這項技巧不支援預先載入依附元件或預先剖析。

共用 worker 的情況如何?

共用工作者已更新為支援 JavaScript 模組,並自 Chrome 83 版起生效。如同專屬工作站,使用 {type:"module"} 選項建構共用工作站時,現在會以模組而非傳統指令碼的形式載入工作站指令碼:

const worker = new SharedWorker('/worker.js', {
  type: 'module'
});

在支援 JavaScript 模組之前,SharedWorker() 建構函式只會預期網址和選用的 name 引數。這項功能將繼續支援傳統共用工作站的使用方式;不過,建立模組共用工作站時,必須使用新的 options 引數。可用的選項與專屬 worker 相同,包括取代先前 name 引數的 name 選項。

服務工作者又如何?

服務工作者規格已更新,可接受 JavaScript 模組做為進入點,並使用與模組 worker 相同的 {type:"module"} 選項,但這項變更尚未在瀏覽器中實作。發生這種情況後,您就可以使用下列程式碼,透過 JavaScript 模組將服務工作者例項化:

navigator.serviceWorker.register('/sw.js', {
  type: 'module'
});

規格已更新,瀏覽器也開始實作新行為。由於將 JavaScript 模組帶入服務工作站會涉及一些額外的複雜性,因此這項作業需要花費一些時間。決定是否要觸發更新時,服務工作者註冊程序需要將匯入的程式碼與先前快取的版本進行比較,並在用於服務工作者時,為 JavaScript 模組實作這項作業。此外,在某些情況下,服務工作者在檢查更新時,需要略過快取的程式碼。

其他資源和延伸閱讀