一. What?
A new way to deliver amazing user experiences on the web.
一種提升 Web 用戶體驗的方式。除了 Web 天生的(便捷)體驗外,還有 3 個特點:Reliable, Fast, Engaging
- 可靠:在不確定的網路環境下,也能立即載入,而不會(因為斷網而)瞬間回到遠古時代
可靠指的是離線快取,斷網狀態走快取,保證離線場景仍然可用,service worker 配合 cache API 建立快取-代理機制
- 快速:迅速以絲滑的動畫作為交互反饋,而不存在掉幀卡頓的捲動
快速,只是強調交互反饋「感覺快」,與推崇的 Material Design 有關,並沒有真正的速度優勢(至少首屏沒有)
另外,得益於快取-代理機制,再次訪問時走本地快取會相當快
- 類 native:像設備原生 App 一樣,具有沈浸式的用戶體驗(即全屏)
除了全屏外,還有主屏圖示(讓 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,那可真蠢..)
[caption id="attachment_1610" align="alignnone" width="169"]
weather-pwa[/caption]
不太樂觀的消息:事實上,故意精心準備了用戶環境(官方正版 Chrome + 官方 Demo),在小米 4 上沒有自動彈出安裝 banner(可能是操作姿勢等條件不滿足,見上文),手動點擊「添加至主屏幕」,toast 添加成功,但主屏幕上啥也沒有……這就是提不起興趣手寫 Demo 試玩的原因(當然,主要原因是懶 ;))
四. 案例
-
餓了麼:奇怪,為什麼沒有感受到 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
暫無評論,快來發表你的看法吧