每日前端夜話0xCD
每日前端夜話,陪你聊前端。
每天晚上18:00準時推送。
正文共:4331 字
預計閱讀時間:12 分鐘
作者:Jake Archibald(Chrome開發團隊成員)
翻譯:瘋狂的技術宅
來源:jakearchibald

上個月,我們在福岡舉行的 W3C TPAC 會議上召開了 service worker 會議。這是幾年來我們第一次專注于潛在的新功能和行為。現總結如下:
複蘇(Resurrection)最終被殺死
1reg.unregister();
複制
如果你取消 service worker 注冊,則會将其從注冊清單中删除,但它仍會繼續控制現有頁面。這意味着它不會中斷任何正在進行的提取等操作。不過一旦所有這些頁面都消失了,就會被垃圾回收。
但是在規範中有一個地方講到:如果一個名為
serviceWorker.register()
的頁面具有相同的作用域,則被登出的 service worker 注冊将會“複蘇”。我不知道為什麼要這麼做。無論如何,這都是一個愚蠢的主意,是以我們把它删除了(https://github.com/w3c/ServiceWorker/pull/1415)。
1// Old behaviour:
2const reg1 = await navigator.serviceWorker.getRegistration();
3await reg1.unregister();
4const reg2 = await navigator.serviceWorker.register('/sw.js', {
5 scope: reg1.scope,
6});
7console.log(reg1 === reg2); // true!
複制
好吧,如果
reg1
不控制任何頁面,那可能是錯誤的。是的,這令人困惑。
1// New behaviour:
2const reg1 = await navigator.serviceWorker.getRegistration();
3await reg1.unregister();
4const reg2 = await navigator.serviceWorker.register('/sw.js', {
5 scope: reg1.scope,
6});
7console.log(reg1 === reg2); // Always false
複制
現在,保證
reg2
是一個新的注冊。複蘇已被殺死。
我們在 2018 年就此達成了共識,并已在 Chrome 中實作,同時在 Firefox 和 Safari 中也已經實作。
- GitHub issue (https://github.com/w3c/ServiceWorker/pull/1415).
- Chrome ticket (https://bugs.chromium.org/p/chromium/issues/detail?id=971571).
- Firefox ticket (https://bugzilla.mozilla.org/show_bug.cgi?id=1557244).
- WebKit ticket (https://bugs.webkit.org/show_bug.cgi?id=201584).
self.serviceWorker
在 service worker 中,很難獲得對自己的
ServiceWorker
執行個體的引用。用
self.registration
可以通路你的注冊,但是究竟哪個 service worker 代表你目前正在執行的服務呢?
self.registration.active
?也許是吧,也或許是
self.registration.waiting
或
self.registration.installing
,或者都不是。
作為代替:
1console.log(self.serviceWorker);
複制
上面的内容将為你提供引用,無論其處于什麼狀态。
這項小功能已在所有浏覽器中達成共識,在 Chrome 中正在積極開發。
- GitHub issue (https://github.com/w3c/ServiceWorker/issues/1077#issuecomment-504330065).
- Chrome ticket (https://bugs.chromium.org/p/chromium/issues/detail?id=977496).
- Firefox ticket (https://bugzilla.mozilla.org/show_bug.cgi?id=1560488).
- WebKit ticket (https://bugs.webkit.org/show_bug.cgi?id=199102).
頁面生命周期和 service workers
我是 page lifecycle API (https://developers.google.com/web/updates/2018/07/page-lifecycle-api) 的忠實擁護者,因為它标準化了多年來浏覽器已經完成的各種行為,特别是在手機上,例如,撤下頁面以節省記憶體和電池。
此外,會話曆史記錄 (https://html.spec.whatwg.org/multipage/history.html#the-session-history-of-browsing-contexts)可以包含 DOM 文檔,這通常稱為“後-轉發頁面緩存”或“ bfcache”。大多數浏覽器中已經存在了許多年,這是 Chrome 的最新版本(https://developers.google.com/web/updates/2019/02/back-forward-cache)。
這意味着頁面可以是:
- 當機 - 該頁面可以通過可見頁籤(作為頂層頁面或其中的 iframe)通路,該頁籤目前未選中。事件循環已暫停,是以該頁面未使用 CPU。該頁面已完全存儲在記憶體中,并且可以被當機而不會丢失任何狀态。如果使用者将焦點放在此頁籤上,則該頁面将被解凍。
- Bfcached - 與 當機類似,但是無法通過标簽通路此頁面。它作為曆史項存在于浏覽上下文中。如果存在該項目的會話導航(例如使用後退/前進),則該頁面将被當機。
- 廢棄 - 可以通過目前未選擇的可見标簽通路該頁面。但是,頁籤實際上隻是一個占位符。該頁面已完全解除安裝,不再使用記憶體。如果使用者将焦點放在此頁籤上,則将重新加載頁面。
我們需要弄清楚這些狀态怎樣适合特定的 service workers 行為:
- 一個新的 service worker 将會一直等待,直到目前活動 service worker 控制的所有頁面都消失了(可以用
跳過)。skipWaiting()
-
将傳回代表頁面的對象。clients.matchAll()
我們決定:
- 預設情況下,當機的頁面将由
傳回。Chrome 希望向用戶端對象添加clients.matchAll()
屬性,但是 Apple 的同行反對。對當機的用戶端的isFrozen
調用将被緩沖,就像client.postMessage()
一樣。BroadcastChannel
- Bfcached 和丢棄的頁面不會顯示在
中。将來我們可能會提供一種選擇加入的方式來擷取被廢棄的用戶端,以便他們可以獲得焦點(例如,響應通知點選)。clients.matchAll()
- 當機的頁面将有助于防止等待的 worker 被激活。
- Bfcached 和廢棄的頁面不會阻止等待中的工作程式被激活。如果 bfcached 頁面的控制器變得多餘(因為已激活了新的 service worker),則該 bfcached 頁面将被删除。該項目保留在會話曆史記錄中,但如果導航到該項目,則必須完全重新加載。
我甚至對所有的情況進行了測試:
現在我們隻需要指定它。
- 有關當機的文檔和 service worker 的 GitHub issue (https://github.com/w3c/ServiceWorker/pull/1442)。
- 有關 bfcache 和 service worker 的 GitHub issue (https://github.com/w3c/ServiceWorker/issues/1038)。
- 有關廢棄标簽的 GitHub issue (https://github.com/w3c/ServiceWorker/issues/626)。
将狀态附加到用戶端
當我們讨論頁面生命周期的内容時,Facebook 的同僚提到了他們如何用
postMessage
向客戶詢問其狀态,例如“使用者目前是否在鍵入消息?”。我們還注意到,我們已經讨論過向用戶端添加更多狀态(大小、密度、獨立模式、全屏等),但是很難劃清界線。
相反,我們讨論了允許開發人員将可克隆的資料附加到用戶端,這些資料将顯示在 service worker 的用戶端對象上。
1// From a page (or other client):
2await clients.setClientData({ foo: 'bar' });
複制
1// In a service worker:
2const allClients = await clients.matchAll();
3console.log(allClients[0].data); // { foo: 'bar' } or undefined.
複制
現在還處于早期,但感覺是這樣可以避免在
postMessage
上來回移動。
- GitHub issue (https://github.com/w3c/ServiceWorker/issues/1475).
立即登出 worker
如前所述,如果你登出 service workers 注冊,則會從注冊清單中将其删除,但是它将慧繼續控制現有頁面。這意味着它不會中斷正在進行的提取等操作。但是在某些情況下,無論中斷什麼事情,你都希望 service workers 立即離開。
這裡的一個用戶端是
Clear-Site-Data
(https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Clear-Site-Data)。如上所述,它正在登出 service workers,但是
Clear-Site-Data
是“立即擺脫一切”開關,是以目前行為不太正确。
正常登出将保持不變,但是我将指定一種方法來立即登出 service worker,這可能會終止正在運作的腳本并中止正在進行的提取。
Clear-Site-Data
将使用此方法,但我們也可以将其公開為 API:
1reg.unregister({ immediate: true });
複制
來自 LinkedIn 的 Asa Kusuma 已編寫了
Clear-Site-Data
測試 (https://github.com/web-platform-tests/wpt/pull/19132)。我隻需要進行規範工作就可以了,不幸的是說起來容易做起來難。
- GitHub issue(https://github.com/w3c/ServiceWorker/issues/614).
URL 模式比對
這是一個很大的問題。我們在整個平台上都使用 URL 比對,尤其是在 service worker 和 Content-Security-Policy (https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) 中。但是比對非常簡單——完全比對或字首比對。開發人員傾向于使用 path-to-regexp 之類的東西。Ben Kelly 提議我們将類似的東西帶到平台上。
它需要比正規表達式路徑更具限制性,因為我們希望能夠在共享程序(例如:浏覽器的網絡程序)中處理這些問題。RegExp 确實很複雜,并且可以進行各種拒絕服務攻擊。浏覽器供應商對開發人員有意鎖定自己的網站感到滿意,但我們并不想鎖定整個浏覽器。
這裡是 Ben 的提案 (https://github.com/wanderview/service-worker-scope-pattern-matching/blob/master/explainer.md)。這非常雄心勃勃,但是如果我們可以跨平台使用更富有表現力的 URL 那就太好了。這樣的例子非常引人注目:
1// Service worker controls `/foo` and `/foo/*`,
2// but does not control `/foobar`.
3navigator.serviceWorker.register(scriptURL, {
4 scope: new URLPattern({
5 baseUrl: self.location,
6 path: '/foo/?*',
7 }),
8});
複制
把流作為請求體
多年來,你可以流式傳輸響應:
1const response = await fetch('/whatever');
2const reader = response.body.getReader();
3
4while (true) {
5 const { done, value } = await reader.read();
6 if (done) break;
7 console.log(value); // Uint8Array of bytes
8}
複制
規範還說,你可以将流用作請求的主體,但沒有浏覽器實作。但是,Chrome 已決定再次使用它,而 Firefox 和 Safari 表示也會這樣做。
1let intervalId;
2const stream = new ReadableStream({
3 start(controller) {
4 intervalId = setInterval(() => {
5 controller.enqueue('Hello!');
6 }, 1000);
7 },
8 cancel() {
9 clearInterval(intervalId);
10 },
11}).pipeThrough(new TextEncoderStream());
12
13fetch('/whatever', {
14 method: 'POST',
15 body: stream,
16 headers: { 'Content-Type': 'text/plain; charset=UTF-8' },
17});
複制
上面的代碼将“hello”作為單個 HTTP 請求的一部分,每秒鐘一次發送到伺服器。這個例子是愚蠢的,但是它展示了一項新功能——在你獲得整個請求體内容之前将資料發送到伺服器。目前,你隻能分塊或使用 websocket 來執行此操作。
一個實際的例子是涉及上傳流式傳輸的内容。例如你可以在編碼或錄制的時候上傳視訊。
HTTP 是雙向的。該模型不是先請求後響應——你可以在仍然發送請求正文的同時開始接收響應。但是,在 TPAC 大會中,浏覽器開發人員注意到,鑒于目前的網絡棧,在擷取過程中公開這個内容确實很複雜,是以請求流的最初實作在請求完成之前不會産生響應。這還算不錯——如果你想模拟雙向通信,則可以使用一次 fetch 進行上傳,而用另一次進行下載下傳。
- GitHub issue(https://github.com/whatwg/fetch/pull/425#issuecomment-527387538).
響應後執行
這已成為 service workers 中非常普遍的模式:
1addEventListener('fetch', event => {
2 event.respondWith(async function() {
3 const response = await getResponseSomehow();
4
5 event.waitUntil(async function() {
6 await doSomeBookkeepingOrCaching();
7 });
8
9 return response;
10 }());
11});
複制
但是,有些人發現他們在
waitUntil
中運作的某些 JavaScript 延遲了
return response
,并用了
setTimeout
hack 來解決。
為了避免這種 hack 操作,我們同意使用
event.handled
,這是一個 promise,一旦 fetch 事件提供了響應或将其推遲給浏覽器後便會解決。
1addEventListener('fetch', event => {
2 event.respondWith(async function() {
3 const response = await getResponseSomehow();
4
5 event.waitUntil(async function() {
6 // And here's the new bit:
7 await event.handled;
8 await doSomeBookkeepingOrCaching();
9 });
10
11 return response;
12 }());
13});
複制
- GitHub issue (https://github.com/w3c/ServiceWorker/issues/1397).
背景同步(background sync)和背景擷取(background fetch)的隐私問題
Firefox 有背景同步 (https://developers.google.com/web/updates/2015/12/background-sync)的實作,但由于隐私問題而被阻止,這是由 Apple 員工共享的。
當使用者處于“線上”狀态時,背景同步會為你提供 service worker 事件,該事件可能會立即消失,也可能會在使用者離開站點後的某個時間出現。由于使用者已經作為頂級頁面通路了該網站(例如原始位置在URL欄中,而不是 iframe),是以 Chrome 很高興在以後允許一個小的,保守的執行視窗。Facebook 已經嘗試過這種方法來發送分析資料并確定聊天消息的傳遞,而且發現了它的性能比
sendBeacon
之類的方法更好。
Mozilla 和 Apple 員工對背景擷取(https://developers.google.com/web/updates/2018/12/background-fetch)模型更加滿意,該模型在擷取期間會持續顯示通知,并允許使用者取消。
Google搜尋已使用背景同步來線上擷取内容,但是他們可以用背景擷取來達到類似的目的。
這次讨論并沒有真正得出結論,但我感覺蘋果公司可能實作了背景擷取而不是背景同步。Mozilla 也可能會做同樣的事情,或者使背景同步變得更加使用者可見。
内容索引
Rayan Kanso 提出了内容索引提案 (https://github.com/rayankans/content-index/blob/master/README.md),它允許網站可以聲明能夠脫機使用的内容,是以浏覽器或 OS 可以在其他位置(例如 Chrome 中的新标簽頁)顯示此資訊。
有人擔心,無論這些東西出現在什麼UI上,網站都可以使用它來發送垃圾郵件。但是,浏覽器可以自由地忽略或驗證所告知的任何内容。
這是個非常新的提案,它已作送出給小組。
啟動事件
Raymes Khoury 向我們提供了有關啟動事件提案 (https://github.com/WICG/sw-launch/blob/master/explainer.md)的最新資訊。這是 PWA 控制多個視窗的一種方式。例如,當使用者單擊指向你網站的連結,但是沒有明确建議網站應如何打開(例如“在新視窗中打開”)時,如果開發人員可以決定是将焦點集中在網站使用的現有視窗上還是打開新視窗,那将是很好的選擇。這反映了當今原生應用的工作方式。
同樣,這項工作正在進行中。
聲明式路由
我向開發人員提供了有關聲明式路由提案 (https://jakearchibald.com/2019/service-worker-declarative-router/)的回報。盡管對浏覽器比較重要,但正常優化更加重要。很公平!這是一個規模很大的 API,需要做大量的工作。在确定我們确實需要它之前,最好先推遲一下。
service worker 的 Top-level await
Top-level await (https://github.com/tc39/proposal-top-level-await) 現在是 JavaScript 中的東西!但是 service worker 的啟動速度非常重要,是以在 service worker 中使用 Top-level await 可能會成為反模式。
我們有3個選擇:
選擇1:允許 top level
await
。service workers 初始化将在完全執行的主腳本上被阻止,包括
await
的事情。因為它可能是反模式,是以我們建議不要使用它,并在 devtools 中顯示警告。
你可以在 service worker 中執行以下操作:
1const start = Date.now();
2while (Date.now() - start < 5000);
複制
…并在初始 5 秒鐘阻止執行,是以
await
有什麼不同嗎?嗯,也許吧,因為異步内容可能有不可預測的性能問題(例如網絡),是以問題在開發過程中可能并不明顯。
選擇2:禁止。service workers 将在頂層使用
await
,是以它将無法被安裝,并且将在控制台中出現錯誤。
選擇3:允許頂層的
await
,但是一旦完成初始執行 + 微任務,則認為 service worker 已經準備就緒。這意味着
await
将繼續運作,但是可以在腳本“完成”之前調用事件。根據目前定義,不允許在執行 + 微任務之後添加事件。
我們認為選擇 3 太複雜,選擇 1 并沒有真正解決問題,是以選擇 2 是合适的。在 service worker 中:
1// If ./bar or any of its static imports use a top-level await,
2// this will be treated as an error
3// and stops the service worker from installing.
4import foo from './bar';
5
6// This top-level await causes an error
7// and stops the service worker from installing.
8await foo();
9
10// This is fine.
11// Also, dynamically imported modules and their
12// static imports may use top-level await,
13// since they aren't blocking service worker start-up.
14const modulePromise = import('./utils');
複制
- GitHub issue.(https://github.com/w3c/ServiceWorker/issues/1407)
擷取 opt-in / opt-out
Facebook 員工注意到 service workers 對直接發送到網絡的請求進行了性能回歸,那講得通。如果一個請求通過了 service worker,而結果是要做浏覽器無論如何要做的事情,那麼 service worker 就是開銷。
Facebook 一直在尋求一種方法,針對特定的 URL 說“這不需要通過 service workers 進行”。
Kinuko Yasuda 提出了提案 (https://github.com/w3c/ServiceWorker/issues/1454),我們讨論了一下,并決定了一個有點像這樣的設計:
1addEventListener('fetch', event => {
2 event.setSubresourceRoutes({
3 includes: paths,
4 excludes: otherPaths,
5 });
6
7 event.respondWith(…);
8});
複制
如果響應是針對用戶端(頁面或 worker)的,則它将僅詢問 service worker 以擷取子資源請求,這些子資源請求的字首與
includes
清單中的路徑比對(預設為所有路徑),而不會查詢 service worker,要求在
excludes
清單中使用字首比對路徑的請求。
最初我不确定這個建議,因為路由資訊是與頁面而不是 service workers 一起工作,而我通常更喜歡由 service workers 負責。但是其他擷取行為已經存在于頁面中,例如 CSP,是以我認為這沒什麼大不了的。
這個 API 并不是很優雅,是以我們希望能搞清楚,但是 Facebook 提供了能夠在 Chromium 中工作的實作,我們很高興它可以進入 Origin Trials (https://github.com/GoogleChrome/OriginTrials),這樣他們就可以檢視它是否解決了實際問題。如果看起來有好處,我們可以考慮調整 API。
就這樣!
還有一些因為時間問題沒有讨論的其他事情,是以我們可能會在 2020 年中期舉行另一場面對面的會議。同時,如果你對上述内容有什麼回報,請在在 GitHub 上告訴我。
原文:https://jakearchibald.com/2019/service-workers-tpac/