離線快取優化
將應用中的靜態資源快取是目前最主流的效能優化方法,甚至能讓應用秒開!目前常見的快取方式有http快取
、memory cache
、disk cahce
、localstorage
、Service worker快取
等方式,本文介紹的Workbox就是實現Service worker離線快取的一個工具。
那麼問題來了,Service worker離線快取和傳統的快取方式對比,有什麼優勢和劣勢呢,service worker之所以越來越流行,是因為它讓前端快取脫離了服務端,不需要服務端再額外做些什麼,前端工程師自己就可以實現快取,而且快取內容完全可控,下面是我搜尋的幾條主流快取方式的介紹和對比。
上圖從深入理解瀏覽器快取處參考
http快取依賴於服務端配置,memory cache和disk cache快取內容不可控,而且只快取一些靜態資源,push cache是臨時快取,localstorage適用於快取一些全域性的資料,對於靜態資源很少用它。
service worker快取的優缺點:
優點:
- 非侵入式快取
- 快取內容開發者完全可控
- 持續性快取
- 獨立於主執行緒之外,不堵塞程式
缺點:
- 許可權太大,能攔截所有fetch請求,需要控制一下
- 發版更新處理比較麻煩
Workbox簡介
Workbox 是 Google Chrome 團隊推出的一套 PWA 的解決方案,這套解決方案當中包含了核心庫和構建工具,因此我們可以利用 Workbox 實現 Service Worker 的快速開發。
引入方式
有兩種方式可以引入workbox:
第一種最為方便,就是通過importScripts()
方法從谷歌官方CDN中引入。
importScripts('https://storage.googleapis.com/workbox-cdn/releases/5.0.0/workbox-sw.js');
if (workbox) {
console.log(`Yay! Workbox is loaded ?`);
} else {
console.log(`Boo! Workbox didn't load ?`);
}
複製程式碼
第二種方式就是從本地引入,本地需要從npm庫中下載相應的workbox包,然後通過import
按需匯入,本文的例子就是這種方式。
import {precaching} from 'workbox-precaching';
import {registerRoute} from 'workbox-routing';
import {BackgroundSyncPlugin} from 'workbox-background-sync';
// 按需引入,然後使用對應模組...
複製程式碼
詳細介紹請查閱官方文件
配置
Workbox可以修改快取名稱,可以用setCacheNameDetails
設定預快取和執行時快取的名稱,還可以通過workbox.core.cacheNames.precache
和 workbox.core.cacheNames.runtime
獲取當前定義的預快取和動態快取名稱。
// 修改預設配置
workbox.core.setCacheNameDetails({
prefix: 'app',
suffix: 'v1',
precache: 'precache',
runtime: 'runtime'
})
// 列印修改結果
// 將列印 'app-precache-v1'
console.log(worbox.core.cacheNames.precache)
// 將列印 'app-runtime-v1'
console.log(workbox.core.cacheNames.runtime)
複製程式碼
更多配置下資訊請參考官方文配置文件
預快取功能
預快取功能可以在service worker安裝前將一些靜態檔案提前快取下來,這樣就能保證service worker安裝後可以直接存快取中獲取這些靜態資源,可以通過以下程式碼實現。
import {precacheAndRoute} from 'workbox-precaching';
precacheAndRoute([
{url: '/index.html', revision: '383676' },
{url: '/styles/app.0c9a31.css', revision: null},
{url: '/scripts/app.0d5770.js', revision: null},
]);
複製程式碼
更多預快取請參考官方預快取功能文件
路由功能
路由功能是workbx的核心功能,主要是匹配資源路徑後,採用相應的快取策略,或者自定義快取處理,使用方法如下所示:
import {registerRoute} from 'workbox-routing';
import {CacheFirst} from 'workbox-strategies';
registerRoute( /\.(?:png|jpg|jpeg|svg|gif)$/, new CacheFirst({
cacheName: 'my-image-cache',
}));
複製程式碼
registerRoute有兩個引數,第一個引數是一個正規表示式,用來匹配路徑,第二個引數是對匹配路徑進行的處理函式,可以用workbox封裝的快取策略處理函式,也可以自定義,上述示例就是使用的workbox內部封裝的CacheFirst
快取策略。
如果第二個引數使用自定義函式,那麼這個函式有三個預設引數,示例如下:
import {registerRoute} from 'workbox-routing';
const handler = async ({url, event, params}) => {
// Response will be "A guide to Workbox"
return new Response(`A ${params.type} to ${params.name}` );
};
registerRoute(/\.css$/, handler);
複製程式碼
快取策略
Workbox內部封裝了以下五種快取策略:
- NetworkFirst:網路優先
- CacheFirst:快取優先
- NetworkOnly:僅使用正常的網路請求
- CacheOnly:僅使用快取中的資源
- StaleWhileRevalidate:從快取中讀取資源的同時傳送網路請求更新本地快取
五種快取策略使用方法一致,各適用於不同的場景,具體適用場景可在離線指南中檢視。
Webpack+Workbox構建離線應用
目前大部分前端專案都離不開webpack,為了方便我們使用workbox,谷歌官方給我們提供了workbox的webpack外掛,通過這個外掛,我們能在專案中快速引入workbox,通過配置來定製化我們的快取。
通過以下四個步驟,我們能將webpack引入到一個由webpack構建的應用中並實現快取。
第一步:使用workbox-webpack-plugin
- 安裝
npm install workbox-webpack-plugin
複製程式碼
- 在webpack 配置檔案中引入並配置
workbox-webpack-plugin有兩種配置方式:
第一種:GenerateSW
通過配置自動在專案中引入service-worker.js,程式碼如下:
const WorkboxPlugin = require('workbox-webpack-plugin');
module.exports = {
// Other webpack config...
plugins: [
// Other plugins...
new WorkboxPlugin.GenerateSW({
// Do not precache images
exclude: [/\.(?:png|jpg|jpeg|svg)$/],
// Define runtime caching rules.
runtimeCaching: [{
// Match any request that ends with .png, .jpg, .jpeg or .svg.
urlPattern: /\.(?:png|jpg|jpeg|svg)$/,
// Apply a cache-first strategy.
handler: 'CacheFirst',
options: {
// Use a custom cache name.
cacheName: 'images',
// Only cache 10 images.
expiration: {
maxEntries: 10,
},
},
}],
})
]
};
複製程式碼
適用於:
- 預快取靜態檔案
- 只簡單的應用執行時的快取功能
不適用:
- 需要使用service worker 其他功能的場景,如push等
- 需要在service worker中匯入其他指令碼或新增其他邏輯
- 具體的配置檔案可查閱官方文件,本文示例使用的是第二種方式
第二種:InjectManifest
通過已有的service-worker.js檔案生成新的service-worker.js,示例如下:
new workboxPlugin.InjectManifest({
// 目前的service worker 檔案
swSrc: './src/sw.js',
// 打包後生成的service worker檔案,一般存到disk目錄
swDest: 'sw.js'
})
複製程式碼
適用於:
- 預快取檔案
- 更多的定製化快取需求
- 使用service worker其他特性
如果你只想簡單的引入service worker,建議使用第一種方式
第二步:註冊Service Worker
配置好外掛之後,我們需要在專案中註冊service worker。
註冊比較簡單,只需要在專案入口檔案中進行註冊即可,程式碼如下:
// Check that service workers are supported
if ('serviceWorker' in navigator) {
// Use the window load event to keep the page load performant
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js');
});
}
複製程式碼
上述程式碼是最簡單的註冊方式,在我們的專案中我們使用register-service-worker
npm包註冊service worker並新增一下自定義事件,方便後期進行更新和離線事件的處理。
程式碼如下:
import { register } from 'register-service-worker';
export default function(swDest) {
console.log("註冊sw")
register(`/${swDest}`, {
ready(registration) {
// 此方法是我們專案中自己封裝的建立自定義事件的公共方法
dispatchServiceWorkerEvent('sw.ready', registration);
},
registered(registration) {
dispatchServiceWorkerEvent('sw.registered', registration);
},
cached(registration) {
dispatchServiceWorkerEvent('sw.cached', registration);
},
updatefound(registration) {
dispatchServiceWorkerEvent('sw.updatefound', registration);
},
updated(registration) {
dispatchServiceWorkerEvent('sw.updated', registration);
},
offline() {
dispatchServiceWorkerEvent('sw.offline', {});
},
error(error) {
dispatchServiceWorkerEvent('sw.error', error);
},
});
}
複製程式碼
在入口檔案中引入註冊檔案:
import registerSW from './sw-register';
registerSW('sw.js');
複製程式碼
第三步:自定義Service Worker快取配置
如果我們使用injectMainfest的方式引入servicce worker,需要在src目錄下建立一個sw.js(命名自定義,但需要和webpack配置中一致),在這個檔案中我們可以進行預快取等操作。程式碼示例如下(sw.js):
import { setCacheNameDetails, clientsClaim } from 'workbox-core';
import { precacheAndRoute } from 'workbox-precaching/precacheAndRoute';
import {createHandlerBoundToURL} from 'workbox-precaching';
import {NavigationRoute, registerRoute} from 'workbox-routing';
import {StaleWhileRevalidate,NetworkOnly} from 'workbox-strategies';
// 設定快取名稱
setCacheNameDetails({
prefix: 'app',
suffix: 'v1.0.2',
});
// 更新時自動生效
clientsClaim();
// 預快取檔案,self.__WB_MANIFEST是workbox生成的檔案地址陣列,專案中打包生成的所有靜態檔案都會自動新增到裡面
precacheAndRoute(self.__WB_MANIFEST || []);
// 單頁應用需要應用NavigationRoute進行快取,此處可自定義白名單和黑名單
// 跳過登入和退出頁面的攔截
const handler = createHandlerBoundToURL('/index.html');
const navigationRoute = new NavigationRoute(handler, {
denylist: [
/login/,
/logout/,
],
});
registerRoute(navigationRoute);
// 執行時快取配置
// 介面資料使用服務端資料
registerRoute(/^api/,new NetworkOnly());
//圖片cdn地址,屬於跨域資源,我們使用StaleWhileRevalidate快取策略
registerRoute(/^https:\/\/img.xxx.com\//,new StaleWhileRevalidate());
複製程式碼
上述的程式碼有一段針對單頁應用處理的邏輯,應為單頁應用依靠路由變化來載入不同的內容,使用navigationRoute
可以匹配導航請求,從而從換從中載入index.html,但預設情況會攔截所有導航請求,如果需要控制,可以在方法中新增白名單和黑名單加以控制。
第四步:處理Service Worker的更新和離線狀態
更新狀態
配置完成後,我們需要注意service worker的更新和離線狀態,service worker的更新較為複雜,如果處理不當回引發各種問題,目前主流的方式就是每次發版,提醒使用者更新,如果使用者點選確定更新,新發版的service worker會替換掉舊的service worker,此程式碼我們專案中放在了入口檔案中(webpack配置的入口檔案),示例程式碼如下:
// sw.updated是在註冊檔案中新增的自定義事件
window.addEventListener('sw.updated', event => {
const e = event;
const reloadSW = async () => {
// Check if there is sw whose state is waiting in ServiceWorkerRegistration
// https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration
const worker = e.detail && e.detail.waiting;
if (!worker) {
return true;
} // Send skip-waiting event to waiting SW with MessageChannel
await new Promise((resolve, reject) => {
const channel = new MessageChannel();
channel.port1.onmessage = msgEvent => {
if (msgEvent.data.error) {
reject(msgEvent.data.error);
} else {
resolve(msgEvent.data);
}
};
worker.postMessage(
{
type: 'skip-waiting',
},
[channel.port2],
);
});
// Refresh current page to use the updated HTML and other assets after SW has skiped waiting
window.location.reload(true);
return true;
};
const key = `open${Date.now()}`;
const btn = (
<Button
type="primary"
onClick={() => {
notification.close(key);
reloadSW();
}}
>
確定
</Button>
);
notification.open({
message: "新版本釋出",
description: "Boss釋出新版本了,請點選頁面更新",
btn,
key,
onClose: async () => {},
});
});
複製程式碼
對應sw.js檔案裡面要監聽主執行緒傳遞過來的更新事件,程式碼如下:
// service worker通過message和主執行緒通訊
addEventListener('message', event => {
const replyPort = event.ports[0];
const message = event.data;
console.log(message)
if (replyPort && message && message.type === 'skip-waiting') {
event.waitUntil(
self.skipWaiting().then(
() =>
replyPort.postMessage({
error: null,
}),
error =>
replyPort.postMessage({
error,
}),
),
);
}
});
複製程式碼
離線狀態
對於離線狀態的監聽比較簡單,在入口檔案中新增以下程式碼即可:
window.addEventListener('sw.offline', () => {
message.warning("當前處於離線狀態",0);
});
複製程式碼
檢查效果
經過上述四個步驟,我們就能將service worker引入到我們已有的用webpack構建的專案上。
如果正常引入,我們可以在控制檯中看到下圖:
總結
- service worker實現快取有非侵入、持久化、快取內容可控等優點
- Workbox可以理解為service worker的庫,利用它可以快速進行service worker開發
- 通過workbox-webpack-plugin可以將workbox引入到現有的用webpack構建的專案中
本文對workbox的介面的解釋較少,需要各位去官網查閱api。