W3C TPAC 大會上的 Service workers 內容總結

做者:Jake Archibald

翻譯:瘋狂的技術宅javascript

原文:https://jakearchibald.com/201...html

未經容許嚴禁轉載前端

上個月,咱們在福岡舉行的 W3C TPAC 會議上召開了 service worker 會議。這是幾年來咱們第一次專一於潛在的新功能和行爲。現總結以下:java

復甦(Resurrection)最終被殺死

reg.unregister();

若是你取消 service worker 註冊,則會將其從註冊列表中刪除,但它仍會繼續控制現有頁面。這意味着它不會中斷任何正在進行的提取等操做。不過一旦全部這些頁面都消失了,就會被垃圾回收。git

可是在規範中有一個地方講到:若是一個名爲 serviceWorker.register() 的頁面具備相同的做用域,則被註銷的 service worker 註冊將會「復甦」。我不知道爲何要這麼作。不管如何,這都是一個愚蠢的主意,所以咱們把它刪除了程序員

// Old behaviour:
const reg1 = await navigator.serviceWorker.getRegistration();
await reg1.unregister();
const reg2 = await navigator.serviceWorker.register('/sw.js', {
  scope: reg1.scope,
});
console.log(reg1 === reg2); // true!

好吧,若是 reg1 不控制任何頁面,那多是錯誤的。 是的,這使人困惑。github

// New behaviour:
const reg1 = await navigator.serviceWorker.getRegistration();
await reg1.unregister();
const reg2 = await navigator.serviceWorker.register('/sw.js', {
  scope: reg1.scope,
});
console.log(reg1 === reg2); // Always false

如今,保證 reg2 是一個新的註冊。復甦已被殺死。web

咱們在 2018 年就此達成了共識,並已在 Chrome 中實現,同時在 Firefox 和 Safari 中也已經實現。面試

self.serviceWorker

在 service worker 中,很難得到對本身的 ServiceWorker 實例的引用。用 self.registration 能夠訪問你的註冊,可是究竟哪一個 service worker 表明你當前正在執行的服務呢? self.registration.active?也許是吧,也或許是 self.registration.waitingself.registration.installing,或者都不是。正則表達式

做爲代替:

console.log(self.serviceWorker);

上面的內容將爲你提供引用,不管其處於什麼狀態。

這項小功能已在全部瀏覽器中達成共識,在 Chrome 中正在積極開發。

頁面生命週期和 service workers

我是 page lifecycle API 的忠實擁護者,由於它標準化了多年來瀏覽器已經完成的各類行爲,特別是在手機上,例如,撤下頁面以節省內存和電池。

此外,會話歷史記錄能夠包含 DOM 文檔,這一般稱爲「後-轉發頁面緩存」或「 bfcache」。大多數瀏覽器中已經存在了許多年,這是 Chrome 的最新版本

這意味着頁面能夠是:

  • 凍結 - 該頁面能夠經過可見選項卡(做爲頂層頁面或其中的 iframe)訪問,該選項卡當前未選中。事件循環已暫停,所以該頁面未使用 CPU。該頁面已徹底存儲在內存中,而且能夠被凍結而不會丟失任何狀態。若是用戶將焦點放在此選項卡上,則該頁面將被解凍。
  • Bfcached - 與 凍結相似,可是沒法經過標籤訪問此頁面。它做爲歷史項存在於瀏覽上下文中。若是存在該項目的會話導航(例如使用後退/前進),則該頁面將被凍結。
  • 廢棄 - 能夠經過當前未選擇的可見標籤訪問該頁面。可是,選項卡實際上只是一個佔位符。該頁面已徹底卸載,再也不使用內存。若是用戶將焦點放在此選項卡上,則將從新加載頁面。

咱們須要弄清楚這些狀態怎樣適合特定的 service workers 行爲:

  • 一個新的 service worker 將會一直等待,直到當前活動 service worker 控制的全部頁面都消失了(能夠用skipWaiting()跳過)。
  • clients.matchAll() 將返回表明頁面的對象。

