本系列文章將以一個實際項目做爲研究對象,探討離線可用這個 PWA 的重要特性在 SSR 架構中的應用思路。最後結合 Vue SSR 進行實際應用。php
本文做爲第一部分,以 PWA-Directory__ 爲例。這是一個陳列 PWA 項目的站點,同時展現項目 Lighthouse 分數及其餘頁面性能數據。html
在下一 Part 中,咱們將順着這一思路,結合 Vue SSR 在項目中進行實際應用(關注OpenWeb開發者,及時獲取文章)。前端
本文假設讀者對 PWA 相關技術尤爲是 Service Worker 的基礎知識已有必定了解。android
App Shell 是支持用戶界面所需的最小的 HTML、CSS 和 JavaScript。對其進行離線緩存,可確保在用戶重複訪問時提供即時、可靠的良好性能。這意味着並非每次用戶訪問時都要從網絡加載 App Shell。 只須要從網絡中加載必要的內容。git
PWA-Directory 包括咱們後續的討論都基於 App Shell 模型。下面咱們須要瞭解一下緩存的細節。github
Service Worker 最重要的功能之一即是控制緩存。這裏先簡單介紹下預緩存或者說 sw-precache 插件的基本工做原理。web
在項目構建階段,將靜態資源列表(數組形式)及本次構建版本號注入 Service Worker 代碼中。在 SW 運行時(Install 階段)依次發送請求獲取靜態資源列表中的資源(JS、CSS、HTML、IMG、FONT...),成功後放入緩存並進入下一階段(Activated)。這個在實際請求以前由 SW 進行緩存的過程就是預緩存。chrome
在 SPA/MPA 架構的應用中,App Shell 一般包含在 HTML 頁面中,連同頁面一併被預緩存,保證了離線可訪問。可是在 SSR 架構場景下,狀況就不同了。全部頁面首屏均是服務端渲染,預緩存的頁面再也不是有限且固定的。若是預緩存所有頁面,SW 須要發送大量請求不說,每一個頁面都包含的 App Shell 部分都被重複緩存,也形成了緩存空間的浪費。shell
既然針對所有頁面的預緩存行不通,咱們能不能將 App Shell 剝離出來,單獨緩存僅包含這個空殼的頁面呢?要實現這一點,就須要對後端模板進行修改,經過傳入參數控制返回包含 App Shell 的完整頁面 OR 代碼片斷。這樣首屏使用完整頁面,然後續頁面切換交給前端路由完成,請求代碼片斷進行填充。這也是基於 React、Vue 等技術實現的同構項目的基本思路。json
對於後端模板的修改並不複雜,例如在 PWA-Directory 中,使用 Handlebars 做爲後端模板,經過自定義的 contentOnly 參數就能適應首屏和後續 HTML 片斷兩種請求。其他模板語言例如 WordPress 使用的 php 也是一樣的思路。
// list.hbs
{{#unless contentOnly}}
<!DOCTYPE html>
<html lang="en">
<head>
{{> head}}
</head>
<body>
{{> header}}
<div class="page-holder">
<main class="page">
{{/unless}}
... 頁面具體內容
{{#unless contentOnly}}
</main>
<div class='page-loader'>
</div>
</div>
{{> footer}}
</body>
</html>
{{/unless}}複製代碼
而後在 SW 中咱們須要對 App Shell 頁面進行預緩存,這裏使用了 sw-toolbox 。同時後端須要增長返回 App Shell 的路由規則,這裏是 /.app/shell。
// service-worker.js
const SHELL_URL = '/.app/shell';
const ASSETS = [
SHELL_URL,
'/favicons/android-chrome-72x72.png',
'/manifest.json',
...
];
// 使用 sw-toolbox 緩存靜態資源
toolbox.precache(ASSETS);複製代碼
最後咱們攔截掉全部 HTML 請求,請求目標頁面的內容片斷而非完整代碼(getContentOnlyUrl 執行了 contentOnly 參數拼接工做),返回以前緩存的 App Shell 頁面。
// service-worker.js
toolbox.router.default = (request, values, options) => {
// 攔截 HTML 請求
if (request.mode === 'navigate') {
// 請求 HTML 代碼片斷
toolbox.cacheFirst(new Request(getContentOnlyUrl(request.url)), values, options);
// 返回 App Shell 頁面
return getFromCache(SHELL_URL)
.then(response => response || gulliverHandler(request, values, options));
}
return gulliverHandler(request, values, options);
};複製代碼
有一點值得注意,一般請求目標頁面內容片斷是放在前端路由中完成的,而這裏放在了 SW 中,有什麼好處呢?這一點 PWA-Directory 開發者有一篇文章__進行了專門討論,這裏就直接使用文中的圖片進行說明了。
先看看以前的作法,也就是在前端路由中:
能夠看出,app.js 加載並執行時纔會發出 HTML 代碼片斷的請求,而後等待服務端響應。整個過程當中 SW 處於空閒狀態,而事實上第一次攔截到 HTML 請求時,SW 就徹底能夠先請求代碼片斷了(拼上參數),拿到響應後放入緩存中。這樣當 app.js 前端路由執行發出請求時,瀏覽器發現該片斷已經在緩存中,能夠直接拿來使用。固然爲了實現這一點,須要在服務端經過設置響應頭 Cache-Control: max-age 保證內容片斷的緩存時間。
總結一下這個思路:
實際效果是,用戶第一次訪問應用站點時,首屏由服務端渲染,隨後 SW 安裝成功後,後續的路由切換包括刷新頁面都將由前端渲染完成,服務端將只負責提供 HTML 代碼片斷的響應。
解決了預緩存問題,下面咱們須要關注另一個離線可用目標中涉及的關鍵問題。
在衡量 PWA 效果時,至少有如下幾個指標能夠考量:
經過 beforeinstallprompt__事件,能夠輕易獲取用戶對添加到桌面 banner 的反應:
window.addEventListener('beforeinstallprompt', e => {
console.log(e.platforms); // e.g., ["web", "android", "windows"]
e.userChoice.then(outcome => {
console.log(outcome); // either "installed", "dismissed", etc.
}, handleError);
});複製代碼
經過在 manifest.json 的 start_url 中添加參數,很容易標識出當前的用戶訪問來自添加後的桌面快捷方式。例如使用 GA Custom campaigns__:
// manifest.json
{
"start_url": "/?utm_source=homescreen"
}複製代碼
最後,使用 navigator.onLine__ 就可以判斷當前是否處於離線狀態。可是要注意,返回 true 時不表明真的能夠訪問互聯網。
如今咱們有了這些統計指標,接下來的問題就是如何保證離線狀態下產生的統計數據不丟失。一個很天然的想法是,在 SW 中攔截全部統計請求,離線時將統計數據存儲在本地 LocalStorage 或者 IndexedDB 中,上線後再進行數據的同步。
Google 以前針對 GA 開發了 sw-offline-google-analytics 類庫實現了這一功能,如今已經移到了 Workbox 中做爲一個獨立模塊 workbox-google-analytics__ 存在,能夠很方便地使用:
// service-worker.js
importScripts('path/to/offline-google-analytics-import.js');
workbox.googleAnalytics.initialize();複製代碼
這樣離線統計的問題就解決了。以上部分代碼以 GA 爲例,不過其餘統計腳本思路也是一致的。
最後說說這個項目在離線用戶體驗上的亮點。PWA 中的離線用戶體驗毫不僅僅只是展現離線頁面代替瀏覽器「恐龍」而已。離線時,「我究竟能使用哪些功能?」每每是用戶最關心的。讓咱們來看看 PWA-Directory 在這一點上是怎麼作的。
在離線時,能夠彈出 Toast(圖中下方紅色部分)給予用戶提示。要實現這一點並不難,經過監聽 online/offline 事件就能作到,接下來纔是亮點。
前面說過,離線時用戶很關心能訪問哪些內容,若是能經過樣式顯式標註就再好不過了。在上圖中,我訪問過第一個 Tab 「New」 下列表中的第一個項目,因此此時離線時,頁面中其他部分都被置灰且不可點擊,只有緩存過的內容被保留了下來,用戶將再也不有四處點擊遇到一樣離線頁面的挫敗感。
要實現這一點能夠從兩方面入手,首先從全局樣式上,離線時給 body 或者具體頁面容器加個自定義屬性,關心離線功能的組件在這個規則下定義本身的離線樣式就好了。
window.addEventListener('offline', () => {
// 給容器加上自定義屬性
document.body.setAttribute('offline', 'true');
});複製代碼
另外具體到某些特定組件,例如這個項目中的列表項,點擊每一個 PWA 項目的連接都將進入對應的詳情頁,首次訪問會被加入 runtimeCache,所以只須要在緩存中按連接地址進行查詢,就能知道這個列表項是否應該置灰。
// 判斷連接是否訪問過
isAvailable(href) {
if (!href || this.window.navigator.onLine) return Promise.resolve(true);
return caches.match(href)
.then(response => response.status === 200)
.catch(() => false);
}複製代碼
總之,離線用戶體驗是須要根據實際項目狀況進行精心設計的。
從 PWA 特性尤爲是離線緩存來看,對於 SSR 架構的項目,進行 App Shell 的分離是頗有必要的。相比 SPA/MPA 的預緩存方案,SSR 須要對後端模板,前端路由進行一些改造。另外,對於 PWA 相關數據的統計和離線同步,能夠借鑑應用 Google 的 Workbox 方案。最後,離線用戶體驗也是須要仔細考量的。
若是感興趣,能夠深刻了解一下 PWA-Directory 的代碼__,同時結合開發者的幾篇技術文章:
在下一 Part 中,咱們將使用 Vue SSR 結合 Workbox 在項目中實踐這一思路:)。
BOW(Brillant Open Web)團隊,是一個專門的Web技術建設小組,致力於推進 Open Web 技術的發展,讓Web從新成爲開發者的首選。
BOW 關注前端,關注Web;剖析技術、分享實踐;談談學習,也聊聊管理。
關注 OpenWeb開發者,點擊「加羣」,讓咱們一塊兒推進 OpenWeb技術的發展!