渐进式 Web 应用的一个关键方面是可靠性;即使在网络状况不佳的情况下,它们也能快速加载资源,让用户保持参与度并立即提供反馈。为什么会这样?得益于服务工作线程 fetch
事件。
提取事件
借助 fetch
事件,我们可以拦截服务工作线程作用域内 PWA 发出的每个网络请求,包括同源请求和跨源请求。除了导航和资源请求之外,从已安装的服务工作线程进行提取还允许在网站首次加载后呈现页面访问,而无需进行网络调用。
fetch
处理程序会接收来自应用的所有请求(包括网址和 HTTP 标头),并让应用开发者决定如何处理这些请求。
您的服务工作线程可以将请求转发到网络,使用之前缓存的响应进行响应,或创建新的响应。一切由您选择。 下面是一个简单的示例:
self.addEventListener("fetch", event => { console.log(`URL requested: ${event.request.url}`); });
响应请求
当请求进入您的 service worker 时,您可以执行两项操作:忽略请求(让请求进入网络)或响应请求。通过在 Service Worker 中响应请求,您可以选择将哪些内容返回给 PWA,以及如何返回,即使在用户离线时也是如此。
如需响应传入的请求,请在 fetch
事件处理脚本中调用 event.respondWith()
,如下所示:
// fetch event handler in your service worker file self.addEventListener("fetch", event => { const response = .... // a response or a Promise of response event.respondWith(response); });
您必须同步调用 respondWith()
,并且必须返回 Response 对象。不过,您无法在 fetch 事件处理脚本完成之后(例如在异步调用中)调用 respondWith()
。如果您需要等待完整响应,可以将一个 Promise 传递给 respondWith()
,该 Promise 会解析为 Response。
创建回答
借助 Fetch API,您可以在 JavaScript 代码中创建 HTTP 响应,这些响应可以使用 Cache Storage API 进行缓存,并像来自 Web 服务器一样返回。
如需创建响应,请创建一个新的 Response
对象,并设置其正文和选项(例如状态和标头):
const simpleResponse = new Response("Body of the HTTP response"); const options = { status: 200, headers: { 'Content-type': 'text/html' } }; const htmlResponse = new Response("<b>HTML</b> content", options)
从缓存中响应
现在,您已经了解如何从 service worker 提供 HTTP 响应,接下来可以开始使用 Caching Storage 接口将资源存储在设备上。
您可以使用缓存存储 API 检查从 PWA 收到的请求是否在缓存中可用,如果可用,则使用该请求响应 respondWith()
。为此,您首先需要在缓存中进行搜索。顶级 caches
接口中提供的 match()
函数用于搜索来源中的所有存储区或单个打开的缓存对象。
match()
函数会接收 HTTP 请求或网址作为实参,并返回一个 promise,该 promise 会通过与相应键关联的 Response 进行解析。
// Global search on all caches in the current origin caches.match(urlOrRequest).then(response => { console.log(response ? response : "It's not in the cache"); }); // Cache-specific search caches.open("pwa-assets").then(cache => { cache.match(urlOrRequest).then(response => { console.log(response ? response : "It's not in the cache"); }); });
缓存策略
仅从浏览器缓存提供文件并不适合所有使用情形。例如,用户或浏览器可以逐出缓存。因此,您应定义自己的策略,以便为 PWA 提供资源。 您不必受限于一种缓存策略。您可以为不同的网址模式定义不同的规则。例如,您可以针对最少的界面资源制定一种策略,针对 API 调用制定另一种策略,再针对图片和数据网址制定第三种策略。 为此,请读取 ServiceWorkerGlobalScope.onfetch
中的 event.request.url
,并通过正则表达式或 网址 模式进行解析。(撰写本文时,并非所有平台都支持网址格式)。
最常见的策略包括:
- 先缓存
- 先搜索缓存的响应,如果未找到,则回退到网络。
- 网络优先
- 先从网络请求响应,如果没有返回任何响应,则检查缓存中是否有响应。
- Stale While Revalidate
- 从缓存中提供响应,同时在后台请求最新版本并将其保存到缓存中,以便下次请求相应资源时使用。
- 仅限网络
- 始终回复来自网络或错误的响应。系统永远不会查询缓存。
- 仅限缓存
- 始终使用缓存中的响应进行回复,或返回错误。系统永远不会咨询网络。使用此策略投放的资源必须先添加到缓存中,然后才能被请求。
先缓存
使用此策略,服务工作线程会在缓存中查找匹配的请求,如果找到,则返回相应的 Response。否则,它会从网络中检索响应(可以选择更新缓存以用于将来的调用)。如果没有缓存响应,也没有网络响应,请求将出错。由于在不访问网络的情况下提供素材资源往往更快,因此此策略优先考虑性能而非新鲜度。
self.addEventListener("fetch", event => { event.respondWith( caches.match(event.request) .then(cachedResponse => { // It can update the cache to serve updated content on the next request return cachedResponse || fetch(event.request); } ) ) });
网络优先
此策略与“先缓存”策略相反;它会检查是否可以从网络中满足请求,如果无法满足,则尝试从缓存中检索。例如,缓存优先。如果既没有网络响应,也没有缓存响应,请求将出错。从网络获取响应通常比从缓存获取响应慢,因此此策略优先考虑更新的内容,而不是性能。
self.addEventListener("fetch", event => { event.respondWith( fetch(event.request) .catch(error => { return caches.match(event.request) ; }) ); });
在重新验证时过时
“过时后重新验证”策略会立即返回缓存的响应,然后检查网络是否有更新,如果有,则替换缓存的响应。此策略始终会发出网络请求,因为即使找到缓存的资源,它也会尝试使用从网络接收的内容更新缓存中的内容,以便在下一个请求中使用更新后的版本。因此,此策略可让您受益于“先快速提供缓存”策略,并在后台更新缓存。
self.addEventListener('fetch', event => { event.respondWith( caches.match(event.request).then(cachedResponse => { const networkFetch = fetch(event.request).then(response => { // update the cache with a clone of the network response const responseClone = response.clone() caches.open(url.searchParams.get('name')).then(cache => { cache.put(event.request, responseClone) }) return response }).catch(function (reason) { console.error('ServiceWorker fetch failed: ', reason) }) // prioritize cached response over network return cachedResponse || networkFetch } ) ) })
仅限网络
仅限网络策略类似于浏览器在没有 Service Worker 或 Cache Storage API 的情况下的行为。只有当资源可以从网络中提取时,请求才会返回该资源。这通常对仅限在线的 API 请求等资源很有用。
仅缓存
缓存优先策略可确保请求永远不会发送到网络;所有传入的请求都会通过预填充的缓存项进行响应。以下代码使用 fetch
事件处理脚本和缓存存储的 match
方法来仅响应缓存:
self.addEventListener("fetch", event => { event.respondWith(caches.match(event.request)); });
自定义策略
虽然上述是常见的缓存策略,但您负责管理服务工作线程以及请求的处理方式。如果这些都不符合您的需求,请自行创建。
例如,您可以使用超时时间优先考虑更新内容的“网络优先”策略,但前提是响应出现在您设置的阈值范围内。您还可以将缓存的响应与网络响应合并,并从 Service Worker 构建复杂的响应。
更新素材资源
让 PWA 的缓存资源保持最新状态可能是一项挑战。虽然“过时时重新验证”策略是一种实现此目的的方法,但并非唯一的方法。在更新章节中,您将学习各种技巧,以保持应用的内容和资源处于最新状态。