咱們決定:

  • 默認狀況下,凍結的頁面將由 clients.matchAll() 返回。 Chrome 但願向客戶端對象添加 isFrozen 屬性,可是 Apple 的同行反對。對凍結的客戶端的 client.postMessage() 調用將被緩衝,就像 BroadcastChannel 同樣。
  • Bfcached 和丟棄的頁面不會顯示在 clients.matchAll() 中。未來咱們可能會提供一種選擇加入的方式來獲取被廢棄的客戶端,以便他們能夠得到焦點(例如,響應通知點擊)。
  • 凍結的頁面將有助於防止等待的 worker 被激活。
  • Bfcached 和廢棄的頁面不會阻止等待中的工做程序被激活。若是 bfcached 頁面的控制器變得多餘(由於已激活了新的 service worker),則該 bfcached 頁面將被刪除。該項目保留在會話歷史記錄中,但若是導航到該項目,則必須徹底從新加載。

我甚至對全部的狀況進行了測試:

img

如今咱們只須要指定它。

將狀態附加到客戶端

當咱們討論頁面生命週期的內容時,Facebook 的同事提到了他們如何用 postMessage 向客戶詢問其狀態,例如「用戶當前是否在鍵入消息?」。咱們還注意到,咱們已經討論過向客戶端添加更多狀態(大小、密度、獨立模式、全屏等),可是很難劃清界線。

相反,咱們討論了容許開發人員將可克隆的數據附加到客戶端,這些數據將顯示在 service worker 的客戶端對象上。

// From a page (or other client):
await clients.setClientData({ foo: 'bar' });
// In a service worker:
const allClients = await clients.matchAll();
console.log(allClients[0].data); // { foo: 'bar' } or undefined.

如今還處於早期,但感受是這樣能夠避免在 postMessage 上來回移動。

當即註銷 worker

如前所述,若是你註銷 service workers 註冊,則會從註冊列表中將其刪除,可是它將慧繼續控制現有頁面。這意味着它不會中斷正在進行的提取等操做。可是在某些狀況下,不管中斷什麼事情,你都但願 service workers 當即離開。

這裏的一個客戶端是 Clear-Site-Data。如上所述,它正在註銷 service workers,可是 Clear-Site-Data 是「當即擺脫一切」開關,所以當前行爲不太正確。

常規註銷將保持不變,可是我將指定一種方法來當即註銷 service worker,這可能會終止正在運行的腳本並停止正在進行的提取。 Clear-Site-Data 將使用此方法,但咱們也能夠將其公開爲 API:

reg.unregister({ immediate: true });

來自 LinkedIn 的 Asa Kusuma 已編寫了 Clear-Site-Data 測試。我只須要進行規範工做就能夠了,不幸的是提及來容易作起來難。

URL 模式匹配

這是一個很大的問題。咱們在整個平臺上都使用 URL 匹配,尤爲是在 service worker 和 Content-Security-Policy 中。可是匹配很是簡單——徹底匹配或前綴匹配。開發人員傾向於使用 path-to-regexp 之類的東西。 Ben Kelly 提議咱們將相似的東西帶到平臺上。

它須要比正則表達式路徑更具限制性,由於咱們但願可以在共享進程(例如:瀏覽器的網絡進程)中處理這些問題。 RegExp 確實很複雜,而且能夠進行各類拒絕服務攻擊。瀏覽器供應商對開發人員有意鎖定本身的網站感到滿意,但咱們並不想鎖定整個瀏覽器。

這裏是 Ben 的提案。這很是雄心勃勃,可是若是咱們能夠跨平臺使用更富有表現力的 URL 那就太好了。這樣的例子很是引人注目:

// Service worker controls `/foo` and `/foo/*`,
// but does not control `/foobar`.
navigator.serviceWorker.register(scriptURL, {
  scope: new URLPattern({
    baseUrl: self.location,
    path: '/foo/?*',
  }),
});

把流做爲請求體

多年來,你能夠流式傳輸響應:

const response = await fetch('/whatever');
const reader = response.body.getReader();

while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  console.log(value); // Uint8Array of bytes
}

規範還說,你能夠將流用做請求的主體,但沒有瀏覽器實現。可是,Chrome 已決定再次使用它,而 Firefox 和 Safari 表示也會這樣作。

