離線後備頁面提供用戶在網路不穩定的情況下,一個備援的顯示頁面。
在過去的網站大多由伺服器提供,所以斷線的情況下原則上就是什麼都沒有,近幾年 JavaScript 相關的進步以及 SPA 觀念開始盛行後,前端能夠掌握的事情也越來越多,離線後備頁面 (offline fallback page) 一個有名的例子就是 Chrome 在斷線狀態下的小恐龍遊戲。
圖片來源: https://web.dev/
對 Progressive Web App 來說,目前最佳的實作方式就是透過 service worker 搭配 Cache Storage API 來提供用戶最佳的離線操作體驗。
接下來會示範一個簡單的情境,當用戶網路斷線時,會自動開啟離線後備頁面,頁面中一方面提供重試的按鈕,另一方面也透過程式實作當網路恢復時自動連線並切換回正常的頁面。
Google 的這個範例主要會有三個頁面加上 service worker 實作:
Demo 站台:
https://linyencheng.github.io/pwa-offline-fallback/
原始碼:
https://github.com/LinYenCheng/pwa-offline-fallback/tree/main/docs
// 這次 Cache 存放的名稱
const CACHE_NAME = "offline";
// 離線後備頁面檔名
const OFFLINE_URL = "offline.html";
install
: 命名快取空間為 CACHE_NAME
並加入 OFFLINE_URL
快取,透過 self.skipWaiting()
跳過等待重啟生效。self.addEventListener("install", (event) => {
event.waitUntil(
(async () => {
// 使用 CACHE_NAME
const cache = await caches.open(CACHE_NAME);
// Setting {cache: 'reload'} in the new request will ensure that the response
// isn't fulfilled from the HTTP cache; i.e., it will be from the network.
await cache.add(new Request(OFFLINE_URL, { cache: "reload" }));
})()
);
// Force the waiting service worker to become the active service worker.
self.skipWaiting();
});
activate
: 透過 self.clients.claim()
讓 Service Worker 直接生效self.addEventListener("activate", (event) => {
event.waitUntil(
(async () => {
// Enable navigation preload if it's supported.
// See https://developers.google.com/web/updates/2017/02/navigation-preload
if ("navigationPreload" in self.registration) {
await self.registration.navigationPreload.enable();
}
})()
);
// Tell the active service worker to take control of the page immediately.
self.clients.claim();
});
fetch
: 遇到連線錯誤則使用 CACHE_NAME
中的 OFFLINE_URL
的快取self.addEventListener("fetch", (event) => {
// We only want to call event.respondWith() if this is a navigation request
// for an HTML page.
if (event.request.mode === "navigate") {
event.respondWith(
(async () => {
try {
// First, try to use the navigation preload response if it's supported.
const preloadResponse = await event.preloadResponse;
if (preloadResponse) {
return preloadResponse;
}
// Always try the network first.
const networkResponse = await fetch(event.request);
return networkResponse;
} catch (error) {
// catch is only triggered if an exception is thrown, which is likely
// due to a network error.
// If fetch() returns a valid HTTP response with a response code in
// the 4xx or 5xx range, the catch() will NOT be called.
console.log("Fetch failed; returning offline page instead.", error);
const cache = await caches.open(CACHE_NAME);
const cachedResponse = await cache.match(OFFLINE_URL);
return cachedResponse;
}
})()
);
}
// If our if() condition is false, then this fetch handler won't intercept the
// request. If there are any other fetch handlers registered, they will get a
// chance to call event.respondWith(). If no fetch handlers call
// event.respondWith(), the request will be handled by the browser as if there
// were no service worker involvement.
});
因為是離線後備頁面,所以我們會需要把所有的資源都快取起來,其中最簡單的方式就是將所有需要的東西都用 inline 的方式寫在 html 中,若是想要自己去實作較複雜的快取機制,比較建議取使用 workbox 這套工具。
參考連結:
https://developers.google.com/web/tools/workbox/guides/advanced-recipes#offline_page_only
import {CacheFirst, NetworkFirst, StaleWhileRevalidate} from 'workbox-strategies';
import { registerRoute } from "workbox-routing";
import { CacheFirst } from "workbox-strategies";
import { CacheableResponsePlugin } from "workbox-cacheable-response";
import { RangeRequestsPlugin } from "workbox-range-requests";
// 把 mp4 相關資源都快取起來
registerRoute(
({ url }) => url.pathname.endsWith(".mp4"),
new CacheFirst({
cacheName: "your-cache-name-here",
plugins: [
new CacheableResponsePlugin({ statuses: [200] }),
new RangeRequestsPlugin(),
],
})
);