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

桌面端 PWA 應用:

桌面端 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

(完)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章