let intervalId;
const stream = new ReadableStream({
  start(controller) {
    intervalId = setInterval(() => {
      controller.enqueue('Hello!');
    }, 1000);
  },
  cancel() {
    clearInterval(intervalId);
  },
}).pipeThrough(new TextEncoderStream());

fetch('/whatever', {
  method: 'POST',
  body: stream,
  headers: { 'Content-Type': 'text/plain; charset=UTF-8' },
});

上面的代碼將「hello」做爲單個 HTTP 請求的一部分,每秒鐘一次發送到服務器。這個例子是愚蠢的,可是它展現了一項新功能——在你得到整個請求體內容以前將數據發送到服務器。當前,你只能分塊或使用 websocket 來執行此操做。

一個實際的例子是涉及上傳流式傳輸的內容。例如你能夠在編碼或錄製的時候上傳視頻。

HTTP 是雙向的。該模型不是先請求後響應——你能夠在仍然發送請求正文的同時開始接收響應。可是,在 TPAC 大會中,瀏覽器開發人員注意到,鑑於當前的網絡棧,在獲取過程當中公開這個內容確實很複雜,所以請求流的最初實如今請求完成以前不會產生響應。這還算不錯——若是你想模擬雙向通訊,則可使用一次 fetch 進行上傳,而用另外一次進行下載。

響應後執行

這已成爲 service workers 中很是廣泛的模式:

addEventListener('fetch', event => {
  event.respondWith(async function() {
    const response = await getResponseSomehow();

    event.waitUntil(async function() {
      await doSomeBookkeepingOrCaching();
    });

    return response;
  }());
});

可是,有些人發現他們在 waitUntil 中運行的某些 JavaScript 延遲了 return response,並用了 setTimeout hack 來解決。

爲了不這種 hack 操做,咱們贊成使用 event.handled,這是一個 promise,一旦 fetch 事件提供了響應或將其推遲給瀏覽器後便會解決。

addEventListener('fetch', event => {
  event.respondWith(async function() {
    const response = await getResponseSomehow();

    event.waitUntil(async function() {
      // And here's the new bit:
      await event.handled;
      await doSomeBookkeepingOrCaching();
    });

    return response;
  }());
});

後臺同步(background sync)和後臺獲取(background fetch)的隱私問題

Firefox 有後臺同步的實現,但因爲隱私問題而被阻止,這是由 Apple 員工共享的。

當用戶處於「在線」狀態時,後臺同步會爲你提供 service worker 事件,該事件可能會當即消失,也可能會在用戶離開站點後的某個時間出現。因爲用戶已經做爲頂級頁面訪問了該網站(例如原始位置在URL欄中,而不是 iframe),所以 Chrome 很高興在之後容許一個小的,保守的執行窗口。 Facebook 已經嘗試過這種方法來發送分析數據並確保聊天消息的傳遞,並且發現了它的性能比 sendBeacon 之類的方法更好。

Mozilla 和 Apple 員工對後臺獲取模型更加滿意,該模型在獲取期間會持續顯示通知,並容許用戶取消。

Google搜索已使用後臺同步來在線獲取內容,可是他們能夠用後臺獲取來達到相似的目的。

此次討論並無真正得出結論,但我感受蘋果公司可能實現了後臺獲取而不是後臺同步。 Mozilla 也可能會作一樣的事情,或者使後臺同步變得更加用戶可見。

內容索引

Rayan Kanso 提出了內容索引提案,它容許網站能夠聲明可以脫機使用的內容,所以瀏覽器或 OS 能夠在其餘位置(例如 Chrome 中的新標籤頁)顯示此信息。

有人擔憂,不管這些東西出如今什麼UI上,網站均可以使用它來發送垃圾郵件。可是,瀏覽器能夠自由地忽略或驗證所告知的任何內容。

這是個很是新的提案,它已做提交給小組。

啓動事件

Raymes Khoury 向咱們提供了有關啓動事件提案的最新信息。這是 PWA 控制多個窗口的一種方式。例如,當用戶單擊指向你網站的連接,可是沒有明確建議網站應如何打開(例如「在新窗口中打開」)時,若是開發人員能夠決定是將焦點集中在網站使用的現有窗口上仍是打開新窗口,那將是很好的選擇。這反映了當今原生應用的工做方式。
一樣,這項工做正在進行中。

