建立離線備用頁面

Google 助理應用程式、Slack 應用程式、Zoom 應用程式,以及手機或電腦上幾乎所有其他平台專屬應用程式,有什麼共同點?沒錯,他們至少會給你一些東西。即使沒有網路連線,你還是可以開啟 Google 助理應用程式、進入 Slack 或啟動 Zoom。您可能無法獲得特別有意義的內容,甚至無法達成預期目標,但至少您會獲得「某些東西」,而且應用程式處於可控狀態。

離線時使用 Google 助理行動應用程式。
Google 助理。

離線時的 Slack 行動應用程式。
Slack。

離線時使用 Zoom 行動應用程式。
縮放。

使用特定平台的應用程式,即使未連上網路,也不會完全沒有任何資訊。

相較之下,在網頁上,您在離線時通常不會收到任何內容。Chrome 會提供離線恐龍遊戲,但僅此而已。

Google Chrome 行動應用程式顯示離線恐龍遊戲。
iOS 版 Google Chrome。

Google Chrome 桌面應用程式顯示離線恐龍遊戲。
macOS 版 Google Chrome。

在網頁上,如果沒有網路連線,則預設不會顯示任何內容。

使用自訂 Service Worker 的離線備用網頁

但不一定非得如此。您可以使用服務工作者和 Cache Storage API,為使用者提供客製化的離線體驗。這可以是簡單的品牌頁面,提供使用者目前離線的資訊,但也可以是更具創意的解決方案,例如著名的 trivago 離線迷宮遊戲,其中包含手動重新連線按鈕和自動重新連線嘗試倒數計時器。

Trivago 離線頁面,其中有 Trivago 離線迷宮。
Trivago 離線迷宮。

註冊 Service Worker

這項功能的實作方式是透過服務工作者。您可以從主頁面註冊服務工作者,如下列程式碼範例所示。通常是在應用程式載入後執行這項操作。

window.addEventListener("load", () => {
  if ("serviceWorker" in navigator) {
    navigator.serviceWorker.register("service-worker.js");
  }
});

Service worker 程式碼

實際的 Service Worker 檔案內容乍看之下可能有點複雜,但下方範例中的註解應該能讓您瞭解其中的細節。核心概念是預先快取名為 offline.html 的檔案,該檔案只會在失敗的導覽要求中提供,並讓瀏覽器處理所有其他情況:

/*
Copyright 2015, 2019, 2020, 2021 Google LLC. All Rights Reserved.
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
 You may obtain a copy of the License at
 http://www.apache.org/licenses/LICENSE-2.0
 Unless required by applicable law or agreed to in writing, software
 distributed under the License is distributed on an "AS IS" BASIS,
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 See the License for the specific language governing permissions and
 limitations under the License.
*/

// Incrementing OFFLINE_VERSION will kick off the install event and force
// previously cached resources to be updated from the network.
// This variable is intentionally declared and unused.
// Add a comment for your linter if you want:
// eslint-disable-next-line no-unused-vars
const OFFLINE_VERSION = 1;
const CACHE_NAME = "offline";
// Customize this with a different URL if needed.
const OFFLINE_URL = "offline.html";

self.addEventListener("install", (event) => {
  event.waitUntil(
    (async () => {
      const cache = await caches.open(CACHE_NAME);
      // Setting {cache: 'reload'} in the new request ensures 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();
});

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();
});

self.addEventListener("fetch", (event) => {
  // Only 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.
});

離線備用頁面

您可以使用 offline.html 檔案製作廣告素材,並根據需求進行調整及加入品牌標誌。以下範例顯示最少的可能內容。這份範例會示範按下按鈕時的手動重新載入,以及根據 online 事件和定期伺服器輪詢自動重新載入。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />

    <title>You are offline</title>

    <!-- Inline the page's stylesheet. -->
    <style>
      body {
        font-family: helvetica, arial, sans-serif;
        margin: 2em;
      }

      h1 {
        font-style: italic;
        color: #373fff;
      }

      p {
        margin-block: 1rem;
      }

      button {
        display: block;
      }
    </style>
  </head>
  <body>
    <h1>You are offline</h1>

    <p>Click the button below to try reloading.</p>
    <button type="button">⤾ Reload</button>

    <!-- Inline the page's JavaScript file. -->
    <script>
      // Manual reload feature.
      document.querySelector("button").addEventListener("click", () => {
        window.location.reload();
      });

      // Listen to changes in the network state, reload when online.
      // This handles the case when the device is completely offline.
      window.addEventListener('online', () => {
        window.location.reload();
      });

      // Check if the server is responding and reload the page if it is.
      // This handles the case when the device is online, but the server
      // is offline or misbehaving.
      async function checkNetworkAndReload() {
        try {
          const response = await fetch('.');
          // Verify we get a valid response from the server
          if (response.status >= 200 && response.status < 500) {
            window.location.reload();
            return;
          }
        } catch {
          // Unable to connect to the server, ignore.
        }
        window.setTimeout(checkNetworkAndReload, 2500);
      }

      checkNetworkAndReload();
    </script>
  </body>
</html>

示範

您可以在下方嵌入的示範中,查看離線備用頁面的實際運作情形。如有興趣,您可以前往 Glitch 探索原始碼

關於讓應用程式可供安裝的附註

網站已設有離線備用網頁,您可能會想知道後續步驟。如要讓應用程式可供安裝,您必須新增網頁應用程式資訊清單,並視需要提供安裝策略

使用 Workbox.js 提供離線備用頁面的附註

您可能聽過Workbox。Workbox 是一組 JavaScript 程式庫,可為網頁應用程式新增離線支援功能。如果您希望自己編寫的服務無人機程式碼較少,可以使用 Workbox 範例,僅針對離線頁面

接下來,我們將說明如何為應用程式定義安裝策略