Tạo trang dự phòng ngoại tuyến

Ứng dụng Trợ lý Google, ứng dụng Slack, ứng dụng Zoom và hầu hết mọi ứng dụng khác dành riêng cho nền tảng trên điện thoại hoặc máy tính của bạn có điểm chung gì? Phải, ít nhất chúng luôn mang đến cho bạn một điều gì đó. Ngay cả khi không có kết nối mạng, bạn vẫn có thể mở ứng dụng Trợ lý, truy cập vào Slack hoặc chạy Zoom. Bạn có thể không nhận được bất kỳ điều gì đặc biệt có ý nghĩa hoặc thậm chí không thể đạt được những gì mình muốn đạt được, nhưng ít nhất bạn sẽ nhận được điều gì đó và ứng dụng có quyền kiểm soát.

Ứng dụng Trợ lý Google dành cho thiết bị di động khi không có mạng.
Trợ lý Google.

Ứng dụng Slack khi không có mạng.
Slack.

Ứng dụng Zoom (dành cho thiết bị di động) khi không có mạng.
Thu phóng.

Với các ứng dụng dành riêng cho nền tảng, ngay cả khi không có kết nối mạng, bạn sẽ không bao giờ nhận được gì.

Ngược lại, trên Web, thông thường bạn không nhận được gì khi bạn ngoại tuyến. Chrome mang đến cho bạn trò chơi khủng long ngoại tuyến, nhưng thế là xong.

Ứng dụng Google Chrome dành cho thiết bị di động đang hiển thị trò chơi khủng long ngoại tuyến.
Google Chrome dành cho iOS.

Ứng dụng Google Chrome dành cho máy tính để bàn hiển thị trò chơi khủng long ngoại tuyến.
Google Chrome cho macOS.

Trên web, khi không có kết nối mạng, theo mặc định, bạn sẽ không nhận được gì.

Trang dự phòng ngoại tuyến có một trình chạy dịch vụ tuỳ chỉnh

Tuy nhiên, bạn không nhất thiết phải làm theo cách này. Nhờ có trình chạy dịch vụ và API Bộ nhớ đệm, bạn có thể cung cấp trải nghiệm ngoại tuyến tuỳ chỉnh cho người dùng. Đây có thể là một trang có thương hiệu đơn giản với thông tin rằng người dùng hiện đang ngoại tuyến, nhưng cũng có thể là một giải pháp sáng tạo hơn, chẳng hạn như trò chơi mê cung ngoại tuyến trivago nổi tiếng với nút Reconnect (Kết nối lại) thủ công và tự động đếm ngược thời gian kết nối lại.

Trang trivago ngoại tuyến với mê cung trivago ngoại tuyến.
Mê cung trivago ngoại tuyến.

Đăng ký trình chạy dịch vụ

Để thực hiện việc này, hãy sử dụng một trình chạy dịch vụ. Bạn có thể đăng ký một trình chạy dịch vụ từ trang chính của mình như trong mã mẫu dưới đây. Thông thường, bạn làm việc này sau khi ứng dụng tải xong.

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

Mã trình chạy dịch vụ

Thoạt nhìn, nội dung của tệp trình chạy dịch vụ thực tế có vẻ hơi liên quan, nhưng các nhận xét trong mẫu dưới đây sẽ làm sáng tỏ mọi thứ. Ý tưởng chính là lưu trước vào bộ nhớ đệm một tệp có tên offline.html chỉ được phân phát khi có các yêu cầu điều hướng không thành công và cho phép trình duyệt xử lý tất cả các trường hợp khác:

/*
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.
});

Trang dự phòng ngoại tuyến

Tệp offline.html là nơi bạn có thể sáng tạo và điều chỉnh cho phù hợp với nhu cầu cũng như thêm thương hiệu của mình. Ví dụ dưới đây cho thấy mức tối thiểu tối thiểu về những gì có thể. Nó minh hoạ cả việc tải lại thủ công dựa trên thao tác nhấn nút cũng như tự động tải lại dựa trên sự kiện online và cuộc thăm dò máy chủ thông thường.

<!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>

Bản minh hoạ

Bạn có thể xem trang dự phòng ngoại tuyến đang hoạt động trong bản minh hoạ nhúng bên dưới. Nếu quan tâm, bạn có thể khám phá mã nguồn trên trang Glitch.

Lưu ý nhỏ về việc đảm bảo ứng dụng dễ cài đặt

Giờ đây, trang web của bạn đã có trang dự phòng ngoại tuyến, có thể bạn sẽ thắc mắc về các bước tiếp theo. Để ứng dụng của bạn có thể cài đặt được, bạn cần thêm tệp kê khai ứng dụng web và đưa ra một chiến lược cài đặt (không bắt buộc).

Lưu ý phụ về việc phân phát trang dự phòng ngoại tuyến bằng Workbox.js

Bạn có thể đã nghe đến Workbox. Workbox là một tập hợp các thư viện JavaScript để thêm tính năng hỗ trợ ngoại tuyến vào các ứng dụng web. Nếu muốn tự viết ít mã trình chạy dịch vụ hơn, bạn có thể sử dụng công thức Workbox cho chỉ trang ngoại tuyến.

Tiếp theo, hãy tìm hiểu cách xác định chiến lược cài đặt cho ứng dụng của bạn.