現在,您可以使用網頁工作人員中的 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 模組用於服務工作人員時實作。此外,在檢查更新時,服務工作人員有時需要略過指令碼的快取。
其他資源和延伸閱讀
- 功能狀態、瀏覽器共識和標準化
- 新增原始模組工作人員規格
- 共用工作站的 JavaScript 模組
- 服務工作人員的 JavaScript 模組:Chrome 實作狀態