跨來源 Service Worker - 試用外來擷取功能

傑夫波斯尼克
Jeff Posnick

背景

Service Worker 可讓網頁開發人員回應網頁應用程式發出的網路要求,讓他們即使在離線狀態下也能繼續工作、解決謊言,並實作複雜的快取互動,例如 sold-while-revalidate。不過,服務工作處理程序過去一直會與特定來源相連結,因此身為網頁應用程式的擁有者,您有責任編寫及部署 Service Worker,以攔截網頁應用程式發出的所有網路要求。在這個模型中,每個 Service Worker 都會負責處理跨來源的要求,例如針對第三方 API 或網路字型發出的要求。

如果 API 的第三方供應商、網頁字型或其他常用服務允許自行部署 Service Worker,有機會處理「其他」來源發出的要求,該怎麼辦?供應商可以實作自己的自訂網路邏輯,並利用單一權威單一的快取執行個體儲存回應。現在,有了外匯擷取,該類型的第三方 Service Worker 部署作業才是真的。

對於可透過瀏覽器透過 HTTPS 要求存取的任何服務供應商,部署可實作外文擷取的 Service Worker 最合理。請思考您可以在哪些情境下提供獨立版本的服務,讓瀏覽器善用通用資源快取。可受惠於這類內容的服務包括 (但不限於):

  • 具有 RESTful 介面的 API 供應商
  • 網站字型提供者
  • 分析服務供應商
  • 圖片代管服務供應商
  • 一般內容傳遞網路

假設你是分析服務供應商部署外國擷取 Service Worker,就能確保服務的所有要求 (在使用者離線時失敗) 都會排入佇列,並在連線恢復後再次發出。雖然服務的用戶端可以透過第一方服務工作站實作類似行為,但要求每個用戶端和每個用戶端為服務編寫專屬邏輯時,都無法像仰賴部署的共用外來擷取 Service Worker 來擴充。

必備條件

來源試用權杖

外國擷取仍視為實驗性質。為避免這個設計在瀏覽器供應商全面指定和同意前就早期進行製,在 Chrome 54 中實作為「來源試用」。只要外國擷取仍處於實驗階段,如要在您代管的服務中使用這項新功能,就必須要求將範圍限定為服務專屬來源的權杖。針對要透過外文擷取處理的資源,以及服務工作人員 JavaScript 資源的回應,憑證必須在所有跨來源要求中,以 HTTP 回應標頭的形式加入:

Origin-Trial: token_obtained_from_signup

試用期將於 2017 年 3 月結束。屆時,我們預計應有所瞭解,確保這項功能穩定運作,然後 (有機會) 預設啟用該功能。如果預設未啟用外國擷取功能,與現有來源試用權杖相關的功能就會停止運作。

如要在註冊官方「來源試用權杖」之前,先進行外文擷取實驗,可以前往 chrome://flags/#enable-experimental-web-platform-features 啟用「實驗性 Web Platform 功能」標記,藉此略過電腦版 Chrome 的相關規定。請注意,您必須在要用於本機實驗的每個 Chrome 執行個體中進行這項操作,而提供來源試用權杖給所有 Chrome 使用者都可以使用這項功能。

HTTPS

與所有 Service Worker 部署作業一樣,用於提供資源和服務工作處理程序指令碼的網路伺服器必須透過 HTTPS 存取。此外,外國擷取攔截功能僅適用於源自安全來源所代管網頁的要求,因此你服務的用戶端必須使用 HTTPS,才能善用外來擷取實作的優勢。

使用外國擷取

完成事前準備後,讓我們深入瞭解讓外國擷取服務工作處理程序都能所需的技術細節。

註冊 Service Worker

您可能遇到的第一個挑戰是如何註冊您的 Service Worker。如果您曾與服務工作人員合作,可能需要學過下列幾點:

// You can't do this!
if ('serviceWorker' in navigator) {
    navigator.serviceWorker.register('service-worker.js');
}