聲明式路由

我向開發人員提供了有關聲明式路由建議的反饋。儘管對瀏覽器比較重要,但常規優化更加劇要。很公平!這是一個規模很大的 API,須要作大量的工做。在肯定咱們確實須要它以前,最好先推遲一下。

service worker 的 Top-level await

Top-level await 如今是 JavaScript 中的東西!可是 service worker 的啓動速度很是重要,因此在 service worker 中使用 Top-level await 可能會成爲反模式。

咱們有3個選擇:

選擇1:容許 top level await。service workers 初始化將在徹底執行的主腳本上被阻止,包括 await 的事情。由於它多是反模式,因此咱們建議不要使用它,並在 devtools 中顯示警告。

你能夠在 service worker 中執行如下操做:

const start = Date.now();
while (Date.now() - start < 5000);

…並在初始 5 秒鐘阻止執行,因此 await 有什麼不一樣嗎?嗯,也許吧,由於異步內容可能有不可預測的性能問題(例如網絡),因此問題在開發過程當中可能並不明顯。

選擇2:禁止。service workers 將在頂層使用 await,所以它將沒法被安裝,而且將在控制檯中出現錯誤。

選擇3:容許頂層的 await,可是一旦完成初始執行 + 微任務,則認爲 service worker 已經準備就緒。這意味着 await 將繼續運行,可是能夠在腳本「完成」以前調用事件。根據當前定義,不容許在執行 + 微任務以後添加事件。

咱們認爲選擇 3 太複雜,選擇 1 並無真正解決問題,所以選擇 2 是合適的。在 service worker 中:

// If ./bar or any of its static imports use a top-level await,
// this will be treated as an error
// and stops the service worker from installing.
import foo from './bar';

// This top-level await causes an error
// and stops the service worker from installing.
await foo();

// This is fine.
// Also, dynamically imported modules and their
// static imports may use top-level await,
// since they aren't blocking service worker start-up.
const modulePromise = import('./utils');

獲取 opt-in / opt-out

Facebook 員工注意到 service workers 對直接發送到網絡的請求進行了性能迴歸,那講得通。若是一個請求經過了 service worker,而結果是要作瀏覽器不管如何要作的事情,那麼 service worker 就是開銷。

Facebook 一直在尋求一種方法,針對特定的 URL 說「這不須要經過 service workers 進行」。

Kinuko Yasuda 提出了提案,咱們討論了一下,並決定了一個有點像這樣的設計:

addEventListener('fetch', event => {
  event.setSubresourceRoutes({
    includes: paths,
    excludes: otherPaths,
  });

  event.respondWith(…);
});

若是響應是針對客戶端(頁面或 worker)的,則它將僅詢問 service worker 以獲取子資源請求,這些子資源請求的前綴與 includes 列表中的路徑匹配(默認爲全部路徑),而不會查詢 service worker,要求在 excludes 列表中使用前綴匹配路徑的請求。

最初我不肯定這個建議,由於路由信息是與頁面而不是 service workers 一塊兒工做,而我一般更喜歡由 service workers 負責。可是其餘獲取行爲已經存在於頁面中,例如 CSP,因此我認爲這沒什麼大不了的。

這個 API 並非很優雅,因此咱們但願能搞清楚,可是 Facebook 提供了可以在 Chromium 中工做的實現,咱們很高興它能夠進入來源試用,這樣他們就能夠查看它是否解決了實際問題。若是看起來有好處,咱們能夠考慮調整 API。

就這樣!

還有一些由於時間問題沒有討論的其餘事情,因此咱們可能會在 2020 年中期舉行另外一場面對面的會議。同時,若是你對上述內容有什麼反饋,請在在 GitHub 上告訴我。


本文首發微信公衆號:前端先鋒

歡迎掃描二維碼關注公衆號,天天都給你推送新鮮的前端技術文章

歡迎掃描二維碼關注公衆號,天天都給你推送新鮮的前端技術文章

歡迎繼續閱讀本專欄其它高贊文章:


相關文章
相關標籤/搜索