背景
服務工作者可讓網頁開發人員回應網路應用程式提出的網路要求,讓應用程式即使在離線時也能繼續運作,對抗假網路,並實作重新驗證時的過時狀態等複雜的快取互動。不過,Service Worker 一向與特定來源綁定。身為網頁應用程式的擁有者,您有責任編寫及部署 Service Worker,以便攔截網頁應用程式發出的所有網路要求。在該模型中,每個服務工作者都負責處理跨來源要求,例如第三方 API 或網頁字型。
如果 API、網頁字型或其他常用服務的第三方供應商有權部署自己的服務工作者,並有機會處理其他來源對其來源提出的要求,那麼情況會如何?供應商可以實作自訂的網路邏輯,並利用單一權威的快取例項來儲存回應。如今,有了外部擷取功能,這類第三方服務 worker 部署作業終於可以實現。
對於透過瀏覽器的 HTTPS 要求存取服務的任何供應商而言,部署實作外部擷取作業的服務工作站,都是明智的做法。只要想想,您可以提供不受網路限制的服務版本,讓瀏覽器充分利用通用資源快取,可能受益的服務包括但不限於:
- 提供 RESTful 介面的 API 供應器
- 網路字型供應商
- 數據分析供應商
- 圖片代管服務供應商
- 一般內容傳遞聯播網
舉例來說,假設您是數據分析供應商,部署外部擷取服務 worker 後,您就能確保在使用者離線時,所有對服務的失敗要求都會排入佇列,並在連線恢復後重播。雖然服務的用戶端可以透過第一方服務 worker 實作類似的行為,但要求每個用戶端都為服務編寫專屬邏輯,並非可擴充的做法,因為您部署的共用外部擷取服務 worker 可提供相同的功能。
必要條件
來源試用權杖
外部擷取功能仍屬於實驗功能。為了避免在瀏覽器供應商完全指定並同意這項設計前,就過早將其納入,我們已在 Chrome 54 中實作這項設計,做為原點測試。只要外部擷取功能仍處於實驗階段,您就必須要求權杖,並將其範圍限定為服務的特定來源,才能在您代管的服務中使用這項新功能。您應在所有跨來源要求中加入權杖做為 HTTP 回應標頭,這些要求是指您要透過外部擷取機制處理的資源,以及服務工作的 JavaScript 資源回應:
Origin-Trial: token_obtained_from_signup
試用期將於 2017 年 3 月結束。屆時,我們預計已找出所有必要的變更,以穩定這項功能,並且 (希望) 預設啟用這項功能。如果在該期限前未預設啟用外部擷取功能,與現有 Origin Trial 權杖相關聯的功能就會停止運作。
如要在註冊官方來源試用權杖前,先試驗外部擷取功能,您可以前往 chrome://flags/#enable-experimental-web-platform-features
並啟用「Experimental Web Platform features」標記,藉此在本機電腦上略過 Chrome 的相關規定。請注意,您必須在所有要用於本機實驗的 Chrome 例項中執行這項操作,但如果使用 Origin Trial 權杖,所有 Chrome 使用者都能使用這項功能。
HTTPS
如同所有服務工作者部署作業,您用於提供資源和服務工作者指令碼的網路伺服器,必須透過 HTTPS 存取。此外,只有來自安全來源的網頁所發出的請求,才適用外擷取攔截,因此您服務用戶端必須使用 HTTPS,才能使用您的外擷取實作。
使用外部擷取
先掌握先決條件後,接下來要說明讓外國擷取 Service Worker 開始運作所需的技術細節。
註冊 Service Worker
您可能會遇到的第一個挑戰,就是如何註冊服務工作者。如果您曾使用過服務工作者,可能會熟悉以下內容:
// You can't do this!
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('service-worker.js');
}
這段用於註冊第一方服務工作者的 JavaScript 程式碼,在使用者前往您控管的網址時觸發,用於網頁應用程式情境中。不過,如果瀏覽器與伺服器的唯一互動是要求特定子資源,而非完整導覽,則註冊第三方服務 worker 並非可行的方法。如果瀏覽器要求您維護的 CDN 伺服器提供圖片,您就無法在回應中加上該 JavaScript 程式碼片段,並期望該程式碼片段會執行。除了一般 JavaScript 執行環境外,必須使用其他的 Service Worker 註冊方法。
解決方案的形式為 HTTP 標頭,您的伺服器可在任何回應中加入此標頭:
Link: </service-worker.js>; rel="serviceworker"; scope="/"
讓我們將範例標頭細分為相關元件,每個元件都以 ;
字元分隔。
</service-worker.js>
是必要參數,用於指定服務工作者檔案的路徑 (將/service-worker.js
替換為指令碼的適當路徑)。這會直接對應至scriptURL
字串,否則會做為第一個參數傳遞至navigator.serviceWorker.register()
。這個值必須以<>
字元括住 (依照Link
標頭規格的規定),如果提供了相對 (而非絕對網址),系統會將這個值解讀為相對於回應位置。rel="serviceworker"
也是必要元素,且應納入其中,無須自訂。scope=/
是選用的範圍宣告,等同於您可以做為第二個參數傳入navigator.serviceWorker.register()
的options.scope
字串。在許多用途中,您可以使用預設範圍,因此除非您確定需要使用,否則可以放心略過這項設定。Link
標頭註冊作業適用於允許的最大範圍的相同限制,以及透過Service-Worker-Allowed
標頭放寬這些限制的功能。
就像使用「傳統」服務工作者註冊一樣,使用 Link
標頭會安裝服務工作者,並用於針對已註冊範圍發出的下一個要求。包含特殊標頭的回應內文會照原樣使用,並立即提供給網頁,無須等待外部服務工作者完成安裝作業。
請注意,外部擷取目前是以來源試用的形式實作,因此您必須在 Link 回應標頭旁邊加入有效的 Origin-Trial
標頭。如要註冊外部擷取服務 worker,您必須新增下列回應標頭:
Link: </service-worker.js>; rel="serviceworker"
Origin-Trial: token_obtained_from_signup
偵錯註冊
開發期間,您可能會想確認外部擷取服務 worker 是否已正確安裝,並處理要求。您可以透過 Chrome 的開發人員工具檢查幾項事項,確認一切運作正常。
系統是否傳送正確的回應標頭?
如要註冊外部擷取服務 worker,您必須在回應中設定網域上代管的資源的 Link 標頭,如本篇文章稍早所述。在來源試用期間,如果您沒有設定 chrome://flags/#enable-experimental-web-platform-features
,也必須設定 Origin-Trial
回應標頭。您可以查看開發人員工具的「Network」面板中的項目,確認網路伺服器是否已設定這些標頭:
外部擷取服務 worker 是否註冊正確?
您也可以前往開發人員工具的應用程式面板查看完整的服務工作站清單,確認基礎 Service Worker 的註冊及其範圍。請務必選取 [全部顯示] 選項,因為根據預設,您只會看到目前來源的 Service Worker。
安裝事件處理常式
註冊第三方服務 worker 後,該服務 worker 將有機會回應 install
和 activate
事件,就像其他服務 worker 一樣。舉例來說,它可以利用這些事件,在 install
事件期間將必要資源填入快取,或是在 activate
事件中修剪過期的快取。
除了一般 install
事件快取活動之外,第三方服務工作程的 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
事件的服務工作者,建議您使用單一明確範圍,該範圍與服務工作者的整體範圍相同。這就是上述範例使用self.registration.scope
值將達到下列效果。origins
也會接受一或多個字串的陣列,並可讓您限制foreignfetch
處理常式只回應特定網域的要求。舉例來說,如果您明確允許「https://example.com」,則從https://example.com/path/to/page.html
代管的網頁發出的要求,如果是針對從外部擷取範圍提供的資源,就會觸發外部擷取處理常式,但從https://random-domain.com/path/to/page.html
發出的要求則不會觸發處理常式。除非您有特定原因,只想針對部分外部來源觸發外部擷取邏輯,否則您可以將'*'
指定為陣列中唯一的值,這樣系統就會允許所有來源。
foreignfetch 事件處理常式
您已安裝第三方服務 worker,並透過 registerForeignFetch()
進行設定,因此服務 worker 將有機會攔截跨來源的子資源要求,這些要求屬於外部擷取範圍內的伺服器。
在傳統的第一方服務 worker 中,每個要求都會觸發 fetch
事件,服務 worker 有機會回應該事件。我們的第三方服務工作架構可處理稍有不同的事件,名為 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()
時,實際上還是有些差異。您需要傳遞 Promise
,並將具有特定屬性的物件解析為 ForeignFetchEvent
的 respondWith()
,而不是像 FetchEvent
那樣,只提供 Response
(或 Promise
,可與 Response
解析) 給 respondWith()
:
response
是必要參數,且必須設為Response
物件,以便傳回給提出要求的用戶端。如果您提供的不是有效的Response
,用戶端的要求就會因網路錯誤而終止。與在fetch
事件處理常式中呼叫respondWith()
不同,您必須在此提供Response
,而不是使用Response
解析的Promise
!您可以透過應許條款鏈結建構回應,並將該鏈結做為參數傳遞至foreignfetch
的respondWith()
,但鏈結必須透過物件解析,其中包含將response
屬性設為Response
物件的屬性。您可以在上述程式碼範例中看到相關示範。origin
為選用項目,可用來判斷傳回的回應是否為「不透明」。如果選擇這個方式,回應將不透明,而用戶端將只能存取有限的回應內文和標頭。如果要求是使用mode: 'cors'
發出,則傳回不透明回應會視為錯誤。不過,如果您指定與遠端用戶端來源相同的字串值 (可透過event.origin
取得),即表示您明確選擇向用戶端提供已啟用 CORS 的回應。headers
也是選用項目,只有在您同時指定origin
並傳回 CORS 回應時才有用。根據預設,回應中只會納入 CORS 安全清單回應標頭清單中的標頭。如果您需要進一步篩選傳回的內容,請使用標頭,其中包含一或多個標頭名稱清單,系統會將該清單用作允許清單,決定在回應中公開哪些標頭。這樣一來,您就能選擇啟用 CORS,同時避免將可能含有敏感資訊的回應標頭直接公開給遠端用戶端。
請注意,執行 foreignfetch
處理常式時,該處理常式可存取代管服務工作的來源的所有憑證和環境權限。開發人員在部署支援外部擷取的服務工作者時,有責任確保不會洩漏任何權限回應資料,否則這些憑證將無法使用。如要降低不慎暴露風險,開發人員可以要求啟用 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}))
);
});
客戶考量事項
還有一些其他考量因素會影響外部擷取服務 worker 處理服務用戶端提出的要求的方式。
擁有自己的第一方服務工作人員的客戶
您服務的部分用戶端可能已經有自己的第一方 Service Worker,用來處理他們網頁應用程式發出的要求。這對第三方、外國擷取 Service 工作人員有何影響?
第一方服務工作處理程序中的 fetch
處理常式會先回應網頁應用程式提出的所有要求,即使有已啟用 foreignfetch
的第三方 Service Worker,且該處理範圍涵蓋要求的範圍亦然。不過,具有第一方服務 worker 的用戶端仍可利用外部擷取服務 worker!
在第一方服務 worker 中,使用 fetch()
擷取跨來源資源,會觸發適當的外部擷取服務 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()
在外部擷取範圍下要求資源,則外部擷取服務 worker 就無法處理要求。
用戶端沒有專屬服務工作人員
當服務部署外擷取 Service Worker 時,所有向第三方服務提出要求的用戶端皆可受益,即使用戶端未使用自己的 Service Worker 也一樣。只要使用支援外部擷取服務 worker 的瀏覽器,用戶端就不需要採取任何特定動作,即可選擇使用外部擷取服務 worker。也就是說,只要部署外國擷取 Service Worker,自訂要求邏輯和共用快取就能立即為服務的許多用戶端帶來好處,不必採取進一步步驟。
總結:客戶尋求回應
考量上述資訊後,我們可以組合用戶端用來尋找跨來源要求回應的來源階層。
- 第一方服務工作程的
fetch
處理常式 (如有) - 第三方 Service Worker 的
foreignfetch
處理常式 (如有,且僅適用於跨來源要求) - 瀏覽器的 HTTP 快取 (如果有新的回應)
- 網路
瀏覽器會從頂端開始,並根據服務工作站的實作方式,繼續向下瀏覽清單,直到找到回應來源為止。
瞭解詳情
掌握最新資訊
我們會根據開發人員的意見回饋,調整 Chrome 實作外部擷取來源試用功能的方式。我們會透過內文變更更新這篇文章,並在下方註明特定變更。我們也會透過 @chromiumdev Twitter 帳戶分享重大異動相關資訊。