博客 / 詳情

返回

在 create-react-app 創建的項目中自定義 Service Worker 緩存策略

在使用 create-react-app 創建的項目中,已經自帶有一個 serviceWorker.js 文件,為項目打包後的資源文件提供了離線緩存,我們只需要開啓或關閉這項特性。一般情況下,默認的緩存配置和策略已經夠用,但是在我們項目中遇到了如下需求

  1. 項目打包後的資源需要做離線緩存
  2. 對於某些遠程資源(非項目內)也需要能夠緩存
  3. 及時更新,用户不需要關閉當前 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.appSwJspaths.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.jssw.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
  • 完整示例代碼
user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.