乐趣区

关于程序员:如何优雅的实现前端版本投产自动触发浏览器刷新更新版本内容

如何优雅的实现前端版本投产主动触发浏览器刷新更新版本内容

需要背景

前端我的项目版本投产后如果用户没有及时的拿到最新投产的资源包,可能会存在以下问题:

  1. 短少新性能或修复:如果最新的资源蕴含新性能或修复了现有的问题,但用户没有及时获取到它们,那么用户将无奈及时的体验到这些新性能或修复的益处。
  2. 平安问题:新版本的资源可能蕴含了安全性修复,修复了已知的破绽或平安危险。如果用户没有及时的获取到这些修复,那么网站可能存在潜在的平安危险,容易受到攻打。
  3. 程序报错:如果最新投产的版本蕴含了一些配合后端接口数据革新,比方后端接口数据结构或者字段产生了扭转,然而前端动态资源 JS 没有获取到最新的,可能会导致程序报错,会重大影响到用户体验。

为了防止以上问题,确保在前端版本投产后,能及时获取到最新的前端动态资源,比方 js、css 和图片等,咱们须要应用一种比拟好的计划来实现该需要,那么,目前有哪些比拟常见的解决方案呢?

后期调研

其实,检测前端我的项目版本内容变动主动刷新浏览器以更新版本内容的实现计划有不少,上面是目前常见的一些实现计划:

  1. 轮询检测版本更新:
  • 在前端代码中增加一个定时器,定期向服务器发送申请,查看是否有新的版本可用。
  • 服务器端能够提供一个接口用于查看版本更新,比拟以后客户端的版本号与服务器端最新版本号是否统一。
  • 如果有新版本可用,前端代码能够主动触发页面刷新更新版本内容。

优缺点:实现起来简略粗犷,然而,定时轮询检测版本更新可能会对服务器端和客户端造成肯定的累赘,而且轮询工夫距离不好管制,多久轮询一次适合?

  1. 应用服务端推送技术(Server-Sent Events,SSE):
  • Server-Sent Events 是一种基于 HTTP 的单向通信机制,容许服务器实时向客户端发送事件音讯。
  • 在前端代码中,通过创立 EventSource 对象来与服务器建设 SSE 连贯,并监听服务器发送的音讯。
  • 当服务器检测到新版本时,能够向客户端发送一个 SSE 事件音讯,客户端收到音讯后触发浏览器主动刷新操作。

优缺点:SSE 的长处是它是一个简略而轻量级的协定,不须要额定的库或框架,实用于一些简略的实时通信场景。

  1. 应用 WebSocket 实时通信:
  • 建设 WebSocket 连贯,使服务器可能实时向客户端发送音讯。
  • 当有新的版本可用时,服务器能够被动向客户端发送告诉。
  • 客户端收到告诉后,能够主动触发浏览器刷新更新版本内容。

优缺点:通过 WebSocket 实时通信,服务器能够间接向客户端推送音讯,无需进行轮询。当服务器检测到新版本时,它能够立刻发送音讯给客户端,客户端收到音讯后触发浏览器主动刷新操作,实现版本内容自动更新。

这三种实现计划都须要后端配合,那有没有不须要后端配合,纯前端就能实现的计划呢?那必须有呀

纯前端实现版本投产主动刷新浏览器更新版本内容

应用 nodejs 脚本生成版本信息 json 文件 + 监听页面显示和暗藏会触发的 visibilitychange 事件,纯前端实现版本投产主动刷新浏览器更新版本内容

大抵实现原理:

  1. 应用 nodejs 编写脚本,获取 git 版本相干信息(必须蕴含 git commitId,用于版本比照),并保留为 json 文件,寄存在构建打包的目录下(比方,public 目录)。
  2. 应用页面显示和暗藏会触发的 visibilitychange 事件,监听页面的显示和暗藏操作,如果页面显示,则申请打包放在 dist 根目录下的版本信息 json 文件,比照以后打包版本的 commitId 与历史版本信息 json 文件中 commitId 是否统一,如果不统一,则触发浏览器刷新。
  3. vite 打包我的项目应用 .env 文件 + import.meta.env保留以后打包变量(webpack 打包我的项目能够应用 definePlugin 插件 + process.env 保留变量)

应用 nodejs 编写获取 git 版本信息的脚本

前置常识:

  • vite 我的项目打包,以及理解我的项目架构和目录构造
  • nodejs 命令执行、文件读写操作相干 api
  • dotenv 装置 dotenv npm 依赖,用于批改 .env 文件