這個用於註冊第一方服務工作處理程序的 JavaScript 程式碼以網頁應用程式來說非常重要,因為當使用者瀏覽至您控管的網址就會觸發這類程式碼。但是註冊第三方服務工作處理程序並不可行,當互動瀏覽器與您的伺服器一起要求特定的子資源時,而不是完整導覽。如果瀏覽器要求 (像是由您維護的 CDN 伺服器提供的圖片),您就無法在回應前附加 JavaScript 程式碼片段,並預期程式碼會順利執行。必須在一般 JavaScript 執行內容之外,使用不同的 Service Worker 註冊方法。

解決方法會以 HTTP 標頭的形式提供,您的伺服器可以在任何回應中加入該標頭:

Link: </service-worker.js>; rel="serviceworker"; scope="/"

現在,讓我們將範例標頭拆解成各個元件,並以 ; 字元分隔。

  • </service-worker.js> 是必要元素,用於指定 Service Worker 檔案的路徑 (將 /service-worker.js 換成適當的指令碼路徑)。這會直接對應 scriptURL 字串,否則會做為第一個參數傳送至 navigator.serviceWorker.register()。這個值必須置於 <> 字元中 (根據 Link 標頭規格的規定)。如果提供的是相對而非絕對網址,系統會將這個值解讀為「相對於回應位置」
  • rel="serviceworker" 也是必要項目,應加入,不需要自訂。
  • scope=/ 是選用的範圍宣告,相當於可以傳入為 navigator.serviceWorker.register()options.scope 字串。許多用途都採用預設範圍,因此如果您不知道有此需求,可以選擇略過。與允許的範圍上限相同,並透過 Service-Worker-Allowed 標頭放寬這些限制的功能,同樣適用於 Link 標頭註冊。

就像註冊「傳統」的 Service Worker 註冊作業一樣,使用 Link 標頭會安裝 Service Worker,該 Worker 會針對註冊的範圍所發出的「下一個」要求使用。包含特殊標頭的回應主體會原封不動使用,立即顯示於網頁,無需等待外國服務工作人員完成安裝。

請注意,外國擷取目前屬於「來源試用」,因此除了連結回應標頭,您還需要加入有效的 Origin-Trial 標頭。為了註冊外國擷取服務工作站,應新增的回應標頭下限為

Link: </service-worker.js>; rel="serviceworker"
Origin-Trial: token_obtained_from_signup

偵錯註冊

在開發期間,您可能需要確認外國擷取 Service Worker 已正確安裝並處理要求。您可以檢查 Chrome 的開發人員工具,確認一切是否正常運作。

是否傳送正確的回應標頭?

如要註冊外國擷取服務工作處理程序,您必須按照本文先前所述,針對網域所代管資源的回應設定 Link 標頭。在來源試用期間,假設您並未設定 chrome://flags/#enable-experimental-web-platform-features,則您還需要設定 Origin-Trial 回應標頭。您可以在開發人員工具的「網路面板」中查看項目,確認網路伺服器設定了這些標頭:

顯示在「Network」面板中的標頭。

外國擷取服務工作處理程序正確嗎?

您也可以查看開發人員工具的應用程式面板,查看服務工作處理程序的完整清單,以確認基礎服務工作處理程序 (包括其範圍)。請務必選取「全部顯示」選項,因為系統預設只會顯示目前來源的 Service Worker。

「應用程式」面板中的外來擷取 Service Worker。

安裝事件處理常式

您已註冊第三方服務 Worker,就能回應 installactivate 事件,就像任何其他 Service Worker 一樣。此功能可善用這些事件,例如在 install 事件期間使用必要資源填入快取,或是修剪 activate 事件中的過時快取。

除了一般的 install 事件快取活動以外,第三方服務 Worker 的 install 事件處理常式中,還有一個必要步驟。您的程式碼必須呼叫 registerForeignFetch(),如以下範例所示:

self.addEventListener('install', event => {
    event.registerForeignFetch({
    scopes: [self.registration.scope], // or some sub-scope
    origins: ['*'] // or ['https://example.com']
    });
});

