在 create-react-app 创建的项目中自定义 Service Worker 缓存策略

位流苔原
• 阅读 4565

在使用 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());
});

参考

点赞
收藏
评论区
推荐文章
blmius blmius
4年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
Wesley13 Wesley13
4年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
橘子橙 橘子橙
4年前
vue-element-admin项目打包后,iconfont图标出现乱码
使用vueelementadmin或者vueelementtemplate开发的项目,打包到线上,就出现了图标乱码,f12后能看到icon元素为.eliconclose:before{content:"□"}的情况(如下)
Wesley13 Wesley13
4年前
VBox 启动虚拟机失败
在Vbox(5.0.8版本)启动Ubuntu的虚拟机时,遇到错误信息:NtCreateFile(\\Device\\VBoxDrvStub)failed:0xc000000034STATUS\_OBJECT\_NAME\_NOT\_FOUND(0retries) (rc101)Makesurethekern
Wesley13 Wesley13
4年前
FLV文件格式
1.        FLV文件对齐方式FLV文件以大端对齐方式存放多字节整型。如存放数字无符号16位的数字300(0x012C),那么在FLV文件中存放的顺序是:|0x01|0x2C|。如果是无符号32位数字300(0x0000012C),那么在FLV文件中的存放顺序是:|0x00|0x00|0x00|0x01|0x2C。2.  
Wesley13 Wesley13
4年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
4年前
PHP创建多级树型结构
<!lang:php<?php$areaarray(array('id'1,'pid'0,'name''中国'),array('id'5,'pid'0,'name''美国'),array('id'2,'pid'1,'name''吉林'),array('id'4,'pid'2,'n
Stella981 Stella981
4年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Stella981 Stella981
4年前
Linux应急响应(二):捕捉短连接
0x00前言​短连接(shortconnnection)是相对于长连接而言的概念,指的是在数据传送过程中,只在需要发送数据时,才去建立一个连接,数据发送完成后,则断开此连接,即每次连接只完成一项业务的发送。在系统维护中,一般很难去察觉,需要借助网络安全设备或者抓包分析,才能够去发现。0x01应急场景​
位流苔原
位流苔原
Lv1
汴水流,泗水流,流到瓜洲古渡头,吴山点点愁。思悠悠,恨悠悠,恨到归时方始休,月明人倚楼。
文章
4
粉丝
0
获赞
0