1. 程式人生 > 其它 >PWA 實踐/實戰/應用(Google Workbox)

PWA 實踐/實戰/應用(Google Workbox)

桌面端 PWA 應用:

移動端新增到桌面:

1 什麼是 PWA

PWA(Progressive Web App - 漸進式網頁應用)是一種理念,由 Google Chrome 在 2015 年提出。PWA 它不是特指某一項技術,而是應用多項技術來改善使用者體驗的 Web App,其核心技術包括 Web App ManifestService WorkerWeb Push 等,使用者體驗才是 PWA 的核心。

PWA 主要特點如下:

  • 可靠 - 即使在網路不穩定甚至斷網的環境下,也能瞬間載入並展現。
  • 使用者體驗 - 快速響應,具有平滑的過渡動畫及使用者操作的反饋。
  • 使用者黏性 - 和 Native App 一樣,可以被新增到桌面,能接受離線通知,具有沉浸式的使用者體驗。

PWA 本身強調漸進式(Progressive),可以從兩個角度來理解漸進式,首先,PWA 還在不斷進化,Service Worker、Web App Manifest、Device API 等標準每年都會有不小的進步;其次,標準的設計向下相容,並且侵入性小,開發者使用新特性代價很小,只需要在原有站點上新增,讓站點的使用者體驗漸進式的增強。相關技術基準線:What makes a good Progressive Web App?

  • 站點需要使用 HTTPS。
  • 頁面需要響應式,能夠在平板和移動裝置上都具有良好的瀏覽體驗。
  • 所有的 URL 在斷網的情況下有內容展現,不會展現瀏覽器預設頁面。
  • 需要支援 Wep App Manifest,能被新增到桌面
  • 即使在 3G 網路下,頁面載入要快,可互動時間要短。
  • 在主流瀏覽器下都能正常展現。
  • 動畫要流暢,有使用者操作反饋。
  • 每個頁面都有獨立的 URL。

2 案例調研

2.1 米哈遊 - 崩壞3

訪問地址:https://bbs.mihoyo.com/bh3/

PWA:僅支援在 IOS 端新增到桌面。

2.2 阿里速賣通(AliExpress)

訪問地址:https://m.aliexpress.com/

PWA:使用 Google Workbox(CDN)

  1. 支援新增到桌面,manifest
  2. 支援快取,Service Worker

2.3 餓了麼

訪問地址:https://h5.ele.me/msite/#pwa=true

PWA:自研 - PWA 在餓了麼的實踐經驗

  1. 支援新增到桌面,manifest
  2. 支援快取和離線訪問,Service Worker

2.4 Instagram

左邊原生應用,右邊 PWA

訪問地址:https://www.instagram.com/

PWA:使用 Google Workbox

  1. 支援新增到桌面,manifest
  2. 支援快取,Service Worker

2.5 Twitter

訪問地址:https://mobile.twitter.com/home

PWA:Twitter 自研 - How we built Twitter Lite

  1. 支援新增到桌面,manifest
  2. 支援快取和離線訪問,Service Worker

除了正常的靜態資源以外,Twitter 把首頁也快取了下來。

離線狀態下有很好的使用者體驗,而不是顯示預設的瀏覽器頁面。

3 技術選型(Service Worker)

3.1 使用 Google Workbox 構建 Service Worker

3.1.1 什麼是 Workbox

Workbox 是一組庫,可以幫助開發者編寫 Service Worker,通過 CacheStorage API 快取資源。當一起使用 Service Worker 和 CacheStorage API 時,可以控制網站上使用的資源(HTML、CSS、JS、影象等)如何從網路或快取中請求,甚至允許在離線時返回快取的內容。

3.1.2 如何使用 Workbox

Workbox 是由許多 NPM 模組組成的。首先要從 NPM 中安裝它,然後匯入專案 Service Worker 所需的模組。Workbox 的主要特性之一是它的路由和快取策略模組。

路由和快取策略

Workbox 允許使用不同的快取策略來管理 HTTP 請求的快取。首先確定正在處理的請求是否符合條件,如果符合,則對其應用快取策略。匹配是通過返回真值的回撥函式進行的。快取策略可以是 Workbox 的一種預定義策略,也可以建立自己的策略。如下是一個使用路由和快取的基本 Service Worker。

import { registerRoute } from 'workbox-routing';
import {
  NetworkFirst,
  StaleWhileRevalidate,
  CacheFirst,
} from 'workbox-strategies';

