漸進式網絡應用 ( Progressive Web Apps ),即咱們所熟知的 PWA,是 Google 提出的用前沿的 Web 技術爲網頁提供 App 般使用體驗的一系列方案。PWA 本質上是 Web App,藉助一些新技術也具有了 Native App 的一些特性。本文將詳細介紹針對現有網站的PWA升級html
之前端小站xiaohuochai.cc的PWA效果作演示,github移步至此前端
【添加到桌面】react
【離線緩存】webpack
因爲手機錄屏選擇沒法進行離線錄製,改由模擬器模擬離線效果git
PWA 的主要特色包括下面三點:github
一、可靠 - 即便在不穩定的網絡環境下,也能瞬間加載並展示web
二、體驗 - 快速響應,而且有平滑的動畫響應用戶的操做chrome
三、粘性 - 像設備上的原生應用,具備沉浸式的用戶體驗,用戶能夠添加到桌面express
主要功能包括站點可添加至主屏幕、全屏方式運行、支持離線緩存、消息推送等json
【PRPL模式】
「PRPL」(讀做 「purple」)是 Google 的工程師提出的一種 web 應用架構模式,它旨在利用現代 web 平臺的新技術以大幅優化移動 web 的性能與體驗,對如何組織與設計高性能的 PWA 系統提供了一種高層次的抽象
「PRPL」其實是 Push/Preload、Render、Precache、Lazy-Load 的縮寫
一、PUSH/PRELOAD,推送/預加載初始 URL 路由所需的關鍵資源
二、RENDER,渲染初始路由,儘快讓應用可被交互
三、PRE-CACHE,用 Service Worker 預緩存剩下的路由
四、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" } ] }
【注意事項】
一、在 Chrome 上首選使用 short_name
,若是存在,則優先於 name 字段使用
二、圖標的類型最好是png,,且存在144px的尺寸,不然會獲得以下提示
Site cannot be installed: a 144px square PNG icon is required, but no supplied icon meets this requirement
三、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')))