使用模組工作站串連網路

使用網路工作處理程序中的 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 對工作站的支援相當優秀,且經過最佳化調整,但也代表其長期採用 JavaScript 模組。由於工作站在設計時沒有模組系統,因此用於將程式碼載入工作站及撰寫指令碼的 API,與 2009 年常見的同步指令碼載入方法十分相似。

歷史記錄:傳統版工作站

工作站建構函式使用傳統指令碼網址,與文件網址相對。它會立即傳回新工作站執行個體的參照,其中會公開訊息介面,以及會立即停止並刪除 worker 的 terminate() 方法。

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

importScripts() 函式可在網路工作站內載入其他程式碼,但會暫停 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';
}

因此,網路工作站以往會對應用程式架構造成巨大影響。開發人員必須打造巧妙的工具和解決方法,才能在不採用新型開發做法的情況下使用 Web Worker。舉例來說,webpack 等套裝組合器會將小型模組載入器實作嵌入產生的程式碼中,這些程式碼會使用 importScripts() 載入程式碼,但會將模組包裝在函式中,以避免發生變數衝突,並模擬依附元件匯入和匯出作業。

輸入模組工作站

具有 JavaScript 模組人體工學且效能優勢的新網路工作站模式,在 Chrome 80 版中稱為模組工作站。Worker 建構函式現在接受新的 {type:"module"} 選項,該選項會將指令碼載入和執行作業變更為符合 <script type="module">

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

模組工作站是標準 JavaScript 模組,因此可以使用匯入和匯出陳述式。和所有 JavaScript 模組一樣,依附元件只會在指定結構定義 (主執行緒、工作站等) 中執行一次,且日後的所有匯入作業都會參照已經執行的模組執行個體。JavaScript 模組的載入及執行方式也會受到瀏覽器最佳化。模組的依附元件可在執行模組之前載入,藉此平行載入整個模組樹狀結構。模組載入作業也會快取剖析的程式碼,也就是說,在主執行緒和工作站中使用的模組只需剖析一次。

移至 JavaScript 模組也可讓您使用動態匯入來延遲載入程式碼,而不必封鎖工作站的執行作業。動態匯入比使用 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() 方法無法在模組工作站中使用。將工作站切換為使用 JavaScript 模組,系統會以嚴格模式載入所有程式碼。另一個值得注意的變更是,JavaScript 模組頂層範圍的 this 值為 undefined,而在傳統版工作站中,這個值則是工作站的全域範圍。所幸,全域 self 一律會提供全域範圍的參照。適用於所有類型的工作站,包括服務工作站和 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>

預先載入的模組也可由主執行緒和模組工作站使用。如果模組同時匯入這兩個結構定義,或無法事先得知模組是否會在主執行緒或 worker 中使用,這個功能就非常實用。

以前,預先載入網路工作站指令碼的選項有限,且不可靠。傳統版工作站擁有自己的「工作站」資源類型可預先載入,但沒有任何瀏覽器實作 <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 引數。可用選項與專用 worker 相同,其中包含取代前一個 name 引數的 name 選項。

Service Worker 呢?

Service Worker 規格已完成更新,支援接受 JavaScript 模組做為進入點 (使用與模組工作站相同的 {type:"module"} 選項),但這項變更尚未在瀏覽器中實作。此時,您可以使用下列程式碼,使用 JavaScript 模組將 Service Worker 例項化:

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

現在規格已更新,瀏覽器就可以開始採用新的行為。這需要時間,因為有一些額外的小工具會將 JavaScript 模組提供給 Service Worker。在判斷是否要觸發更新時,服務工作站註冊需要比較匯入的指令碼與先前的快取版本,而這需要在用於服務工作站的 JavaScript 模組中進行實作。此外,在檢查更新時,服務工作站必須能夠在某些情況下略過指令碼的快取

其他資源與延伸閱讀