PWA 簡單介紹和實作
目錄
- 何謂 PWA,他解決了什麼?
- Service Worker
- Manifest json
- 跨瀏覽器及裝置問題
- 實作
- 加到 home screen
- 離線使用
何謂 PWA,他解決了什麼?
全名為 Progress Web App,其實就是 web 想要達到 native app 的功能,那究竟哪些功能是它想要達到的呢?我們先比較一下兩者的差異
Web | App | |
---|---|---|
傳遞性 | 較高,因為不用下載,有瀏覽器就可以使用 | 較低,因為需要下載,我們不太會在下載 50 個 app,但有可能逛了超過 50 個網頁 |
黏著度 | 較低,以 google map 來說,比較方便的是點開 app 去使用,而不是用瀏覽器打開 | 較高,手機桌面的 app 通常是我們花最多時間的,而不是在瀏覽器上面 |
PWA 的出現,就是希望帶著 Web 原本的優勢,慢慢將 App 獨有的優勢加到 Web 裡面,那之所以 app 會有高黏著度的優勢,我認為是以下幾點
- 手機可以下載成一個 icon 放在首頁
- 離線可以繼續使用
- 推播通知功能提醒使用者繼續回去使用
Web 若能達到以上三點,我認為就是 PWA 了,那首先要克服的就是離線瀏覽,接下來就會介紹 Service Worker
Service Worker
- Service Worker 類似一個中間層,能夠攔截網頁每次的發送和接收
- 它能決定該請求要從 Cache Storage 拿還是 Network
- Precaching 讓使用者第一次造訪網頁就可以使用離線功能
- 在 build time 就先把資料存入 cache
Service Worker 生命週期
- 是為了確保同一個註冊的網頁只有一個版本
- 避免開多個視窗卻有不同的 service worker,導致資料不一致
主要分成三階段
- Registration
if ('serviceWorker' in navigator) {
window.addEventListener('load', function() {
navigator.serviceWorker.register('/sw.js').then(function(registration) {
// Registration was successful
console.log('ServiceWorker registration successful with scope: ', registration.scope);
}, function(err) {
// registration failed :(
console.log('ServiceWorker registration failed: ', err);
});
});
}
- Installation
self.addEventListener('install', function(event) {
// 連上 cache api
// 將檔案存入
// 確認檔案已存入
});
- Activation
self.addEventListener('activate', event => {
// 開始攔截每次 api 的發送
// 但如果該頁面已經開啟了,註冊完後要重新整理
});
完成 activation 之後可以監聽
- Fetch(攔截 API 發送)
- Sync(背景程式執行)
- Push(推播功能)
使用 workbox 註冊(Google 提供的 PWA Libraries)
importScripts('https://storage.googleapis.com/workbox-cdn/releases/6.0.2/workbox-sw.js');
workbox.routing.registerRoute(
// 哪些檔案要 cache,也可以是 /.*\.css/ 的 css 檔案
// 接收字串、regex 和 function
new RegExp('.*\.js'),
// 這些檔案要執行什麼策略
workbox.strategies.networkFirst()
);
拿取資料策略
- NetworkFirst
- 資料常需更新
- 若沒有網路才用 cache
- 每次網路請求都把資料放到 cache
- StaleWhileRevalidate
- 資料不常需要更新
- 若 cache 找不到才用網路,並且將資料存入 cache
- NetworkOnly
- offline 完全不能用,像一些非 get api, 需要帶東西的
- 官網說 StaleWhileRevalidate 有處理好,不太需要用到這種策略
- CacheOnly
- 只吃 cache 資源
- precache 要設定好
所有策略參數
- 自定義策略名稱,ex: "image-cache"
- Cache 若過期的限制行為
- 陣列裡面放每個 cache 的生命週期
registerRoute(
({request}) => request.destination === 'image',
new CacheFirst({
// 自定義名稱
cacheName: 'image-cache',
plugins: [
new ExpirationPlugin({
// cache 多久
maxAgeSeconds: 7 * 24 * 60 * 60,
// 每次最多 cache 多少個 request
maxEntries: 10,
}),
]
})
);
Manifest json
- 描述 App 的特性
{
"short_name": "KB",
"name": "KenBlog",
"lang": "en",
"description": "A blog",
"start_url": "/",
"background_color": "#eef5ff",
"theme_color": "#285a99",
"dir": "ltr",
"display": "standalone",
"orientation": "any",
"scope": "/",
"icons": [
// ...各種大小的 icon
],
"prefer_related_applications": true,
"related_applications": [
{
"platform": "play",
"id": "com.google.samples.apps.iosched"
}
]
}
- start_url 開啟應用程式時的預設網址
- scope 應用程式的使用範圍
- name 應用程式的名稱
- icons 應用程式的圖示
- theme_color 應用程式的主要顏色
- background_color 啟動畫面(splash screen)的背景色
- short_name 應用程式的簡寫
- lang 主要語言
- description 應用程式的描述
- dir 文字書寫方向
- display 應用程式的顯示模式
- orientation 預設顯示的方向是直的或橫的
- prefer_related_applications 是否要推薦一個原生的app
- related_applications 推薦原生app的連結
跨瀏覽器及裝置問題
- iOS icons 吃不到 manifest json 的,可以再 head 裡面加
<link rel="apple-touch-icon" href="touch-icon-iphone.png">
<link rel="apple-touch-icon" sizes="152x152" href="touch-icon-ipad.png">
<link rel="apple-touch-icon" sizes="180x180" href="touch-icon-iphone-retina.png">
<link rel="apple-touch-icon" sizes="167x167" href="touch-icon-ipad-retina.png">
- iOS lunch screen 是一片白,可以再 head 裡面加
<meta name="apple-mobile-web-app-capable" content="yes" />
<link href="/apple_splash_2048.png" sizes="2048x2732" rel="apple-touch-startup-image" />
<link href="/apple_splash_1668.png" sizes="1668x2224" rel="apple-touch-startup-image" />
<link href="/apple_splash_1536.png" sizes="1536x2048" rel="apple-touch-startup-image" />
<link href="/apple_splash_1125.png" sizes="1125x2436" rel="apple-touch-startup-image" />
<link href="/apple_splash_1242.png" sizes="1242x2208" rel="apple-touch-startup-image" />
<link href="/apple_splash_750.png" sizes="750x1334" rel="apple-touch-startup-image" />
<link href="/apple_splash_640.png" sizes="640x1136" rel="apple-touch-startup-image" />
- 進入網頁後,iOS 不會自動跳出 install popup
// 偵測是否為 iOS
const isIos = () => {
const userAgent = window.navigator.userAgent.toLowerCase();
return /iphone|ipad|ipod/.test( userAgent );
}
// Display 模式是否為 standalone
const isInStandaloneMode = () => ('standalone' in window.navigator) && (window.navigator.standalone);
// Checks if should display install popup notification:
if (isIos() && !isInStandaloneMode()) {
this.setState({ showInstallMessage: true });
}
- 到背景或重新整理,iOS 會把所有資料都清空,可以使用 redux-persist 等套件去留存
- Standalone 模式不會有 navigation bar,要自己做往回按鈕
實作
- 加到 home screen
-
加上 manifest.json 到 public folder 內 --> 確認 manifest 內的資料都有帶上
-
將所有需要的 icons 也放到 public folder 內
-
header 裡加入該加入的 meta tag
<Head>
<title>{title}</title>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1" />
<link rel="manifest" href="/manifest.json" />
<link href="/icons/favicon-16x16-dunplab-manifest-28429.png" rel="icon" type="image/png" sizes="16x16" />
<link href="/icons/favicon-32x32-dunplab-manifest-28429.png" rel="icon" type="image/png" sizes="32x32" />
<link href="/icons/apple-icon-60x60-dunplab-manifest-28429.png" rel="apple-touch-icon" type="image/png" sizes="60x60" />
</Head>
相對路徑可能要稍微注意一下
- 修改 next.config.js(這邊使用 next-pwa 的套件)
const withPWA = require("next-pwa");
module.exports = withPWA(
{
pwa: {
// 在 build 的時候,webpack 會從 public 去找 manifest, 自動產出 workbox-*.js 跟 sw.js 兩個的檔案
dest: "public"
}
}
)
在 Next.js 9+ 版本以前,必須自己寫個 server 去 host manifest.json 這個檔案,但現在直接放在 public folder 裡就好了
-
npm run build
產生sw.js
,sw.js.map
,workbox-******.js
和workbox-*******.js.map
這幾個檔案 -
接下來去 devtool 的 lighthouse 去產一份報告,看 PWA 那部分有沒有成功,或是還缺什麼,若在 localhost 的話,應該只有 https 會是紅色的
- 接下來從手機去打開網址,打開選單後點擊加入主畫面
離線使用
- 可在 devtool 將網路關成 offline 再 refresh 一次
之後會再嘗試推播功能和其他偵測裝置的功能,目前先到這裡
tags:pwa
date:2021/2/16