使用模組工作站串連網路

使用網頁工作站中的 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 模組之前就已存在。由於在設計工作站時並沒有模組系統,用於將程式碼載入工作站和撰寫指令碼的 API 與 2009 年常見的同步指令碼載入方法一樣。

歷史:傳統 worker

Worker 建構函式會採用傳統指令碼網址,該網址會相對於文件網址。它會立即傳回新 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() 進行程式碼載入作業,但會將模組包裝在函式中,以避免變數衝突,並模擬依附元件匯入和匯出作業。

輸入模組工作者

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

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

使用 modulepreload 預先載入工作站

模組 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 引數。可用的選項與專屬工作站相同,包括取代先前 name 引數的 name 選項。

服務工作者又如何?

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

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

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

其他資源和延伸閱讀