在使用 create-react-app 創建的項目中,已經自帶有一個 serviceWorker.js 文件,為項目打包後的資源文件提供了離線緩存,我們只需要開啓或關閉這項特性。一般情況下,默認的緩存配置和策略已經夠用,但是在我們項目中遇到了如下需求
- 項目打包後的資源需要做離線緩存
- 對於某些遠程資源(非項目內)也需要能夠緩存
- 及時更新,用户不需要關閉當前 tab 頁也能訪問到最新版本
下面介紹如何實現這些需求。
去除原始配置
create-react-app 使用 WorkboxWebpackPlugin 插件來實現離線緩存,去到 {project-root}/config/webpack.config.js 文件中刪除 WorkboxWebpackPlugin 相關代碼,當前版本下刪除的是下面這一段
isEnvProduction &&
new WorkboxWebpackPlugin.GenerateSW({
...
})
然後刪除 {project-root}/src/serviceWorker.js 並去除 {project-root}/src/index.js 中的引用代碼。
緩存遠程資源
要緩存非項目內的遠程資源, 我們使用 Workbox 庫來實現相關功能。
創建 sw.js
在 {project-root}/src 目錄下新建 sw.js 作為 Service Worker 註冊文件,這個文件必須作為一個單獨的構建入口,不能和其他項目代碼一起打包,所以得修改 webpack.config.js 添加多入口。
將原先的 entry 配置
entry: [
isEnvDevelopment &&
require.resolve('react-dev-utils/webpackHotDevClient'),
paths.appIndexJs,
].filter(Boolean),
修改為
entry: {
main: [
isEnvDevelopment &&
require.resolve('react-dev-utils/webpackHotDevClient'),
paths.appIndexJs,
].filter(Boolean),
sw: paths.appSwJs
},
paths.appSwJs 為 paths.js 中新添加的一個路徑
module.exports = {
...
appSwJs: resolveModule(resolveApp, 'src/sw')
};
filename 配置修改為
filename: chunkData => {
if (chunkData.chunk.name === "sw") {
// sw.js 不能帶有 hash
return "sw.js";
}
return isEnvProduction
? "static/js/[name].[contenthash:8].js"
: isEnvDevelopment && "[name].bundle.js";
}
runtimeChunk 修改為
runtimeChunk: false
如此, 我們就可以在 sw.js 中自由的導入 workbox 庫了。
緩存優先
使用 CacheFirst 策略緩存遠程 mp3/mp4 資源。注意 CacheableResponsePlugin 插件的使用,指定只有響應狀態碼為 0 或者 200 時才緩存資源,否則會把錯誤的響應也緩存了,並且在之後的請求中一直錯誤下去。
在我們的項目中,遠程資源沒有版本號控制,所以使用 ExpirationPlugin 來配置緩存何時刪除,如果很明確的知道何時該刪除遠程資源,可以調用 Workbox 提供的 api 精確的清除緩存。
// sw.js
import { registerRoute } from 'workbox-routing';
import { CacheFirst } from 'workbox-strategies';
import { CacheableResponsePlugin } from 'workbox-cacheable-response';
import { ExpirationPlugin } from 'workbox-expiration';
registerRoute(
({ url }) => {
// 篩選出需要緩存的資源
return /webapp.*saturnv.*\.(?:mp3|mp4)$/.test(url.href);
},
new CacheFirst({
cacheName: 'meida-cache',
plugins: [
new CacheableResponsePlugin({
statuses: [0, 200]
}),
new ExpirationPlugin({
maxEntries: 200,
maxAgeSeconds: 30 * 24 * 60 * 60,
purgeOnQuotaError: true
})
]
})
)
項目內資源離線緩存
create-react-app 原始配置的離線緩存已經被我們刪掉了,因此要自行配置項目內資源的離線緩存。 Workbox 提供有 injectManifest 函數供我們配置離線緩存,我們需要在 webpack 構建後,告知哪些資源做離線緩存。
injectManifest 函數的作用就是在 sw.js 文件中插入一段代碼,指明離線緩存文件列表。因此我們需要先在 sw.js 文件中指定插槽。
// sw.js
import { precacheAndRoute } from 'workbox-precaching';
// self.__WB_MANIFEST 就是 `Workbox` 指定的插槽,
// 在調用 injectManifest 之後,會被替換成一個數組
// 數組包含有所有需要離線緩存的文件
precacheAndRoute(self.__WB_MANIFEST || [], {
cleanURLs: false,
});
這裏沒有過多思考,就直接在 {project-root}/scripts/build.js 文件中,選擇在構建完成之後調用 injectManifest 函數,生成離線緩存列表。
// {project-root}/scripts/build.js
const { injectManifest } = require('workbox-build');
checkBrowsers(paths.appPath, isInteractive)
.then(() => { ... })
.then(previousFileSizes => { ... })
.then(
({ stats, previousFileSizes, warnings }) => {
// 這裏 build 目錄已經生成, 可以調用 injectManifest 函數
injectManifest({
swSrc: 'build/sw.js', // 指定要插入緩存的 sw.js 文件
swDest: 'build/sw.out.js', // 插入緩存後生成的文件路徑
globDirectory: 'build', // 指定操作的根目錄
globIgnores: [
'sw.js' // 忽略 sw.js 文件自身
],
globPatterns: [
'**\/*.{js,zip,mp3,png}', // 匹配需要離線緩存的文件, 注意這裏沒有緩存 html
]
}).then(({count, size}) => {
console.log(`which will precache ${count} files, totaling ${size} bytes.`);
});
})
註冊 Service Worker
經過以上步驟, build 目錄已經生成了 sw.js 和 sw.out.js 目錄。其中 sw.js 沒有緩存項目內資源, sw.out.js 中有項目內資源的緩存列表。
因此,我們可以在開發階段使用 sw.js 註冊 Service Worker,生產階段使用 sw.out.js 註冊 Service Worker,這樣一來開發階段也能享受遠程資源的緩存,並且本地代碼也能夠實時更新。
在 {project-root}/src/index.js 文件中註冊 Service Worker
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register(process.env.NODE_ENV === 'development' ? './sw.js' : './sw.out.js')
.then(registration => {
console.log(`Service worker registered with scope: ${registration.scope}`);
})
}
及時更新
按照 creat-react-app 的默認配置,index.html 也會被離線緩存。在項目發版後,用户第一次訪問的任然是緩存的舊版本,並且需要關閉當前 tab 頁,再次打開才能訪問到新版本。
在我們的配置中,已經去掉 index.html 的離線緩存,每次項目發版,用户能夠訪問到最新的 index.html,因此只要讓新版的 Service Worker 立即生效即可。
在 sw.js 中添加以下代碼,即可讓新版的 Service Worker 立即生效,接管緩存。
self.addEventListener('install', () => {
self.skipWaiting();
});
self.addEventListener('activate', (event) => {
// clients 是 workbox 創建的全局變量
event.waitUntil(clients.claim());
});
參考
- workbox
- Service Worker
- 完整示例代碼