话不多说,间接上代码:

// useNodeGetGitInfo.js 

/** 定义模块和变量 **/
// const exec = require('child_process').exec // 异步子过程
const execSync = require('child_process').execSync // 同步子过程
const fs = require('fs') // 文件读取模块
const path = require('path') // 文件门路解决模块
const gitInfoPath = 'gitInfo.json' // gitInfo 门路
const publicPath = 'public' // 不能放到 dist 目录(该目录打包文件会被清空),要放到 public 目录,const autoPush = false // 写入版本信息之后是否主动提交 git 上
const isVite = true // 是否是 vite 构建打包
const commitId = execSync('git show -s --format=%H').toString().trim() // 以后提交的版本号

// 不借用 chalk 库,原生 Node 打印色彩
// console.log('\x1b[32m%s\x1b[0m', '这是绿色文本') // 绿色
// console.log('\x1b[33m%s\x1b[0m', '这是黄色文本') // 黄色
// console.log('\x1b[31m%s\x1b[0m', '这是红色文本') // 红色

/** 程序开始 **/
let gitInfoObj = {} // 保留 git 版本信息

Date.prototype.format ||
  (Date.prototype.format = function (fmt) {
    const opt = {'Y+': this.getFullYear().toString(), // 年
      'm+': (this.getMonth() + 1).toString(), // 月
      'd+': this.getDate().toString(), // 日
      'H+': this.getHours().toString(), // 时
      'M+': this.getMinutes().toString(), // 分
      'S+': this.getSeconds().toString(), // 秒
      // 有其余格式化字符需要能够持续增加,必须转化成字符串
    }
    for (let k in opt) {if (new RegExp('(' + k + ')', 'i').test(fmt)) {
        fmt = fmt.replace(
          RegExp.$1,
          RegExp.$1.length == 1
            ? opt[k]
            : opt[k].padStart(RegExp.$1.length, '0'),
        )
      }
    }
    return fmt
  })

// 如果 gitInfoPath 存在,将先读取里边的版本信息
if (fs.existsSync(gitInfoPath)) {gitInfoObj = JSON.parse(fs.readFileSync(gitInfoPath).toString())
}

// 判断以后版本是否曾经存在,存在则不再次生成
if (gitInfoObj.commitId === commitId) {console.warn('\x1B[33m%s\x1b[0m', 'warning: 以后的 git 版本数据曾经存在了!\n')
} else {const currentGitBranch = execSync('git rev-parse --abbrev-ref HEAD')
    .toString()
    .trim() // 以后 git 分支
  const name = execSync('git show -s --format=%cn').toString().trim() // 姓名
  const email = execSync('git show -s --format=%ce').toString().trim() // 邮箱
  const date = new Date(execSync('git show -s --format=%cd').toString()) // 日期
  const message = execSync('git show -s --format=%s').toString().trim() // 阐明

  gitInfoObj = {
    currentGitBranch,
    name,
    email,
    date: date.format('yyyy-mm-dd hh:mm:ss'),
    commitId,
    message,
  }
  const saveInfoStr = JSON.stringify(gitInfoObj, null, 2)
  fs.writeFileSync(gitInfoPath, saveInfoStr)
  // 写入版本信息之后,主动将版本信息提交到以后分支的 git 上
  if (autoPush) {execSync(`git add .`)
    execSync(`git commit ${gitInfoPath} -m 主动提交版本信息 `)
    execSync(`git pull origin ${execSync('git rev-parse --abbrev-ref HEAD')
        .toString()
        .trim()}`,
    )
    execSync(`git push origin ${execSync('git rev-parse --abbrev-ref HEAD')
        .toString()
        .trim()}`,
    )
  }

  // 程序执行完结
  console.log(
    '\x1b[32m%s\x1b[0m',
    `execute success: file address is ${process.cwd()}/${gitInfoPath}\n`,
  )
}

// 将 gitInfo 文件移植到 public 文件中,以便构建工具可能失常打包到我的项目根目录
if (fs.existsSync(publicPath)) {
  fs.writeFileSync(`${process.cwd()}/${publicPath}/${gitInfoPath}`,
    fs.readFileSync(gitInfoPath),
  )
}

