前面的話
漸進式網路應用 ( Progressive Web Apps ),即我們所熟知的 PWA,是 Google 提出的用前沿的 Web 技術為網頁提供 App 般使用體驗的一系列方案。PWA 本質上是 Web App,藉助一些新技術也具備了 Native App 的一些特性。本文將詳細介紹針對現有網站的PWA升級
效果演示
以前端小站xiaohuochai.cc的PWA效果做演示,github移步至此
【新增到桌面】
【離線快取】
由於手機錄屏選擇無法進行離線錄製,改由模擬器模擬離線效果
概述
PWA 的主要特點包括下面三點:
1、可靠 – 即使在不穩定的網路環境下,也能瞬間載入並展現
2、體驗 – 快速響應,並且有平滑的動畫響應使用者的操作
3、粘性 – 像裝置上的原生應用,具有沉浸式的使用者體驗,使用者可以新增到桌面
主要功能包括站點可新增至主螢幕、全屏方式執行、支援離線快取、訊息推送等
【PRPL模式】
“PRPL”(讀作 “purple”)是 Google 的工程師提出的一種 web 應用架構模式,它旨在利用現代 web 平臺的新技術以大幅優化移動 web 的效能與體驗,對如何組織與設計高效能的 PWA 系統提供了一種高層次的抽象
“PRPL”實際上是 Push/Preload、Render、Precache、Lazy-Load 的縮寫
1、PUSH/PRELOAD,推送/預載入初始 URL 路由所需的關鍵資源
2、RENDER,渲染初始路由,儘快讓應用可被互動
3、PRE-CACHE,用 Service Worker 預快取剩下的路由
4、LAZY-LOAD 按需懶載入、懶例項化剩下的路由
【Service workers】
Service Workers 是谷歌 chrome 團隊提出並大力推廣的一項 web 技術。在 2015 年,它加入到 W3C 標準,進入草案階段
PWA 的關鍵在於 Service Workers 。就其核心來說,Service Workers 只是後臺執行的 worker 指令碼。它們是用 JavaScript 編寫的,只需短短几行程式碼,它們便可使開發者能夠攔截網路請求,處理推送訊息並執行許多其他任務
Service Worker 中用到的一些全域性變數:
self: 表示 Service Worker 作用域, 也是全域性變數
caches: 表示快取
skipWaiting: 表示強制當前處在 waiting 狀態的指令碼進入 activate 狀態
clients: 表示 Service Worker 接管的頁面
Service Worker 的工作機制大致如下:使用者訪問一個具有 Service Worker 的頁面,瀏覽器就會下載這個 Service Worker 並嘗試安裝、啟用。一旦啟用,Service Worker 就到後臺開始工作。接下來使用者訪問這個頁面或者每隔一個時段瀏覽器都會下載這個 Service Worker,如果監測到 Service Worker 有更新,就會重新安裝並啟用新的 Service Worker,同時 revoke 掉舊的 Service Worker,這就是 SW 的生命週期
因為 Service Worker 有著最近的許可權接觸資料,因此 Service Worker 只能被安裝在 HTTPS 加密的頁面中,雖然無形當中提高了 PWA 的門檻,不過也是為了安全做考慮
離線快取
下面來通過service worker實現離線快取
一般地,通過sw-precache-webpack-plugin外掛來實現動態生成service worker檔案的效果
不過,首先要在index.html中引用service worker
<script> (function() { if(`serviceWorker` in navigator) { navigator.serviceWorker.register(`/service-worker.js`); } })() </script>
【SPA】
通過create-react-app生成的react SPA應用預設就進行了sw-precache-webpack-plugin的設定。但是,其只對靜態資源進行了設定
如果是介面資源,則一般的處理是優先通過網路訪問,如果網路不通,再通過service worker的快取進行訪問
webpack.config.prod.js檔案的配置如下
const SWPrecacheWebpackPlugin = require(`sw-precache-webpack-plugin`); new SWPrecacheWebpackPlugin({ // By default, a cache-busting query parameter is appended to requests // used to populate the caches, to ensure the responses are fresh. // If a URL is already hashed by Webpack, then there is no concern // about it being stale, and the cache-busting can be skipped. dontCacheBustUrlsMatching: /.w{8}./, filename: `service-worker.js`, logger(message) { if (message.indexOf(`Total precache size is`) === 0) { // This message occurs for every build and is a bit too noisy. return; } if (message.indexOf(`Skipping static resource`) === 0) { // This message obscures real errors so we ignore it. // https://github.com/facebookincubator/create-react-app/issues/2612 return; } console.log(message); }, minify: true, // For unknown URLs, fallback to the index page navigateFallback: publicUrl + `/index.html`, // Ignores URLs starting from /__ (useful for Firebase): // https://github.com/facebookincubator/create-react-app/issues/2237#issuecomment-302693219 navigateFallbackWhitelist: [/^(?!/__).*/], // Don`t precache sourcemaps (they`re large) and build asset manifest: staticFileGlobsIgnorePatterns: [/.map$/, /asset-manifest.json$/], runtimeCaching: [{ urlPattern: `/`, handler: `networkFirst` }, { urlPattern: //api/, handler: `networkFirst` } ] })
【SSR】
如果是伺服器端渲染的應用,則配置基本類似。但由於無法使用代理,則需要設定網站實際路徑,且由於靜態資源已經存到CDN,則快取不再通過service worker處理
配置如下
new SWPrecacheWebpackPlugin({ dontCacheBustUrlsMatching: /.w{8}./, filename: `service-worker.js`, logger(message) { if (message.indexOf(`Total precache size is`) === 0) { return; } if (message.indexOf(`Skipping static resource`) === 0) { return; } console.log(message); }, navigateFallback: `https://www.xiaohuochai.cc`, minify: true, navigateFallbackWhitelist: [/^(?!/__).*/], dontCacheBustUrlsMatching: /./, staticFileGlobsIgnorePatterns: [/.map$/, /.json$/], runtimeCaching: [{ urlPattern: `/`, handler: `networkFirst` }, { urlPattern: //(posts|categories|users|likes|comments)/, handler: `networkFirst` }, ] }) ]
新增到螢幕
沒人願意多此一舉地在移動裝置鍵盤上輸入長長的網址。通過新增到螢幕的功能,使用者可以像從應用商店安裝本機應用那樣,選擇為其裝置新增一個快捷連結,並且過程要順暢得多
【配置項說明】
使用manifest.json檔案來實現新增到螢幕的功能,下面是該檔案內的配置項
short_name: 應用展示的名字 icons: 定義不同尺寸的應用圖示 start_url: 定義桌面啟動的 URL description: 應用描述 display: 定義應用的顯示方式,有 4 種顯示方式,分別為: fullscreen: 全屏 standalone: 應用 minimal-ui: 類似於應用模式,但比應用模式多一些系統導航控制元素,但又不同於瀏覽器模式 browser: 瀏覽器模式,預設值 name: 應用名稱 orientation: 定義預設應用顯示方向,豎屏、橫屏 prefer_related_applications: 是否設定對應移動應用,預設為 false related_applications: 獲取移動應用的方式 background_color: 應用載入之前的背景色,用於應用啟動時的過渡 theme_color: 定義應用預設的主題色 dir: 文字方向,3 個值可選 ltr(left-to-right), rtl(right-to-left) 和 auto(瀏覽器判斷),預設為 auto lang: 語言 scope: 定義應用模式下的路徑範圍,超出範圍會以瀏覽器方式顯示
下面是一份常規的manifest.json檔案的配置
{ "name": "小火柴的前端小站", "short_name": "前端小站", "start_url": "/", "display": "standalone", "description": "", "theme_color": "#fff", "background_color": "#d8d8d8", "icons": [{ "src": "./logo_32.png", "sizes": "32x32", "type": "image/png" }, { "src": "./logo_48.png", "sizes": "48x48", "type": "image/png" }, { "src": "./logo_96.png", "sizes": "96x96", "type": "image/png" }, { "src": "./logo_144.png", "sizes": "144x144", "type": "image/png" }, { "src": "./logo_192.png", "sizes": "192x192", "type": "image/png" }, { "src": "./logo_256.png", "sizes": "256x256", "type": "image/png" } ] }
【注意事項】
1、在 Chrome 上首選使用 short_name
,如果存在,則優先於 name 欄位使用
2、圖示的型別最好是png,,且存在144px的尺寸,否則會得到如下提示
Site cannot be installed: a 144px square PNG icon is required, but no supplied icon meets this requirement
3、start_url表示專案啟動路徑
如果是`/`,則啟動路徑為
localhost:3000/
如果是`/index.html`,則啟動路徑為
localhost:3000/index.html
所以,最好填寫`/`
【HTML引用】
在HTML文件中通過link標籤來引用manifest.json檔案
<link rel="manifest" href="/manifest.json">
要特別注意manifest檔案路徑問題,要將該檔案放到靜態資源目錄下,否則,會找不到該檔案,控制檯顯示如下提示
Manifest is not valid JSON. Line: 1, column: 1, Unexpected token
如果index.html也位於靜態資源目錄,則設定如下
<link rel="manifest" href="/manifest.json">
如果index.html位於根目錄,而靜態資源目錄為static,則設定如下
<link rel="manifest" href="/static/manifest.json" />
【meta標籤】
為了更好地SEO,需要通過meta標籤設定theme-color
<meta name="theme-color" content="#fff"/>
【SSR】
如果是伺服器端配置,需要在server.js檔案中配置manifest.json、logo、icon等檔案的靜態路徑
app.use(express.static(path.join(__dirname, `dist`))) app.use(`/manifest.json`, express.static(path.join(__dirname, `manifest.json`))) app.use(`/logo`, express.static(path.join(__dirname, `logo`))) app.use(`/service-worker.js`, express.static(path.join(__dirname, `dist/service-worker.js`)))