一.What?css
A new way to deliver amazing user experiences on the web.
一種提高Web用戶體驗的方式。除了Web天生的(便捷)體驗外,還有3個特色:Reliable, Fast, Engaginghtml
可靠:在不肯定的網絡環境下,也能當即加載,而不會(由於斷網而)瞬間回到遠古時代git
可靠指的是離線緩存,斷網狀態走緩存,保證離線場景仍然可用,service worker配合cache API創建緩存-代理機制github
快速:迅速以絲滑的動畫做爲交互反饋,而不存在掉幀卡頓的滾動web
快速,只是強調交互反饋「感受快」,與推崇的Material Design有關,並無真正的速度優點(至少首屏沒有)shell
另外,得益於緩存-代理機制,再次訪問時走本地緩存會至關快json
類native:像設備原生App同樣,具備沉浸式的用戶體驗(即全屏)promise
除了全屏外,還有主屏圖標(讓Web App在主屏幕有一席之地)和系統通知(「拉活」的能力),經過Web App Manifest配置來實現,依賴用戶環境支持瀏覽器
P.S.Engaging這個抽象形容詞真很差翻譯,這裏暫且取其實際意義,類native緩存
因此,表面上看,PWA的亮點分2部分:
(離線)緩存-代理機制
全屏,主屏圖標和系統通知等類native特性
緩存機制在Web App/SPA裏一點不新鮮,抽離出數據層以後,緩存順手就作了。但側重點不一樣,PWA的緩存機制偏向於靜態資源緩存,而Web App/SPA的緩存層多用來作動態內容緩存(上次的內容沒過時的話,再也不從新獲取動態部分,而是直接作客戶端渲染)
至於全屏,主屏圖標以及系統通知等類native特性,算是漸進加強中的加強,在支持的用戶環境是可用的(一些瀏覽器提供了支持,但更普遍的WebView環境在不久的未來可能仍是不行)。但這代表Web正在以漸進加強的方式走出PC時代,向着移動化發展
二.試玩
依賴環境
HTTPS
要求服務源必須是安全的,因此須要HTTPS環境。除了出於Web信息安全的考慮,想要推動HTTPS普及也是一個重要緣由,HTTPS做爲Web技術發展的必要基礎設施,對於拍照,錄音,push API等新特性,都須要得到用戶許可,而HTTPS是權限工做流的關鍵部分,必不可少
P.S.在permission.site可以體驗到HTTPS與HTTP環境在獲取用戶受權方面的差別
類native加強
經過引入Web App Manifest配置文件來實現類native加強,在支持PWA的瀏覽器生效(在不支持的環境最壞結果也就是多請求一個JSON文件):
<link rel="manifest" href="./manifest.json">
注意,有個比較類似的東西,叫Application Cache(HTML5特性,已過期),其manifest引入方式不一樣:
<html manifest="example.appcache"> ... </html>
由於兩者引入方式不一樣,因此Web App Manifest與Application Cache是不相干的,沒有歷史包袱的後顧之憂
P.S.Application Cache對SPA支持較好,對多頁應用則不適用,但存在不少問題,這裏很少作介紹
主屏圖標
Web App Manifest內容示例以下:
{ "short_name": "主屏顯示的應用名稱", "name": "安裝banner顯示的應用名稱", "icons": [ { "src": "launcher-icon-1x.png", "type": "image/png", "sizes": "48x48", "density": "1.0" }, { "src": "launcher-icon-2x.png", "type": "image/png", "sizes": "96x96", "density": "1.0" }, { "src": "launcher-icon-4x.png", "type": "image/png", "sizes": "192x192", "density": "1.0" } ], "start_url": "index.html?launcher=true" }
P.S.安裝banner是指一個相似於獲取權限的彈出面板,用戶能夠選擇添加至主屏幕或取消,知足必定條件的話,Chrome會自動彈出安裝banner,具體見Web App Install Banners
這樣理想狀況下咱們就擁有了主屏圖標,支持Web App Manifest的環境會選用最合適的(最接近48dp的)圖標
注意:index.html裏的內容應該是首屏渲染須要的最小化內容,爲了達到首屏當即加載的效果,能夠把帶loading和默認佔位圖的頁面框架做爲App Shell展現出來。另外,爲了達到秒開可用的首屏性能,Web App首屏性能優化其它常規手段在PWA也是推薦使用的,好比數據直出。如開篇所說,PWA並無天生的(首屏)性能優點,Web App適用的常規優化手段仍然是必要的
閃屏(Splash)
從主屏圖標進入,可定製的啓動過程顯示內容包括:標題,背景色和圖像。新配置項以下:
// 背景色 "background_color": "#2196F3", // 主題色,包括工具欄 "theme_color": "#2196F3",
圖像從icons中選取最接近128dp的圖像做爲閃屏,不支持動圖
另外,還能夠指定顯示模式和頁面方向:
// 全屏(隱藏瀏覽器的UI) "display": "standalone", // 顯示瀏覽器外殼,像打開書籤同樣 "display": "browser", // 橫屏 "orientation": "landscape"
P.S.關於閃屏的示例及更多信息請查看Adding a Splash Screen for Installed Web Apps in Chrome 47
特別注意:若是manifest.json文件有更新,這些改動不會自動生效,除非用戶從新添加應用到主屏
系統通知
與Web App Manifest無關,依賴Push API。簡單示例以下:
// service-worker.js self.addEventListener("push", function (event) { event.waitUntil( self.self.registration.showNotification("發佈新文章啦", { body: "有新文章發佈啦,點擊查看。" }) ); });
這裏很少作介紹(目前(2017/12/15)幾乎能夠認爲這個特性不存在),由於規範定義了API,但沒規定統一個push協議,因此各家瀏覽器的push機制不一樣,好比Chrome的GCM在咱們這片天空下就不可用
關於Push API的更多信息,請查看【Service Worker】消息推送功能「全軍覆沒」
緩存-代理
緩存分爲幾部分:
首屏靜態資源緩存(預緩存)
已訪問資源緩存(運行時緩存)
動態內容緩存(運行時緩存)
緩存是純數據操做(包括持久化),而service worker可以在後臺運行,尤爲適合處理這種與頁面及交互無關的事情,因此service worker與Cache API,Push API成了搭檔。但service worker自身也應該看作「加強」項,在不支持service worker的環境應該跳過緩存機制保證基本的頁面體驗,簡單的特徵檢測方案以下:
if ('serviceWorker' in navigator) { navigator.serviceWorker .register('./service-worker.js') .then(function() { console.log('Service Worker Registered'); }); }
service worker在install事件處理器完成包括App Shell在內的首屏靜態資源緩存:
// service-worker.js var cacheName = 'weatherPWA-step-6-1'; var filesToCache = [ // 入口URL '/', '/index.html', '/scripts/app.js', '/styles/inline.css', // App Shell須要的資源 '/images/ic_add_white_24px.svg', '/images/ic_refresh_white_24px.svg', // 內容展現可能用到的資源 '/images/clear.png', '/images/cloudy-scattered-showers.png', '/images/cloudy.png', '/images/fog.png', '/images/partly-cloudy.png', '/images/rain.png', '/images/scattered-showers.png', '/images/sleet.png', '/images/snow.png', '/images/thunderstorm.png', '/images/wind.png' ]; self.addEventListener('install', function(e) { console.log('[ServiceWorker] Install'); e.waitUntil( caches.open(cacheName).then(function(cache) { console.log('[ServiceWorker] Caching app shell'); //! 只要有一個失敗就不接着作下一個了 return cache.addAll(filesToCache); }) ); });
固然,還須要對緩存作基本的版本控制:
// service-worker.js self.addEventListener('activate', function(e) { console.log('[ServiceWorker] Activate'); e.waitUntil( caches.keys().then(function(keyList) { return Promise.all(keyList.map(function(key) { // 覺得cacheName爲cache key,若是存在舊的緩存,刪除掉 if (key !== cacheName) { console.log('[ServiceWorker] Removing old cache', key); return caches.delete(key); } })); }) ); // 要求當即激活service worker,避免邊界case return self.clients.claim(); });
P.S.邊界case指的是某些狀況下service worker沒法馬上恢復激活態,致使不走緩存。爲了屏蔽這些邊界case,推薦使用GoogleChromeLabs/sw-precache幫助處理緩存控制問題(包括過時,更新策略等等)
緩存有了,接下來實現代理部分,攔截請求,並把緩存內容做爲響應:
// service-worker.js // 攔截請求 self.addEventListener('fetch', function(event) { console.log('[ServiceWorker] Fetch', e.request.url); // 自定義響應內容 e.respondWith( // 查找緩存,沒有才請求 caches.match(e.request).then(function(response) { return response || fetch(e.request); }) ); });
到這裏基本的緩存-代理機制就準備好了,咱們作了這些事情:
按資源列表預先緩存靜態資源
攔截請求
把緩存內容做爲響應給過去
有3個注意事項:
瀏覽器緩存可能會影響緩存更新,因此install事件處理器中的請求不會走緩存,而是直接進入網絡
註銷service worker不會清掉緩存,cache key不變的話,以後還會拿到舊的緩存內容
默認新註冊的service worker在頁面從新載入以後纔會生效,除非作特殊處理
另外,咱們的簡版實現還存在一些問題,例如:
緩存版本控制依賴一個靜態的cache key,每次更新service-worker.js都要修改這個key
一旦cache key有變化,會抹掉全部緩存,從新請求一遍,對於靜態資源有些浪費
缺乏運行時緩存,資源列表不夠靈活,指望更強大的邊訪問邊緩存
第1個問題沒什麼太好的辦法,第2個問題能夠經過細分資源類型來緩解,例如:
// Shorthand identifier mapped to specific versioned cache. var CURRENT_CACHES = { font: 'font-cache-v' + FONT_CACHE_VERSION, css: 'css-cache-v' + CSS_CACHE_VERSION, img: 'img-cache-v' + IMG_CACHE_VERSION };
經過更細粒度的版本控制,能在必定程度上下降強制更新緩存的成本,固然,緩存層下面還有HTTP Cache兜底,緩存更新成本不是很是關鍵
至於運行時緩存,實際上只須要再作最後一小步就行了:
沒命中緩存的話,請求資源並緩存
具體以下:
// 查找緩存,沒有才請求 caches.match(e.request).then(function(response) { return response || fetch(e.request).then(function(res) { return caches.open(dataCacheName).then(function(cache) { // 並緩存起來 cache.put(e.request.url, res.clone()); return res; ) }) })
另外,還能夠根據資源類型及場景要求,針對性的選用合適的緩存策略,例如:
// service-worker.js self.addEventListener('fetch', function(e) { console.log('[Service Worker] Fetch', e.request.url); var dataUrl = 'https://cache.domain.com/fresh/'; // 策略1:有實時性要求的資源,請求優先,fetch then cache if (e.request.url.indexOf(dataUrl) > -1) { e.respondWith( caches.open(dataCacheName).then(function(cache) { return fetch(e.request).then(function(response){ cache.put(e.request.url, response.clone()); return response; }); }) ); } else { // 策略2:通常資源,緩存優先,cache falling back to fetch } });
P.S.更多緩存策略,見參考資料部分
三.Demo
官方Demo:Weather PWA,可能沒法正常訪問
搬運Demo(把官方Demo挪到github pages):https://ayqy.github.io/pwa/demo/weather-pwa/index.html
P.S.github pages很是適合用做試驗田,穩定可靠的HTTPS,發佈內容沒有任何限制能夠隨便折騰,之後的博客Demo都會逐步遷移過去(以前一直放在本身的FTP,那可真蠢..)
weather-pwa
不太樂觀的消息:事實上,故意精心準備了用戶環境(官方正版Chrome + 官方Demo),在小米4上沒有自動彈出安裝banner(多是操做姿式等條件不知足,見上文),手動點擊「添加至主屏幕」,toast添加成功,但主屏幕上啥也沒有……這就是提不起興趣手寫Demo試玩的緣由(固然,主要緣由是懶;))
四.案例
阿里巴巴國際站
AliExpress
餓了麼:奇怪,爲何沒有感覺到Cache的做用呢
注意,隱身模式可能會致使阿里巴巴國際站的service worker拋以下錯誤:
Uncaught (in promise) DOMException: Quota exceeded.
正常環境可正常體驗
P.S.更多案例,請查看Case Studies | Web | Google | Developers
五.應用場景
簡言之,PWA算是Web App的升級版,主要亮點是類native支持。以漸進加強的方式,不須要過高成本就能完成Web App到PWA的「升級」,讓部分用戶(支持PWA的環境)得到更快(緩存)更便捷(主屏圖標)的類native體驗(全屏)
那麼具體應用場景分如下幾種:
緩存能帶來明顯收益的Web App
指望具備離線能力,或類native體驗,或者單純只是想要個主屏圖標的Web應用
指望蹭個技術熱點/協助推進其發展的Web應用或瀏覽器供應商
無論應用場景,話說回來,正如zxx某篇關於緩存(仍是worker?)的文章所說,這麼點兒成本就能讓頁面得到離線能力,真切看到緩存帶來的收益,何樂而不爲呢?
另外,Angular,React,Vue等主流框架都提供了PWA腳手架,具體請查看The Ultimate Guide to Progressive Web Applications
參考資料
The offline cookbook:緩存策略圖解,好東西,待翻譯,就着ServiceWorker Cookbook一塊兒看
如何看待 Progressive Web Apps 的發展前景?
Your First Progressive Web App:官方教程
A Beginner’s Guide To Progressive Web Apps:比較全面的入門指南
改造你的網站,變身PWA:原文Retrofit Your Website as a Progressive Web App