前端实现发版通知 - 学习笔记

121 阅读3分钟

构建工具

使用rollup进行项目打包

思路整理

发版流程:项目打包 -> push到oss -> 自动化部署

一开始设想的是在上传到OSS的这一步去操作,拿到Etag去判断上传的文件是否被修改了。但是发现响应头里没带上Etag(现在想想有可能是nginx或者oss关于Etag的配置没开启),之后看到了 juejin.cn/post/749463… 这位大佬的文章,发现在项目打包阶段直接对dist目录里的文件内容哈希化,生成文件存储哈希值,比对哈希值就可以实现发版通知。

实现步骤

1. 生成版本文件

首先是读取打包目录下的index.html和public文件夹下的所有静态资源。

可能会疑惑为什么要读取public文件夹下的静态资源,只读index.html不行吗?

这是因为public文件夹不经过rollup的构建钩子,在打包出来之后也可以看到public文件夹下的文件是完全原封不动的,没有被处理过。因此,只读取index.html是无法覆盖所有情况的。

/**
 * 用于读取index.html文件信息,创建版本号文件
 */
function createVersionFile(){
  //读取dist下index.html的文件内容转化成utf-8的字符串
  const html = fs.readFileSync('dist/index.html', 'utf-8');
  //使用crypto生成MD5哈希,将html注入哈希计算,并设置生成16进制格式的哈希值
  const htmlHash = crypto.createHash('md5').update(html).digest('hex');
  //计算dist/public文件夹下的所有静态资源的哈希值
  const assetsHash = calcDirHash('dist/public');
  //生成最终的版本哈希值
  const versionHash = crypto.createHash('md5').update(htmlHash).update(assetsHash).digest('hex');
  //将生成的哈希值存入dist下的version.json,记录版本变化
  fs.writeFileSync('dist/version.json', JSON.stringify({ version: versionHash}));
}

/**
 * 计算指定文件夹内所有文件的哈希值
 * @param dir {String} - 指定文件夹
 */
function calcDirHash(dir){
  // 创建MD5哈希
  const hash = crypto.createHash('md5');
  const filesContent = [];
  const traverse = (currentDir) => {
   //读取当前目录下的所有文件
   const files = fs.readdirSync(currentDir);
   files.forEach(file => {
    //获取文件完整路径
    const filePath = path.join(currentDir,file);
    //获取文件信息
    const stat = fs.statSync(filePath);
    //如果是文件夹
    if(stat.isDirectory()){
     //递归执行
     traverse(filePath);
    }
    //如果不是文件夹
    else{
     //读取文件信息并push到filesContent
     const fileContent = fs.readFileSync(filePath);
     filesContent.push({ path: filePath,content: fileContent });
    }
   })
  };
  traverse(dir);
  //使用localeCompare本地化排序,排序后的文件比较符合直觉
  filesContent.sort((a, b) => a.path.localeCompare(b.path))
  //遍历filesContent,更新到hash
  filesContent.forEach(({ content }) => {
   hash.update(content)
  })
  //返回混合所有文件的16进制哈希
  return hash.digest('hex')
}

在vite.config.js的defineConfig.build.rollupOptions.output中

plugins: [
  {
   name: 'createVersion',
   // hook: 打印最终构建结果前
   writeBundle: () => {
    createVersionFile();
   }
  }
]

2. 轮询比对版本文件

新建一个js文件,引入到App.vue里

import {env} from '@/view/api/server.js'
(async () => {
  if(env === 'production'){
   await nextTick(async() => {
    //轮询时间间隔60 * 1000代表一分钟
    const CHECK_INTERVAL = 1000
    
    //先拿一遍版本号,用于和轮询的做对比
    const version = await getVersion();
    
    //获取版本号
    async function getVersion(){
     try {
      const time = dayjs().unix();
      const res = await fetch(`/version.json?t=${ time }`)
      return (await res.json()).version
     }catch(e){
      console.error("获取版本号失败:" + e.message);
     }
    }
    
    //轮询检查版本号
    async function pollingCheckVersion(){
     try {
      const pollingVersion = await getVersion();
      if(version !== pollingVersion){
       throw new Error('版本号不一致,请刷新页面');
      }
     }catch(e){
      console.error(e.message);
      $message.warning({
       content: '版本更新啦,请刷新以更新页面'
      })
     }
    }
    
    setInterval(pollingCheckVersion,CHECK_INTERVAL)
   })
  }
})();

补充

其实如果不在意public文件夹下的文件与每次build出来的哈希都会变化的话,实际上是不需要去生成这样一个version文件的,完全可以直接读取index.html的Last-Modified的,代码的实现也简单很多,只需要轮询即可。