// Used for filtering matches based on status code, header, or both
import { CacheableResponsePlugin } from 'workbox-cacheable-response';
// Used to limit entries in cache, remove entries after a certain period of time
import { ExpirationPlugin } from 'workbox-expiration';

// Cache page navigations (html) with a Network First strategy
registerRoute(
  // Check to see if the request is a navigation to a new page
  ({ request }) => request.mode === 'navigate',
  // Use a Network First caching strategy
  new NetworkFirst({
    // Put all cached files in a cache named 'pages'
    cacheName: 'pages',
    plugins: [
      // Ensure that only requests that result in a 200 status are cached
      new CacheableResponsePlugin({
        statuses: [200],
      }),
    ],
  }),
);

// Cache CSS, JS, and Web Worker requests with a Stale While Revalidate strategy
registerRoute(
  // Check to see if the request's destination is style for stylesheets, script for JavaScript, or worker for web worker
  ({ request }) =>
    request.destination === 'style' ||
    request.destination === 'script' ||
    request.destination === 'worker',
  // Use a Stale While Revalidate caching strategy
  new StaleWhileRevalidate({
    // Put all cached files in a cache named 'assets'
    cacheName: 'assets',
    plugins: [
      // Ensure that only requests that result in a 200 status are cached
      new CacheableResponsePlugin({
        statuses: [200],
      }),
    ],
  }),
);

// Cache images with a Cache First strategy
registerRoute(
  // Check to see if the request's destination is style for an image
  ({ request }) => request.destination === 'image',
  // Use a Cache First caching strategy
  new CacheFirst({
    // Put all cached files in a cache named 'images'
    cacheName: 'images',
    plugins: [
      // Ensure that only requests that result in a 200 status are cached
      new CacheableResponsePlugin({
        statuses: [200],
      }),
      // Don't cache more than 50 items, and expire them after 30 days
      new ExpirationPlugin({
        maxEntries: 50,
        maxAgeSeconds: 60 * 60 * 24 * 30, // 30 Days
      }),
    ],
  }),
);

這個 Service Worker 使用一個網路優先的策略來快取導航請求(用於新的 HTML 頁面),當它狀態碼為 200 時,該策略將快取的頁面儲存在一個名為 pages 的快取中。使用 Stale While Revalidate strategy 快取 CSS、JavaScript 和 Web Worker,將快取的資源儲存在一個名為 assets 的快取中。採用快取優先的策略來快取影象,將快取的影象儲存在名為 images 的快取中,30 天過期,並且一次只允許 50 個。

預快取

除了在發出請求時進行快取(執行時快取)之外,Workbox 還支援預快取,即在安裝 Service Worker 時快取資源。有許多資源是非常適合預快取的:Web 應用程式的起始 URL、離線回退頁面以及關鍵的 JavaScript 和 CSS 檔案。

使用一個支援預快取清單注入的外掛(webpack 或 rollup)來在新的 Service Worker 中使用預快取。

import { precacheAndRoute } from 'workbox-precaching';

// Use with precache injection
precacheAndRoute(self.__WB_MANIFEST);

這個 Service Worker 將在安裝時預快取檔案,替換 self.__WB_MANIFEST,其中包含在構建時注入到 Service Worker 中的資源。

離線回退

讓 Web 應用在離線工作時感覺更健壯的常見模式是提供一個後退頁面,而不是顯示瀏覽器的預設錯誤頁面。通過 Workbox 路由和預快取,你可以在幾行程式碼中設定這個模式。

import { precacheAndRoute, matchPrecache } from 'workbox-precaching';
import { setCatchHandler } from 'workbox-routing';

// Ensure your build step is configured to include /offline.html as part of your precache manifest.
precacheAndRoute(self.__WB_MANIFEST);

// Catch routing errors, like if the user is offline
setCatchHandler(async ({ event }) => {
  // Return the precached offline page if a document is being requested
  if (event.request.destination === 'document') {
    return matchPrecache('/offline.html');
  }

  return Response.error();
});

如果使用者處於離線狀態,則返回快取的離線頁面的內容,而不是生成一個瀏覽器錯誤。

有了 Workbox,可以利用 Service Worker 的力量來提高效能,並給您的站點提供獨立於網路的優秀的使用者體驗。

3.2 自研 Service Worker

自研 Service Worker 更加靈活、可控,但是因為需要考慮到各種相容,研發成本較高。

4 技術實踐(Service Worker)

4.1 使用 CLI

安裝 Workbox:

npm install workbox-cli -D

npx workbox --help

按照引導配置 workbox-config.js

npx workbox wizard

根據配置生成 Service Worker 程式:

