使用模組工作站串連網路

使用網頁工作站中的 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 模組實作這項設定。此外,在檢查更新時,服務工作站必須能夠在特定情況下略過指令碼的快取

其他資源和延伸閱讀