PWA 實踐/實戰/應用(Google Workbox)
桌面端 PWA 應用:
移動端新增到桌面:
1 什麼是 PWA
PWA(Progressive Web App - 漸進式網頁應用)是一種理念,由 Google Chrome 在 2015 年提出。PWA 它不是特指某一項技術,而是應用多項技術來改善使用者體驗的 Web App,其核心技術包括 Web App Manifest、Service Worker、Web 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)
- 支援新增到桌面,manifest。
- 支援快取,Service Worker。
2.3 餓了麼
訪問地址:https://h5.ele.me/msite/#pwa=true
PWA:自研 - PWA 在餓了麼的實踐經驗
- 支援新增到桌面,manifest。
- 支援快取和離線訪問,Service Worker。
2.4 Instagram
左邊原生應用,右邊 PWA
訪問地址:https://www.instagram.com/
PWA:使用 Google Workbox
- 支援新增到桌面,manifest。
- 支援快取,Service Worker。
2.5 Twitter
訪問地址:https://mobile.twitter.com/home
PWA:Twitter 自研 - How we built Twitter Lite
- 支援新增到桌面,manifest。
- 支援快取和離線訪問,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
(完)