不使用轮巡来完成前端版本监测

697 阅读1分钟

背景

最近联调接口时,后端偶尔不刷新页面就调试接口,时不时需要提醒她刷页面再操作。于是想到了版本更新这个功能来自动完成这个事。

功能实现

  1. 使用 vite 插件构建 git 提交信息来生成版本信息文件
  2. 将版本信息在 window 存一份,方便后续比较
  3. 使用事件操作来对比版本文件信息是否一致
  4. 既然是提示,我们不强制一定更新

生成 vite 插件

下列是我所有项目必备的一个插件,会在根目录生产一个资产文件和一个版本信息文件

import { execSync } from 'child_process'
import os from 'os'
const platform = os.platform()

let code = 0

export default function VitePluginBuildLegacy() {
  return {
    name: 'vite-plugin-build-legacy',
    apply: 'build',
    generateBundle(_options, bundle) {
      let content = `| 文件名 | 大小 | \n| :---: | --- | \n`
      const [assets, chunks] = Object.values(bundle).reduce(
        (r, i) => (r[i.type === 'asset' ? 0 : 1].push(i), r),
        [[], []]
      )
      assets.forEach(
        (item) => (content += `| ${item.fileName} | ${formatSize(item.source.length)} | \n`)
      )
      chunks.forEach(
        (item) => (content += `| ${item.fileName} | ${formatSize(item.code.length)} | \n`)
      )
      const totalSize =
        assets.reduce((total, item) => total + item.source.length, 0) +
        chunks.reduce((total, item) => total + item.code.length, 0)
      content += `| 总计 | ${formatSize(totalSize)}(仅构建资产) | \n`

      this.emitFile({ type: 'asset', fileName: 'assets.md', source: content })
      this.emitFile({
        type: 'asset',
        fileName: 'version.json',
        source: JSON.stringify(GeneratVersion(), null, 2),
      })
    },
  }
}

function formatSize(size) {
  if (size < 1024) {
    return size + 'B'
  } else if (size < 1024 * 1024) {
    return (size / 1024).toFixed(2) + 'KB'
  } else if (size < 1024 * 1024 * 1024) {
    return (size / (1024 * 1024)).toFixed(2) + 'MB'
  } else {
    return (size / (1024 * 1024 * 1024)).toFixed(2) + 'GB'
  }
}

export function GeneratVersion(assets_dir) {
  try {
    const commitId = execSync('git log -n1 --format=format:"%H"').toString().trim()
    const author = execSync('git log -n1 --format=format:"%an"').toString().trim()
    let branch

    try {
      branch = execSync('git symbolic-ref --short HEAD').toString().trim()
    } catch (error) {
      branch = execSync('git rev-parse --abbrev-ref HEAD').toString().trim()
    }
    const commitTime = execSync('git log -n1 --format=format:"%ad" --date=iso')
      .toString()
      .substring(0, 19)
    const content = execSync('git log -n1 --format=format:"%s"').toString().trim()

    const json = { platform, commitId, author, branch, commitTime, content, code }

    assets_dir && fs.writeFileSync(assets_dir, JSON.stringify(json, null, 2))

    return json
  } catch (error) {
    return { msg: '获取版本信息失败', error: error.message }
  }
}

插件注册

vite.config.js 代码注册使用

import VitePluginBuildLegacy, { GeneratVersion } from './vite-plugin-build-legacy'

export default defineConfig(({ command, mode }) => {
  return {
    // ... your code
    define: {
      __version__: JSON.stringify(GeneratVersion()),
    },
    plugins: [
      // ... your code
      VitePluginBuildLegacy(),
    ]
  }
})

现在我们执行构建项目时候根目录会生成一个version.json(信息如下:),之所以是 json 信息,因为便于处理。

{
  "platform": "darwin",
  "commitId": "27c13a818c9745bf66cef63f67be4ed684341049",
  "author": "****",
  "branch": "master",
  "commitTime": "2024-10-31 15:34:41",
  "content": "refactor: 优化记忆模块排序和计数逻辑"
}

浏览器版本更新事件

  • 我没采用轮巡的是不想看到一堆无效的请求出现,而且这个功能和后端几乎不挨边。
  • 由于处理的东西可能较多,我加使用 class构造类 来完成,这样代码代码逻辑会清晰,功能明确一些。

功能说明

  • 方案采取页面激活(visibilitychange)来比较版本信息
  • 使用页面聚焦(focus)来比较版本信息
  • 可以不强制用户一定刷新页面,需要配置取消操作,怕用户正进行一些表单的操作或者重要信息的操作。
  • 万变不离其宗,目的就是为了完成页面刷新。下列代码在 main 里面调用一下就完成了。
import { ElMessageBox } from 'element-plus'

const isDev = import.meta.env.MODE === 'development'
const oneMinutes = 1 * 60 * 1000

const versioned = (url) =>
  new Promise((resolve, reject) => {
    fetch(`${url}?_=${Date.now()}`)
      .then((res) => res.json())
      .then(resolve)
      .catch(reject)
  })

export class RefreshBrowserScript {
  url = '/version.json'
  version = window.__version__
  status = false
  now = 0

  constructor() {
    if (isDev) return

    window.addEventListener('focus', (this.__focus__fn__ = this.focus.bind(this)))
    document.addEventListener(
      'visibilitychange',
      (this.__visibilitychange__fn__ = this.visibilitychange.bind(this))
    )

    this.execute()
  }

  visibilitychange() {
    if (document.visibilityState !== 'visible') return

    this.execute()
  }

  focus() {
    if (this.now + oneMinutes >= Date.now()) return
    this.now = Date.now()
    
    this.execute()
  }

  async execute() {
    if (this.status) return
    const version = await this.fetchData()
    const samecase = this.equalVersion(this.version, version)
    if (samecase) return

    this.throwMessage()
    this.bindEvent()
    this.status = true
  }

  fetchData() {
    return versioned(this.url)
  }

  bindEvent() {
    document.addEventListener('keypress', this.refreshBrowser)
  }

  unbingEvent() {
    document.removeEventListener('keypress', this.refreshBrowser)
  }

  throwMessage() {
    return ElMessageBox.alert('发现新版本,请刷新浏览器(按任意键刷新页面)。', '版本提示', {
      type: 'warning',
      showCancelButton: true,
      confirmButtonText: '刷新页面',
      cancelButtonText: '不刷新,继续留在当前页面',
    })
      .then(this.refreshBrowser)
      .catch(() => {
        this.skipRefresh()
        this.unbingEvent()
      })
  }

  equalVersion(privite, resource) {
    return privite.commitId === resource.commitId
    // return privite.code === resource.code // 不使用 commit hash,使用randomString
  }

  skipRefresh() {
    window.removeEventListener('focus', this.__focus__fn__)
    document.removeEventListener('visibilitychange', this.__visibilitychange__fn__)
    this.status = false
  }

  refreshBrowser() {
    window.location.reload()
  }
}