Progressive Web App 离线後备页面: 玩过 Chrome 小恐龙游戏了吗 (11)

离线後备页面介绍 (offline fallback page)

离线後备页面提供用户在网路不稳定的情况下,一个备援的显示页面。

在过去的网站大多由服务器提供,所以断线的情况下原则上就是什麽都没有,近几年 JavaScript 相关的进步以及 SPA 观念开始盛行後,前端能够掌握的事情也越来越多,离线後备页面 (offline fallback page) 一个有名的例子就是 Chrome 在断线状态下的小恐龙游戏。

图片来源: https://web.dev/

离线後备页面实作

对 Progressive Web App 来说,目前最佳的实作方式就是透过 service worker 搭配 Cache Storage API 来提供用户最佳的离线操作体验。

接下来会示范一个简单的情境,当用户网路断线时,会自动开启离线後备页面,页面中一方面提供重试的按钮,另一方面也透过程序实作当网路恢复时自动连线并切换回正常的页面。

Google 的这个范例主要会有三个页面加上 service worker 实作:

  1. 连线正常的第一页
  2. 连线正常的第二页
  3. 离线後备页面
  4. service worker: 侦测到断线时会将第一页或第二页切换到离线後备页面

Demo 站台:
https://linyencheng.github.io/pwa-offline-fallback/

原始码:
https://github.com/LinYenCheng/pwa-offline-fallback/tree/main/docs

service worker 离线後备页面实作

  1. 宣告常数
// 这次 Cache 存放的名称
const CACHE_NAME = "offline";
// 离线後备页面档名
const OFFLINE_URL = "offline.html";
  1. 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();
});
  1. 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();
});
  1. 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';
  • 文件中有提供范例可参考修改,底下的例子说明怎麽把 mp4 相关资源快取
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(),
    ],
  })
);

<<:  [Part 3 ] Vue.js 的精随-元件 provide/inject

>>:  8. 你的薪水是怎麽决定的?

Day18 遇到问题该怎麽办?

大家好,我是乌木白,今天想和大家聊聊,如果在学习时或者在写专案时遇到问题该怎麽办? 遇到问题? 今...

Day 10- 物品借用纪录系统 (2) 设定 Calendar

昨天我们完成了基础建设,但是有个地方忘记讲到,我现在赶快补充一下! 我们昨天设定归还日期时,一定有人...

Day 24: 人工智慧在音乐领域的应用 (AI作曲- RNN作曲)

循环神经网路(Recurrent neural network, RNN) 首先我们先来介绍循环神经...

Day 13:第三方套件、授权

前言 以前很经常使用 GitHub 上的各种套件, 不管是大到页面,小到按钮,深到语法,都喜欢用自己...

Day10-元件沟通传递(part2)

没有props还可以传资料吗 v-bind和v-on在没有props的情况下一样可以得到父层的资料。...