前面的話
漸進式網絡應用 ( 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')))