寫於 2017.08.15html
談起PWA,許多人可能還只停留在「瞭解」的層面,比較少在實踐中真正地嘗試過,更多的僅僅是對着網上的教程和例子大概玩過。然而,網絡上的例子可能是簡單的demo,鮮有與真正的開發相結合,例如和webpack的工程化結合。這篇文章將會從一個webpack plugin出發,談一談如何使用這個名爲offline-plugin
的webpack插件輕鬆實現PWA。android
因爲PWA相關的文章太多,因此本文再也不對「什麼是PWA」,「PWA的生命週期」等基礎內容再次贅述。webpack
offline-plugin
相關連接:git
service-worker.js
PWA的核心可謂是service-worker
(之後簡稱SW),任何一個PWA都有且只有一個service-worker.js
文件,用於爲SW添加資源列表,進行註冊、激活等生命週期操做。可是在webpack構建的項目中,生成一個service-worker.js
可能會面臨兩個較大的問題:github
看到這你可能已經想到,萬能的webpack社區是否已經提供了相應的plugin來幫咱們自動處理這些事情呢?答案是確定的。除了官方推薦的sw-precache-webpack-plugin以外,還有咱們今天的主角offline-plugin。web
相比與sw-precache-webpack-plugin,我的認爲offline-plugin具備以下優勢:chrome
manifest
文件。npm install offline-plugin [--save-dev]
複製代碼
第一步,進入webpack.config
:npm
// webpack.config.js example
var OfflinePlugin = require('offline-plugin');
module.exports = {
// ...
plugins: [
// ... other plugins
// it's always better if OfflinePlugin is the last plugin added new OfflinePlugin() ] // ... } 複製代碼
第二步,把runtime
添加到你的入口js文件當中:json
require('offline-plugin/runtime').install();
複製代碼
ES6/Babel/TypeScript數組
import * as OfflinePluginRuntime from 'offline-plugin/runtime';
OfflinePluginRuntime.install();
複製代碼
通過上面的步驟,offline-plugin
已經集成到項目之中,經過webpack構建便可。
前面說過,offline-plugin
支持細緻的配置,以知足不一樣的需求。下面將介紹幾個比較經常使用的配置項,方便你們進一步使用。
告訴插件應該緩存什麼東西,並以何種方式進行緩存 all
: 意味着全部webpack構建出來的資源,以及在externals
選項中的資源都會被緩存。 Object
: 包含三個數組或正則的配置對象(main
, additional
, optional
),它們都是可選的,且默認爲空。 默認:all
。
externals: Array 容許開發者指定一些外部資源(好比CDN引用,或者不是經過webpack生成的資源)。配合Caches
的additional
項,可以實現緩存外部資源的功能。
默認:null
舉例:['fonts/roboto.woff']
ServiceWorker: Object | null | false 該對象包含多個配置項,這裏僅列舉最經常使用的。
events
:布爾值。容許runtime接受來自sw的消息,默認值爲false。 navigateFallbackURL
:當一個URL請求從緩存或網絡都沒法被獲取時,將會重定向到該選項所指向的URL。
AppCache: Object | null | false
offline-plugin
默認支持AppCache
,可是AppCache
草案已經被web標準所廢棄,不建議使用。 可是因爲仍然有部分瀏覽器支持,因此插件默認提供這個功能。
上一節介紹了offline-plugin
在webpack當中的配置,這一節將介紹runtime的一些用法。 若要使offline-plugin
生效,用戶必須在入口js文件中經過runtime進行初始化操做:
// 經過AMD方式
require('offline-plugin/runtime').install();
// 或者經過ES6/Babel/TypeScript方式
import * as OfflinePluginRuntime from 'offline-plugin/runtime';
OfflinePluginRuntime.install();
複製代碼
OfflinePluginRuntime
對象提供了下列三個方法:
install(options: Object) 開啓ServiceWorker/AppCache的安裝流程。這個方法是安全的,而且必須在頁面初始化的時候就被調用。另外請勿把它放在任何的條件語句以內。(這句話不全對,在後面的降級方案裏面會詳細介紹)
applyUpdate() 接受當前所安裝的sw的更新信息。
update() 檢查新版本的ServiceWorker/AppCache的更新信息。
runtime.install()
方法接受一個配置對象參數,用於處理sw各個生命週期裏面的事件:
onInstalled 當ServiceWorker/AppCache被install時執行,可用於展現「APP已經支持離線訪問」。
onUpdating AppCache不支持該方法 當更新信息被獲取且瀏覽器正在進行資源更新時觸發。在這個時刻,一些資源正在被下載。
onUpdateReady 當onUpdating
事件完成時觸發。這時,全部資源都已經下載完畢。 經過調用runtime.applyUpdate()
方法來觸發更新。
onUpdateFailed 當onUpdating
事件由於某些緣由失敗時觸發。 這時沒有任何資源被下載,同時全部的資源更新進程都應該被取消或跳過。
onUpdated 當更新被接受時觸發。
當某些時候咱們須要撤掉sw進行降級的時候,咱們須要主動註銷sw。然而offline-plugin
默認沒有提供註銷sw的unregister()
方法,因此咱們須要本身實現。
其實要主動註銷sw很是簡單,咱們能夠直接調用ServiceWorkerContainer.getRegistrations()
方法來拿到registration
實例,而後調用registration.unregister()
方法便可,具體代碼以下:
if ('serviceWorker' in navigator) {
navigator.serviceWorker.getRegistration().then((registration) => {
registration && registration.unregister().then((boolean) => {
boolean ? alert('註銷成功') : alert('註銷失敗')
});
})
}
複製代碼
在調用該方法後,sw已經被註銷,刷新一下頁面就能看到資源是從新從網絡獲取的了。
在真實的生產環境中,咱們能夠經過調用接口,來決定是否使用降級方案:
fetch(URL).then((switch) => {
if (switch) {
OfflinePluginRuntime.install()
} else {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.getRegistration().then((registration) => {
registration && registration.unregister().then((boolean) => {
boolean ? alert('註銷成功') : alert('註銷失敗')
})
})
}
}
})
複製代碼
在具體實踐中,遇到一個比較大的坑,就是sw.js
文件的更新。
在service worker的設計中,瀏覽器每一次加載站點的URL,都會從新請求一遍sw.js
。若發現這一次的sw.js
內容和上一次的不同,就會斷定爲資源更新,從新觸發sw的生命週期。然而,sw.js
也是一個普通的js資源文件,會默認使用服務器設置的expired時間,也就是它的max-age
。在理解了service worker的設計後,咱們不難發現,sw.js
的max-age
應該儘量短,以便瀏覽器可以及時更新資源列表。
這也是我在研究階段直接使用http-server
時所發現的問題。後來在官方的例子中,我發現npm script
裏面是這麼寫的:
"start": "http-server ./dist -p 7474 -c no-cache"
複製代碼
直接指定了全部資源都不使用緩存,這一點值得咱們注意。
另外,webpack-dev-server
裏沒法正常使用offline-plugin
,由於它須要具體的文件去生成sw.js
,可是經過webpack-dev-server
構建的項目,其文件是存放在內存中的,因此沒法和offline-plugin
正常搭配使用。建議僅在生產模式內使用offline-plugin
。
手機瀏覽器都提供了「添加到主屏」的功能,但普通的網站添加到主屏,僅僅是把網站的書籤放到桌面。若是要想把網站以PWA的形式添加到主屏,咱們須要一個manifest.json文件:
{
"name": "offline-plugin",
"icons": [
{
"src": "/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#181743",
"background_color": "#181743",
"start_url": "/",
"display": "standalone"
}
複製代碼
而後,把這個manifest.json
和其餘靜態資源一併打包到網站根目錄便可:
示例地址:
打開chrome開發者工具,進入到Application
一列,選擇Manifest
,就能夠看到效果了:
截止到目前(2017年8月15日),我所使用的iOS10.3.2版本的iPhone7手機,已經支持PWA了,效果以下: 通過查閱大量的資料,到目前爲止,iOS並不支持PWA,可是能夠經過在html裏面添加幾個標籤,實現web頁面和原生APP類似的體驗效果:
應用圖標:
<link rel="apple-touch-icon" href=「/custom_icon.png"> 啓動畫面: <link rel="apple-touch-startup-image" href="/launch.png"> 應用名稱: <meta name="apple-mobile-web-app-title" content="AppTitle"> 全屏效果: <meta name="apple-mobile-web-app-capable" content="yes"> 設置狀態欄顏色: <meta name="apple-mobile-web-app-status-bar-style" content="black"> 複製代碼
使用safari打開
添加到主屏後打開
離線後從主屏打開
打開任務管理器
能夠看到,PWA不管從表現仍是功能,都像一個獨立的APP那樣存在。
原來一直覺得蘋果對PWA支持很差,但經過此次實踐,能夠知道其實PWA也取得了極大的推動,開發者們能夠開心地搭建本身的PWA啦! 結論不能下太早。。。