使用模組工作站串連網路

現在,您可以使用網頁工作人員中的 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() 函式,可載入額外程式碼,但會暫停工作人員的執行作業,以便擷取及評估每個指令碼。此外,這個代碼也會在全域範圍內執行指令碼,就像傳統的 <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 將推出新的網頁工作站模式,具備 JavaScript 模組的人體工學和效能優勢,稱為模組工作站。Worker 建構函式現在接受新的 {type:"module"} 選項,可變更指令碼載入和執行方式,以符合 <script type="module">

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

由於模組工作站是標準 JavaScript 模組,因此可以使用匯入和匯出陳述式。與所有 JavaScript 模組一樣,依附元件只會在特定環境 (主執行緒、工作站等) 中執行一次,所有後續匯入都會參照已執行的模組例項。瀏覽器也會最佳化 JavaScript 模組的載入和執行作業。模組的依附元件可在模組執行前載入,因此整個模組樹狀結構可以平行載入。模組載入作業也會快取剖析的程式碼,因此在主執行緒和工作站中使用的模組只需要剖析一次。

改用 JavaScript 模組後,您也可以使用動態 import 延遲載入程式碼,不會阻礙工作人員執行作業。相較於使用 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 全域變數,可提供全域範圍的參照。這項功能適用於所有類型的 Worker (包括 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>

主執行緒和模組工作站都可以使用預先載入的模組。如果模組同時在這兩種環境中匯入,或是無法預先得知模組是否會在主執行緒或工作站中使用,這項功能就非常實用。

先前可預先載入 Web 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() 建構函式只會預期有 URL 和選用的 name 引數。這項功能仍適用於傳統共用工作站的使用情境,但如要建立模組共用工作站,必須使用新的 options 引數。可用選項與專用工作站相同,包括取代先前 name 引數的 name 選項。

服務工作人員呢?

服務工作人員規格已更新,支援接受 JavaScript 模組做為進入點,並使用與模組工作人員相同的 {type:"module"} 選項,但瀏覽器尚未實作這項變更。完成後,您就能使用 JavaScript 模組,透過下列程式碼例項化服務工作人員:

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

規格更新後,瀏覽器就會開始實作新行為。 將 JavaScript 模組帶入 Service Worker 時,會遇到一些額外的複雜情況,因此需要時間。服務工作人員註冊時,需要比較匯入的指令碼與先前快取版本,判斷是否要觸發更新,而這項作業必須在 JavaScript 模組用於服務工作人員時實作。此外,在檢查更新時,服務工作人員有時需要略過指令碼的快取

其他資源和延伸閱讀