有兩種設定選項,兩種必須擇一使用:

  • scopes 採用一或多個字串的陣列,每個字串都代表會觸發 foreignfetch 事件的要求範圍。不過請稍待片刻,您可能會想:我已在註冊 Service Worker 時定義了範圍!這確實是如此,而整體範圍也仍然相關,您在這裡指定的每個範圍都必須等於或等於服務工作處理程序整體範圍的子範圍。這裡的額外範圍限制可讓您部署適用於所有用途的 Service Worker,能處理第一方 fetch 事件 (針對從自有網站發出的要求) 和第三方 foreignfetch 事件 (針對從其他網域發出的要求),並明確指出只有較大範圍的子集應觸發 foreignfetch。實際上,如果您部署的是專門處理第三方 foreignfetch 事件的 Service Worker,可以只使用一個等於服務工作站整體範圍的明確範圍。這就是上述範例使用 self.registration.scope 值的行為。
  • origins 也會採用一或多個字串的陣列,並且能讓您限制 foreignfetch 處理常式僅回應來自特定網域的要求。舉例來說,如果您明確允許「https://example.com」,那麼由 https://example.com/path/to/page.html 代管的網頁針對來自外國擷取範圍的資源發出的要求會觸發外文擷取處理常式,但從 https://random-domain.com/path/to/page.html 提出的要求不會觸發您的處理常式。除非您有特定理由只針對部分遠端來源觸發外文擷取邏輯,否則只要將 '*' 指定為陣列中的唯一值,系統就會允許所有來源。

外擷取事件處理常式

現在您已安裝第三方 Service Worker ,且已透過 registerForeignFetch() 設定該工作程式,之後將有機會攔截傳送至外地擷取範圍的跨來源子資源要求,並傳送至您的伺服器。

在傳統的第一方 Service Worker 中,每個要求都會觸發 fetch 事件,表示服務工作站有機會回應。我們的第三方服務工作處理程序有機會處理名為 foreignfetch 的一些事件。概念上,這兩個事件非常相似,可讓您檢查傳入的要求,並選擇是否要透過 respondWith() 回應:

self.addEventListener('foreignfetch', event => {
    // Assume that requestLogic() is a custom function that takes
    // a Request and returns a Promise which resolves with a Response.
    event.respondWith(
    requestLogic(event.request).then(response => {
        return {
        response: response,
        // Omit to origin to return an opaque response.
        // With this set, the client will receive a CORS response.
        origin: event.origin,
        // Omit headers unless you need additional header filtering.
        // With this set, only Content-Type will be exposed.
        headers: ['Content-Type']
        };
    })
    );
});

儘管概念相似,在 ForeignFetchEvent 上呼叫 respondWith() 時,實際上有一些差異。比起提供以 Response 解析的 Response (或 Promise 來解析 Response) 給 respondWith(),您必須傳遞 Promise,以將含有特定屬性的物件解析至 ForeignFetchEventrespondWith()FetchEvent

  • response 是必要項目,且必須設為 Response 物件,以便傳回給提出要求的用戶端。如果您提供有效 Response 以外的任何其他資訊,就會發生網路錯誤,導致用戶端要求終止。與在 fetch 事件處理常式中呼叫 respondWith() 時不同,您「必須」提供 Response,而非使用 Response 解析的 Promise!您可以透過承諾鏈結建構回應,並將該鏈結做為參數傳遞至 foreignfetchrespondWith(),但鏈結必須使用含有設為 Response 物件的 response 屬性的物件進行解析。您可以在上方的程式碼範例中看到相關示範。
  • origin 是選用項目,可用來判斷傳回的回應是否為「不透明」。如果保留這個值,回應就不會透明,用戶端對於回應內文和標頭的存取權也會受到限制。如果要求是透過 mode: 'cors' 送出,那麼傳回不透明回應就會視為錯誤。不過,如果您指定的字串值與遠端用戶端的來源相同 (可透過 event.origin 取得),就等於明確選擇為用戶端提供啟用 CORS 的回應。
  • headers 也是選用項目,只有在您同時指定 origin 及傳回 CORS 回應時才實用。根據預設,只有 CORS-safelisted 回應標頭清單中的標頭會包含在回應中。如果您需要進一步篩選傳回的內容,標頭會列出一或多個標頭名稱,並將該清單做為要在回應中公開的標頭的許可清單。您可以選用 CORS,同時防止潛在敏感的回應標頭直接曝露給遠端用戶端。

