導讀:2016年,Google提出了PWA,志在增強Web體驗??娠@著提高加載速度、可離線工作、可被添加至主屏、全屏執行、推送通知消息……等等這些特性可使Web應用漸進式地變成App,甚至與APP相匹敵。這一系列特性背后有哪些核心關鍵技術支撐,本文將為您一一分析,解開它的神秘面紗。
近年來,Web 應用在整個軟件與互聯網行業承載的責任越來越重,軟件復雜度和維護成本越來越高,Web 技術,尤其是 Web 客戶端技術,迎來了爆發式的發展。
包括但不限于基于 Node.js 的前端工程化方案;諸如 Webpack、Rollup 這樣的打包工具;Babel、PostCSS 這樣的轉譯工具;TypeScript、Elm 這樣轉譯至 JavaScript 的編程語言;React、Angular、Vue 這樣面向現代 Web 應用需求的前端框架及其生態,也涌現出了像同構 JavaScript與通用 JavaScript 應用這樣將服務器端渲染(Server-side Rendering)與單頁面應用模型(Single-page App)結合的 Web 應用架構方式,可以說是百花齊放。
但是,Web 應用在移動時代并沒有達到其在桌面設備上流行的程度。究其原因,盡管上述的各種方案已經充分利用了現有的 JavaScript 計算能力、CSS 布局能力、HTTP 緩存與瀏覽器 API 對當代基于 Ajax 與響應式設計的 Web 應用模型的性能與體驗帶來了工程角度的巨大突破,我們仍然無法在不借助原生程序輔助瀏覽器的前提下突破 Web 平臺本身對 Web 應用固有的桎梏:客戶端軟件(即網頁)需要下載所帶來的網絡延遲;與 Web 應用依賴瀏覽器作為入口所帶來的體驗問題。
圖1 Web與原生應用在移動平臺上的使用時長對比(圖片來源:Google)
在桌面設備上,由于網絡條件穩定,屏幕尺寸充分,交互方式趨向于多任務,這兩點造成的負面影響對比 Web 應用免于安裝、隨叫隨到、無需更新等優點,瑕不掩瑜。但是在移動時代,脆弱的網絡連接與全新的人機交互方式使得這兩個問題被無限放大,嚴重制約了 Web 應用在移動平臺的發展。在用戶眼里,原生應用不會出現「白屏」,清一色都擺在主屏幕上;而 Web 應用則是瀏覽器這個應用中的應用,使用起來并不方便,而且加載也比原生應用要慢。
Progressive Web Apps(以下簡稱 PWA)以及構成 PWA 的一系列關鍵技術的出現,終于讓我們看到了徹底解決這兩個平臺級別問題的曙光:能夠顯著提高應用加載速度、甚至讓 Web 應用可以在離線環境使用的 Service Worker 與 Cache Storage;用于描述 Web 應用元數據(metadata)、讓 Web 應用能夠像原生應用一樣被添加到主屏、全屏執行的 Web App Manifest;以及進一步提高 Web 應用與操作系統集成能力,讓 Web 應用能在未被激活時發起推送通知的 Push API 與 Notification API 等等。
將這些技術組合在一起會是怎樣的效果呢?「印度阿里巴巴」 —— Flipkart 在 2015 年一度關閉了自己的移動端網站,卻在年底發布了現在為人津津樂道的 PWA 案例 FlipKart Lite,成為世界上個支撐大規模業務的 PWA。發布的一周后它就亮相于 Chrome Dev Summit 2015 上,我當時就被驚艷到了。為了方便各媒介上的讀者觀看,我做了幾幅圖方便給大家介紹:
圖2 PWA案例FlipKart Lite展示(圖片來源:Hux & Medium.com)
當瀏覽器發現用戶需要 Flipkart Lite 時,它就會提示用戶「嘿,你可以把它添加至主屏哦」(用戶也可以手動添加)。這樣,Flipkart Lite 就會像原生應用一樣在主屏上留下一個自定義的 icon 作為入口;與一般的書簽不同,當用戶點擊 icon 時,Flipkat Lite 將直接全屏打開,不再受困于瀏覽器的 UI 中,而且有自己的啟動屏效果。
圖3 FlipKart Lite啟動效果展示(圖片來源: Hux&Medium.com)
更強大的是,在無法訪問網絡時,Flipkart Lite 可以像原生應用一樣照常執行,還會很騷氣的變成黑白色;不但如此,曾經訪問過的商品都會被緩存下來得以在離線時繼續訪問。在商品降價、促銷等時刻,Flipkart Lite 會像原生應用一樣發起推送通知,吸引用戶回到應用。
無需擔心網絡延遲;有著獨立入口與獨立的保活機制。之前兩個問題的一并解決,宣告著 Web 應用在移動設備上的浴火重生:滿足 PWA 模型的 Web 應用,將逐漸成為移動操作系統的一等公民,并將向原生應用發起挑戰與「復仇」。
更令我興奮的是,就在今年 11 月的 Chrome Dev Summit 2016 上,Chrome 的工程 VP Darin Fisher 介紹了 Chrome 團隊正在做的一些實驗:把「添加至主屏」重命名為「安裝」,被安裝的 PWA 不再僅以 widget 的形式顯示在桌面上,而是真正做到與所有原生應用平級,一樣被收納進應用抽屜(App Drawer)里,一樣出現在系統設置中。
圖4 Flipkart Lite“安裝”展示(圖片來源: Hux&@adityapunjani)
圖中從左到右分別為:類似原生應用的安裝界面;被收納在應用抽屜里的 Flipkart Lite 與 Hux Blog;設置界面中并列出現的 Flipkart 原生應用與 Flipkart Lite PWA (可以看到 PWA 巨大的體積優勢)
我相信,PWA 模型將繼約 20 年前橫空出世的 Ajax 與約 10 年前風靡移動互聯網的響應式設計之后,掀起 Web 應用模型的第三次根本性革命,將 Web 應用帶進一個全新的時代。
Web App Manifest,即通過一個清單文件向瀏覽器暴露 Web 應用的元數據,包括名字、icon 的 URL 等,以備瀏覽器使用,比如在添加至主屏或推送通知時暴露給操作系統,從而增強 Web 應用與操作系統的集成能力。
讓 Web 應用在移動設備上的體驗更接近原生應用的嘗試其實早在 2008 年的 iOS 1.1.3 與 iOS 2.1.0 時就開始了,它們分別為 Web 應用增加了對自定義 icon 和全屏打開的支持。
圖5 2008年iOS系統對Web應用在移動設備上獲得原生應用體驗的嘗試(圖片來源:appleinsider.com)
但是很快,隨著越來越多的私有平臺通過
/ 標簽來為 Web 應用添加「私貨」,“ 很快就被塞滿了:
<meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-status-bar-style" content="black"> <meta name="apple-mobile-web-app-title" content="Lighten"> <meta name="mobile-web-app-capable" content="yes"> <mate name="theme-color" content="#000000"> <link rel="apple-touch-icon-precomposed" sizes="144x144" href="images/touch/apple-touch-icon-144x144-precomposed.png"> <link rel="apple-touch-icon-precomposed" sizes="114x114" href="images/touch/apple-touch-icon-114x114-precomposed.png"> <link rel="apple-touch-icon-precomposed" sizes="72x72" href="images/touch/apple-touch-icon-72x72-precomposed.png"> <link rel="apple-touch-icon-precomposed" href="images/touch/apple-touch-icon-57x57-precomposed.png"> <link rel="shortcut icon" sizes="196x196" href="images/touch/touch-icon-196x196.png"> <meta name="msapplication-TileImage" content="images/touch/ms-touch-icon-144x144-precomposed.png"> <meta name="msapplication-TileColor" content="#3372DF"> <link rel="shortcut icon" href="images/touch/touch-icon-57x57.png">
顯然,這種做法并不優雅:分散又重復的元數據定義多余且難以維持同步,與 html 耦合在一起也加重了瀏覽器檢查元數據未來變動的成本。與此同時,社區里開始出現使用 manifest 文件以中心化地描述元數據的方案,比如 Chrome Extension、 Chrome Hosted Web Apps (2010) 與 Firefox OS App Manifest (2011) 使用 JSON;Cordova 與 Windows Pinned Site 使用 XML;
2013 年,W3C WebApps 工作組開始對基于 JSON 的 Manifest 進行標準化,于同年年底發布份公開 Working Draft,并逐漸演化成為今天的 W3C Web App Manifest:
{
"short_name": "Manifest Sample",
"name": "Web Application Manifest Sample",
"icons": [{
"src": "launcher-icon-2x.png",
"sizes": "96x96",
"type": "image/png" }],
"scope": "/sample/",
"start_url": "/sample/index.html",
"display": "standalone",
"orientation": "landscape" "theme_color": "#000",
"background_color": "#fff",
}
<link rel="manifest" href="/manifest.json">
諸如 name、icons、display 都是我們比較熟悉的,而大部分新增的成員則為 Web 應用帶來了一系列以前 Web 應用想做卻做不到(或在之前只能靠 hack)的新特性:
scope:定義了 Web 應用的瀏覽作用域,比如作用域外的 URL 就會打開瀏覽器而不會在當前 PWA 里繼續瀏覽。
start_url:定義了一個 PWA 的入口頁面。比如說你添加 Hux Blog 的任何一個文章到主屏,從主屏打開時都會訪問Hux Blog 的主頁。
orientation:終于,我們可以鎖定屏幕旋轉了(喜極而泣…)
theme_color/background_color:主題色與背景色,用于配置一些可定制的操作系統 UI 以提高用戶體驗,比如 Android 的狀態欄、任務欄等。
這個清單的成員還有很多,比如用于聲明「對應原生應用」的 related_applications 等等,本文就不一一列舉了。作為 PWA 的「戶口本」,承載著 Web 應用與操作系統集成能力的重任,Web App Manifest 還將在日后不斷擴展,以滿足 Web 應用高速演化的需要。
我們原有的整個 Web 應用模型,都是構建在「用戶能上網」的前提之下的,所以一離線就只能玩小恐龍了。其實,對于「讓 Web 應用離線執行」這件事,Service Worker 至少是 Web 社區的第三次嘗試了。
故事可以追溯到 2007 年的 Google Gears:為了讓自家的 Gmail、Youtube、Google Reader 等 Web 應用可以在本地存儲數據與離線執行,Google 開發了一個瀏覽器拓展來增強 Web 應用。Google Gears 支持 IE 6、Safari 3、Firefox 1.5 等瀏覽器;要知道,那一年 Chrome 都還沒出生呢。
在 Gears API 中,我們通過向 LocalServer 模塊提交一個緩存文件清單來實現離線支持:
// Somewhere in your javascript var localServer = .gears.factory.create("bata.localserver"); var store = localServer.createManagedStore(STORE_NAME);
store.manifestUrl = "manifest.json"
// manifest.json - 假設 JSON 有注釋
{
"betaManifestVersion": 1,
"version": "1.0",
"entries": [
{ "url": "index.html"},
{ "url": "main.js"}
] }
是不是感到很熟悉?好像 HTML5 規范中的 Application Cache 也是類似的東西?
<html manifest="cache.appcache">
CACHE MANIFEST CACHE: index.html main.js
是的,Gears 的 LocalServer 就是后來大家所熟知的 App Cache 的前身,大約從 2008 年開始 W3C 就開始嘗試將 Gears 進行標準化了;除了 LocalServer,Gears 中用于提供并行計算能力的 WorkerPool 模塊與用于提供本地數據庫與 SQL 支持的 Database 模塊也分別是日后 Web Worker 與 Web SQL Database(后被廢棄)的前身。
HTML5 App Cache 作為第二波「讓 Web 應用離線執行」的嘗試,確實也服務了比如 Google Doc、尤雨溪早年作品 HTML5 Clear、以及一直用 Web 應用作為自己 iOS 應用的 FT.com(Financial Times)等不少 Web 應用。那么,還有 Service Worker 什么事呢?
是啊,如果 App Cache 沒有被設計得爛到完全不可編程、無法清理緩存、幾乎沒有路由機制、出了 Bug 一點救都沒有,可能就真沒 Service Worker 什么事了。App Cache 已經在前不久定稿的 HTML5.1 中被拿掉了,W3C 為了挽救 Web 世界真是不惜把自己的臉都打腫了……
時至今日,我們終于迎來了 Service Worker 的曙光。簡單來說,Service Worker 是一個可編程的 Web Worker,它就像一個位于瀏覽器與網絡之間的客戶端代理,可以攔截、處理、響應流經的 HTTP 請求;配合隨之引入 Cache Storage API,你可以自由管理 HTTP 請求文件粒度的緩存,這使得 Service Worker 可以從緩存中向 Web 應用提供資源,即使是在離線的環境下。
圖6 Service Worker 就像一個運行在客戶端的代理
比如說,我們可以給網頁 foo.html 注冊這么一個 Service Worker,它將劫持由 foo.html 發起的一切 HTTP 請求,并統統返回未設置 Content-Type 的 Hello World!:
// sw.js self.onfetch = (e) => {
e.respondWith(new Response('Hello World!'))
}
Service Worker 次發布于 2014 年的 Google IO 上,目前已處于 W3C 工作草案的狀態。其設計吸取了 Application Cache 的失敗經驗,作為 Web 應用的開發者的你有著完全的控制能力;同時,它還借鑒了 Chrome 多年來在 Chrome Extension 上的設計經驗(Chrome Background Pages 與 Chrome Event Pages),采用了基于「事件驅動」的喚醒機制,以大幅節省后臺計算的能耗。比如上面的 fetch 其實就是會喚醒 Service Worker 的事件之一。
圖7 Service Worker 的生命周期
除了類似 fetch 這樣的功能事件外,Service Worker 還提供了一組生命周期事件,包括安裝、激活等等。比如,在 Service Worker 的「安裝」事件中,我們可以把 Web 應用所需要的資源統統預先下載并緩存到 Cache Storage 中去:
// sw.js self.oninstall = (e) => {
e.waitUntil(
caches.open('installation')
.then(cache => cache.addAll([ './', './styles.css', './script.js' ]))
)
});
這樣,當用戶離線,網絡無法訪問時,我們就可以從緩存中啟動我們的 Web 應用:
//sw.js self.onfetch = (e) => { const fetched = fetch(e.request) const cached = caches.match(e.request)
e.respondWith(
fetched.catch(_ => cached)
)
}
可以看出,Service Worker 被設計為一個相對底層(low-level)、高度可編程、子概念眾多,也因此異常靈活且強大的 API,故本文只能展示它的冰山一角。出于安全考慮,注冊 Service Worker 要求你的 Web 應用部署于 HTTPS 協議下,以免利用 Service Worker 的中間人攻擊。我在今年 GDG 北京的 DevFest 上分享了 Service Worker 101,涵蓋了 Service Worker 譬如「網絡優先」、「緩存優先」、「網絡與緩存比賽」這些更復雜的緩存策略、學習資料、以及示例代碼,可以供大家參考。
圖8 Service Worker 的一種緩存策略:讓網絡請求與讀取緩存比賽
你也可以嘗試在支持 PWA 的瀏覽器中訪問我的博客 Hux Blog,感受 Service Worker 的實際效果:所有訪問過的頁面都會被緩存并允許在離線環境下繼續訪問,所有未訪問過的頁面則會在離線環境下展示一個自定義的離線頁面。
在我看來,Service Worker 對 PWA 的重要性相當于 XMLHTTPRequest 之于 Ajax,媒體查詢(Media Query)之于響應式設計,是支撐 PWA 作為「下一代 Web 應用模型」的核心技術。由于 Service Worker 可以與包括 Indexed DB、Streams 在內的大部分 DOM 無關 API 進行交互,它的潛力簡直無可限量。我幾乎可以斷言,Service Worker 將在未來十年里成為 Web 客戶端技術工程化的兵家必爭之地,帶來「離線優先(Offline-first)」的架構革命。
PWA 推送通知中的「推送」與「通知」,其實使用的是兩個不同但又相得益彰的 API:
Notification API 相信大家并不陌生,它負責所有與通知本身相關的機制,比如通知的權限管理、向操作系統發起通知、通知的類型與音效,以及提供通知被點擊或關閉時的回調等等,目前國內外的各大網站(尤其在桌面端)都有一定的使用。Notification API 早應該是在 2010 年前后由 Chromium 提出草案以 webkitNotifications 前綴方式實現;隨著 2011 年進入標準化;2012 年在 Safari 6(Mac OSX 10.8+)上獲得支持;2015 年 Notification API 成為 W3C Recommendation;2016 年 Edge 的支持;Web Notifications 已經在桌面瀏覽器中獲得了全面支持(Chrome、Edge、Firefox、Opera、Safari)的成就。
Push API 的出現則讓推送服務具備了向 Web 應用推送消息的能力,它定義了 Web 應用如何向推送服務發起訂閱、如何響應推送消息,以及 Web 應用、應用服務器與推送服務之間的鑒權與加密機制;由于 Push API 并不依賴 Web 應用與瀏覽器 UI 存活,所以即使是在 Web 應用與瀏覽器未被用戶打開的時候,也可以通過后臺進程接受推送消息并調用 Notification API 向用戶發出通知。值得一提的是,Mac OSX 10.9 Mavericks 與 Safari 7 在 2013 年就發布了自己的私有推送支持,基于 APNS 的 Safari Push Notifications。
在 PWA 中,我們利用 Service Worker 的后臺計算能力結合 Push API 對推送事件進行響應,并通過 Notification API 實現通知的發出與處理:
// sw.js self.addEventListener('push', event => {
event.waitUntil( // Process the event and display a notification. self.registration.showNotification("Hey!")
);
});
self.addEventListener('notificationclick', event => { // Do something with the event event.notification.close();
});
self.addEventListener('notificationclose', event => { // Do something with the event });
對于 Push Notification,我的幾次分享中一直都提的稍微少一些,一是因為 Push API 還處于 Editor Draft 的狀態,二是目前瀏覽器與推送服務的互相支持都還不夠成熟:Android 上的 Chrome(與其它基于 Blink 的瀏覽器)目前只支持基于 Google 私有的 GCM/FCM 的通知推送,只有 Firefox 已經實現了成在由 IETF 進行標準化的 Web 推送協議(Web Push Protocol)。
不過,如果你已經在使用 Google 的云服務(比如 Firebase),并且主要面向的是海外用戶,那么在 Web 應用上支持基于 GCM/FCM 的推送通知并不是一件費力的事情,我推薦你閱讀一下 Google Developers 的系列文章,很多國外公司已經玩起來了。
本站文章版權歸原作者及原出處所有 。內容為作者個人觀點, 并不代表本站贊同其觀點和對其真實性負責,本站只提供參考并不構成任何投資及應用建議。本站是一個個人學習交流的平臺,網站上部分文章為轉載,并不用于任何商業目的,我們已經盡可能的對作者和來源進行了通告,但是能力有限或疏忽,造成漏登,請及時聯系我們,我們將根據著作權人的要求,立即更正或者刪除有關內容。本站擁有對此聲明的最終解釋權。