// 如果是 vite 构建打包,把 git 信息追加写入.env 文件中
if (isVite) {const dotenv = require('dotenv')

  const envPath = `${process.cwd()}/dotenv/.env`
  // 读取 .env 文件内容
  const envContent = fs.readFileSync(envPath, {encoding: 'utf-8',})

  // 解析内容为键值对对象
  const envVariables = dotenv.parse(envContent)
  const gitInfoStr = JSON.stringify(gitInfoObj)
  // 批改特定的环境变量
  envVariables.VITE_GIT_INFO = gitInfoStr

  // 将批改后的键值对转换为字符串
  const updatedEnvContent = Object.entries(envVariables)
    .map(([key, value]) => `${key}=${value}`)
    .join('\n')

  // 将批改后的内容写入 .env 文件
  console.log(updatedEnvContent)
  fs.writeFileSync(envPath, updatedEnvContent, { encoding: 'utf-8'})

  console.log('\x1b[32m%s\x1b[0m', '.env 文件已更新')
}

配置执行获取 git 版本信息脚本命令

// package.json

"scripts": {
  "build": "npm run get-git-info && vite build",
  "preview": "vite preview",
  "get-git-info": "node scripts/git/useNodeGetGitInfo.js",
},

留神:scripts/git/useNodeGetGitInfo.js这个是笔者我的项目寄存脚本的门路,可依据本人我的项目适当调整

我的项目入口 JS 文件,监听 visibilitychange 事件

前置常识:

  • vite + vue3 生命周期
  • visibilitychange 事件用法
  • import.meta.env 获取 vite 打包相干变量
// app.js

const updateVersion = () => {
  // 检测新版本主动刷新浏览器更新版本内容
  if (import.meta.env.MODE !== 'development') {
    // 获取以后版本 git 信息
    const gitInfo = import.meta.env.VITE_GIT_INFO
    const gitInfoObj = gitInfo && JSON.parse(gitInfo)

    // 通过监听 visibilitychange 事件,取获取 git 版本信息
    document.addEventListener('visibilitychange', () => {
      // 只有在页面显示的时候才触发版本检测
      if (document.visibilityState === 'hidden') return
      // 应用工夫戳避免申请到缓存的数据
      fetch(`/gitInfo.json?v=${Date.now()}`)
        .then((res) => {return res.json()
        })
        .then((data) => {if (data.commitId !== gitInfoObj.commitId) {location.reload()
          }
        })
    })
  }
}

onMounted(() => {updateVersion()
})

实现以上步骤,能够在本人我的项目中应用 npm run build && npm run preview进行测试并查看预览成果。

node 脚本生成的文件

生成的 git 版本信息 json 文件

// gitInfo.json

{
  "currentGitBranch": "master",
  "name": "Better",
  "email": "924902324@qq.com",
  "date": "2023-06-12 21:34:07",
  "commitId": "51007620dcea797659336606631f331082fad0c2",
  "message": "feat: 打包版本发生变化主动触发浏览器刷新"
}

生成 .env 文件

# .env 文件

VITE_GIT_INFO={"currentGitBranch":"master","name":"Better","email":"924902324@qq.com","date":"2023-06-12 21:34:07","commitId":"51007620dcea797659336606631f331082fad0c2","message":"feat: 打包版本发生变化主动触发浏览器刷新"}

总结

通过应用 nodejs 脚本生成 git 版本信息 json 文件 + 监听页面显示和暗藏会触发的 visibilitychange 事件,纯前端实现版本投产主动刷新浏览器更新版本内容,相比定时轮询等实现形式更加优雅,也不须要后端配合,当然也不会减少后端服务器的压力,惟一的毛病可能就是,频繁切换标签关上页面会屡次申请 git 版本信息 json 文件,不过因为该文件很小只有 500 字节左右,还是能够承受的。

有小伙伴可能会问,间接通过在 nginx 服务器上设置 html 文件禁止应用缓存(no-store)就好了,因为只有 index.html 禁止应用缓存,html 加载的 js 和 css 都是通过 vite 或者 webpack 构建工具打包的,文件变动会自动更新文件哈希串,为啥还要用这种形式呢?这种形式的确能够在肯定水平上解决浏览器缓存问题,然而有个前提是须要用户手动刷新浏览器或者从新关上页面,能力加载到最新的资源,如果用户关上页面当前停留在该页面始终不被动刷新,那么用户可能还是拿到的旧的资源,而不是投产后的最新资源,而通过监听页面显示和暗藏会触发的 visibilitychange 事件就能够解决这个问题,即便用户不手动刷新浏览器,只有用户切屏或者切换标签(包含锁屏、睡眠)等都会触发接口申请比照版本,版本更新主动触发浏览器刷新,从而拿到最新的资源包。

如果大家有更好的实现计划,欢送评论区留言,一起学习,一起提高!

本文由 mdnice 多平台公布

退出移动版