請注意,foreignfetch 處理常式執行時,可存取服務 Worker 來源的所有憑證和環境權威機構。如果開發人員部署外文已啟用擷取功能的 Service Worker,必須確保不會洩露任何因憑證而無法取得的權限回應資料。要求啟用 CORS 回應是限制意外曝光的一個步驟,但開發人員可以在 foreignfetch 處理常式中明確提出 fetch() 要求,不要透過下列元件使用隱含憑證

self.addEventListener('foreignfetch', event => {
    // The new Request will have credentials omitted by default.
    const noCredentialsRequest = new Request(event.request.url);
    event.respondWith(
    // Replace with your own request logic as appropriate.
    fetch(noCredentialsRequest)
        .catch(() => caches.match(noCredentialsRequest))
        .then(response => ({response}))
    );
});

客戶注意事項

您的外文擷取服務工作人員還需考量其他事項,以致處理您從服務用戶端發出的要求。

有專屬第一方 Service Worker 的用戶端

您有部分服務的用戶端可能已經有自己的第一方 Service Worker,且會處理來自其網頁應用程式的要求。這對第三方外國擷取 Service Worker 有何影響?

第一方服務工作站中的 fetch 處理常式可優先回應網頁應用程式發出的所有要求,即使有第三方服務工作站啟用 foreignfetch,且範圍涵蓋要求。不過,擁有第一方服務工作處理程序的用戶端仍可利用外國擷取 Service Worker!

在第一方 Service Worker 中,使用 fetch() 擷取跨來源資源會觸發適當的外傳擷取 Service Worker。也就是說,下列程式碼可以使用 foreignfetch 處理常式:

// Inside a client's first-party service-worker.js:
self.addEventListener('fetch', event => {
    // If event.request is under your foreign fetch service worker's
    // scope, this will trigger your foreignfetch handler.
    event.respondWith(fetch(event.request));
});

同樣地,如果有第一方擷取處理常式,但在處理跨來源資源的要求時沒有呼叫 event.respondWith(),則該要求會自動「中斷」至 foreignfetch 處理常式:

// Inside a client's first-party service-worker.js:
self.addEventListener('fetch', event => {
    if (event.request.mode === 'same-origin') {
    event.respondWith(localRequestLogic(event.request));
    }

    // Since event.respondWith() isn't called for cross-origin requests,
    // any foreignfetch handlers scoped to the request will get a chance
    // to provide a response.
});

如果第一方的 fetch 處理常式呼叫 event.respondWith(),但「並未」使用 fetch() 要求外部擷取範圍下的資源,則外部擷取服務工作處理人員將無法處理這項要求。

沒有專屬 Service Worker 的用戶端

當服務部署外國擷取 Service Worker 後,向第三方服務發出要求的所有用戶端都很實用,即使他們尚未使用自有的 Service Worker。只要所用的瀏覽器支援外文擷取 Service Worker,客戶不必採取特定行動即可選擇使用這項服務。這表示只要部署外文擷取 Service Worker,您的自訂要求邏輯和共用快取就能立即讓許多服務用戶端受益,無需採取進一步步驟。

總結:客戶希望獲得回應

為了瞭解以上資訊,我們可彙整用戶端用於尋找跨來源要求回應的來源階層。

  1. 第一方服務工作站的 fetch 處理常式 (如果有的話)
  2. 第三方服務工作站的 foreignfetch 處理常式 (如有,且僅適用於跨來源要求)
  3. 瀏覽器的 HTTP 快取 (如果有即時回應)
  4. 網路

瀏覽器會從頂端開始,並根據 Service Worker 的實作內容繼續向下移動清單,直到找到回應的來源為止。

瞭解詳情

掌握最新資訊

隨著我們回應開發人員的意見,Chrome 的外來擷取來源試用功能實作方式可能會隨時變動。我們會透過內嵌變更內容隨時更新本文內容,並在異動發生時特別留意以下具體變更。我們也會透過 @chromiumdev Twitter 帳戶分享重大異動資訊。