建立離線備用頁面

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 範例,僅針對離線頁面

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