npx workbox generateSW workbox-config.js

由於實際靜態資源是掛載在 CDN 上面,需要修改預渲染資源的字首

Workbox CLI - generateSW - Configuration

// A transformation that prepended the origin of a CDN for any URL starting with '/assets/' could be implemented as:

const cdnTransform = async (manifestEntries) => {
  const manifest = manifestEntries.map(entry => {
    const cdnOrigin = 'https://example.com';
    if (entry.url.startsWith('/assets/')) {
      entry.url = cdnOrigin + entry.url;
    }
    return entry;
  });
  return {manifest, warnings: []};
};

更多快取配置可查閱官方文件

4.2 使用 Webpack

安裝:

npm install workbox-webpack-plugin --save-dev

Webpack 配置:

// Inside of webpack.config.js:
const WorkboxPlugin = require('workbox-webpack-plugin');
// Version info...
const id = `${page}-v${version}`;

module.exports = {
  // Other webpack config...

  plugins: [
    // Other plugins...

    // WIKI https://developers.google.com/web/tools/workbox/reference-docs/latest/module-workbox-webpack-plugin.GenerateSW#GenerateSW
    new WorkboxPlugin.GenerateSW({
        cacheId: `${id}-gsw`,
        // Do not precache images
        exclude: [/\.(?:png|jpg|jpeg|svg)$/, 'service-wroker.js'], // Page need refresh twice.
        // target dir
        swDest: `../dist/${page}/service-worker.js`,
        skipWaiting: true,
        clientsClaim: true,
        // Define runtime caching rules.
        // WIKI https://developers.google.com/web/tools/workbox/reference-docs/latest/module-workbox-build#.RuntimeCachingEntry
        // Example https://gist.github.com/jeffposnick/fc761c06856fa10dbf93e62ce7c4bd57
        runtimeCaching: [
          // icon images
          {
            // Match any request that ends with .png, .jpg, .jpeg or .svg.
            urlPattern: /^https:\/\/cdn.example.com\/platform/, // /\.(?:png|jpg|jpeg|svg)$/,
            // Apply a cache-first strategy.
            handler: 'CacheFirst',
            options: {
              // Use a custom cache name.
              cacheName: `${id}-icon-images`,
              // Only cache 50 images, and expire them after 30 days
              expiration: {
                maxEntries: 50
              },
              // Ensure that only requests that result in a 200 status are cached
              cacheableResponse: {
                statuses: [0, 200]
              }
            }
          },
          // note images & others
          {
            // Match any request that ends with .png, .jpg, .jpeg or .svg.
            urlPattern: /^https:\/\/image.example.com/, // /\.(?:png|jpg|jpeg|svg)$/,
            // Apply a cache-first strategy.
            handler: 'CacheFirst',
            options: {
              // Use a custom cache name.
              cacheName: `${id}-note-images`,
              // Only cache 50 images, and expire them after 30 days
              expiration: {
                maxEntries: 50,
                maxAgeSeconds: 60 * 60 * 24 * 30 // 30 Days
              },
              // Ensure that only requests that result in a 200 status are cached
              cacheableResponse: {
                statuses: [0, 200]
              }
            }
          }
        ]
      });
  ]
};

頁面中觸發 Service Work:

<script>
// Check that service workers are supported
if ('serviceWorker' in navigator) {
  // Use the window load event to keep the page load performant
  window.addEventListener('load', () => {
    navigator.serviceWorker.register('/service-worker.js');
  });
}
</script>

5 新增到桌面方案

5.1 manifest.json 配置

{
  "name": "不知不問",
  "short_name": "不知不問",
  "description": "yyds",
  "start_url": "/?entry_mode=standalone",
  "display": "standalone",
  "orientation": "portrait",
  "background_color": "#F3F3F3",
  "theme_color": "#F3F3F3",
  "icons": [
    {
      "src": "https://mazey.cn/fav/logo-dark-circle-32x32.png",
      "sizes": "32x32",
      "type": "image/png"
    },
    {
      "src": "https://mazey.cn/fav/logo-dark-circle-144x144.png",
      "sizes": "144x144",
      "type": "image/png"
    },
    {
      "src": "https://mazey.cn/fav/logo-dark-circle-152x152.png",
      "sizes": "152x152",
      "type": "image/png"
    },
    {
      "src": "https://mazey.cn/fav/logo-dark-circle-180x180.png",
      "sizes": "180x180",
      "type": "image/png"
    },
    {
      "src": "https://mazey.cn/fav/logo-dark-circle-192x192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "https://mazey.cn/fav/logo-dark-circle-512x512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ],
  "scope": "/"
}

5.2 <head> 配置

為網站配置開屏圖片、狀態列等。

<!--Mazey's favicon begin-->
<link rel="shortcut icon" type="image/png" href="https://mazey.cn/fav/logo-dark-circle-transparent-144x144.png">
<link rel="icon" type="image/png" sizes="32x32" href="https://mazey.cn/fav/logo-dark-circle-transparent-32x32.png">
<link rel="apple-touch-icon" sizes="144x144" href="https://mazey.cn/fav/logo-dark-circle-144x144.png">
<link rel="apple-touch-icon" sizes="152x152" href="https://mazey.cn/fav/logo-dark-circle-152x152.png">
<link rel="apple-touch-icon" sizes="180x180" href="https://mazey.cn/fav/logo-dark-circle-180x180.png">
<link rel="apple-touch-icon" sizes="192x192" href="https://mazey.cn/fav/logo-dark-circle-192x192.png">
<link rel="apple-touch-icon" sizes="512x512" href="https://mazey.cn/fav/logo-dark-circle-512x512.png">
<!--Mazey's favicon end-->
<!--Mazey's pwa manifest.json-->
<link rel="manifest" href="/wp-content/themes/polestar/manifest.json">
<!-- 開機圖片 - begin -->
<!-- iPhone Xs Max (1242px × 2688px) -->
<link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3)" href="https://i.mazey.net/asset/read/cat-lovers-1242x2688.jpg" sizes="1242x2688">
<!-- iPhone Xr (828px x 1792px) -->
<link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2)" href="https://i.mazey.net/asset/read/cat-lovers-828x1792.jpg" sizes="828x1792">
<!-- iPhone X, Xs (1125px x 2436px) -->
<link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3)" href="https://i.mazey.net/asset/read/cat-lovers-1125x2436.jpg" sizes="1125x2436">
<!-- iPhone 8, 7, 6s, 6 (750px x 1334px) -->
<link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2)" href="https://i.mazey.net/asset/read/cat-lovers-750x1334.jpg" sizes="750x1334">
<!-- iPhone 8 Plus, 7 Plus, 6s Plus, 6 Plus (1242px x 2208px) -->
<link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3)" href="https://i.mazey.net/asset/read/cat-lovers-1242x2208.jpg" sizes="1242x2208">
<!-- iPhone 5 (640px x 1136px) -->
<link rel="apple-touch-startup-image" media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2)" href="https://i.mazey.net/asset/read/cat-lovers-640x1136.jpg" sizes="640x1136">
<!-- 開機圖片 - end -->
<!-- Touch Bar區域顯示的網站圖示 -->
<link rel="mask-icon" href="https://mazey.cn/fav/logo-dark-circle.svg" color="#F3F3F3">
<!-- 主題色 = manifest.json theme_color -->
<meta name="theme-color" content="#F3F3F3">
<meta name="apple-mobile-web-app-capable" content="yes">
<!-- 狀態列顏色 default/black/black-translucent -->
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<!-- 應用名 -->
<meta name="apple-mobile-web-app-title" content="不知不問">
<!-- 在Windows 8上,我們可以將網站固定在開始螢幕上,而且支援個性化自定義色塊icon和背景圖片。這個標籤是用來定義色塊的背景圖的。色塊圖應該為144*144畫素的png格式圖片,背景透明。 -->
<meta name="msapplication-TileImage" content="https://mazey.cn/fav/logo-dark-circle-transparent-144x144.png">
<!-- 同前一個元資料msapplication-TileImage類似,這個功能是用來設定顏色值,個性化自定義色塊(磁貼)icon -->
<meta name="msapplication-TileColor" content="#F3F3F3">

開屏圖片尺寸總結:

螢幕尺寸 倍數 圖片尺寸
1024x1366(512x683) x2 2048x2732
834x1194(417x597) x2 1668x2388
768x1024(384x512) x2 1536x2048
834x1112(417x556) x2 1668x2224
810x1080 x2 1620x2160
428x926(214x463) x3 1284x2778
390x844 x3 1170x2532
375x812 x3 1125x2436
414x896 x3 1242x2688
414x896 x2 828x1792
414x736 x3 1242x2208
375x667 x2 750x1334
320x568 x2 640x1136

版權宣告

本部落格所有的原創文章,作者皆保留版權。轉載必須包含本宣告,保持本文完整,並以超連結形式註明作者後除和本文原始地址:https://blog.mazey.net/2675.html

(完)