我把前端部署用户提醒做成了一个npm包

背景

最近发现很多手机APP和小程序在打开或者使用过程中会弹框提示用户新版本发布,是否更新。我就想着自己能不能实现一个。

点此跳转Github查看源码

功能要求

  • 纯前端实现,不限制框架用React、Vue、Angular都可
  • 不限制打包工具Webpack、Vite都可使用
  • 使用发布订阅模式实现,支持用户自定义更新提醒事件
  • 发布到npm,安装后开箱即用

功能实现

功能实现也非常简单,大致实现如下:

  1. 打包构建的时候拿到webpack或者vite的打包id,通过Node的fs模块将其放到打包(/dist)目录下(最好是可访问的静态资源里面,例如/public/config.json)。
js 复制代码
// /public/config.json
{
  hash: "123456"
}
  1. 我们需要写一个webpack或者vite插件在打包构建完成后拿到hash生成文件,然后输出到/dist目录里,还好我们有unplugin,一套代码可以适配webpack、vite插件。

    注意:Webpack的afterEmit钩子里有提供当前打包构建的hash值,而Vite没有(希望只是我没有找到,有没有大佬告诉我Vite哪个钩子可以获取到打包构建的hash值)所以我是自己生成了uuid作为hash

    代码如下:

    js 复制代码
    import { createUnplugin } from "unplugin"
    import { writeFileSync, mkdirSync } from "node:fs"
    import path from "node:path"
    
    // 生成config.json文件
    const generateConfig = (configPath: string, hash: string) => {
      // 确保目录存在
      mkdirSync(path.dirname(configPath), { recursive: true })
      writeFileSync(
        configPath,
        JSON.stringify(
          {
            hash,
          },
          null,
          2
        )
      )
    }
    
    // filePath不传默认生成到/dist目录下
    export default createUnplugin((filePath: string = "config.json") => {
      let viteOutDir = "dist"
      return {
        name: "unplugin-app-update",
        // 生成Vite插件
        vite: {
          configResolved(config) {
            viteOutDir = config.build.outDir
          },
          closeBundle() {
            const configPath = path.join(process.cwd(), viteOutDir, filePath)
            generateConfig(configPath, uuid())
          },
        },
        // 生成webpack插件
        webpack(compiler) {
          // 只在生产模式下生成文件
          if (compiler.options.mode !== "production") {
            return
          }
          compiler.hooks.afterEmit.tap(
            "unplugin-app-update",
            (compilation: any) => {
              const configPath = path.join(
                compiler.options.output.path as string,
                filePath
              )
              generateConfig(configPath, compilation.hash)
            }
          )
        },
      }
    })
  2. 然后前端通过轮询这个文件,对比hash有变化则打开一个弹窗提醒用户有新功能发布。

    我用发布订阅模式写了一个AppUpdate的类去实现这个功能,。

    首先我们需要有一个获取config.json配置文件的方法,为了不引入axios直接使用fetch API

js 复制代码
export class AppUpdate {
  // 请求配置文件的url,默认不传请求url: http://your.website.com/config.json
  constructor({url = "config.json"}) {
    this.url = url
  }
  /* ... */
  async getConfig() {
    const config = await fetch(this.url, {
      // 强制开启协商缓存
      headers: { "Cache-Control": "max-age=0" },
    }).then((res) => res.text())
    return JSON.parse(config)
  }
  /* ... */
  1. 然后我们需要在首次进入页面时加载一次当前配置文件获取初始hash值
js 复制代码
async init() {
  this.oldConfig = await this.getConfig()
}
  1. 开启轮询,对比hash是否变化
js 复制代码
// 开始检查
check() {
  this.stop()
  this.timer = setInterval(async () => {
    this.newConfig = await this.getConfig()
    this.compare()
  }, this.interval) as unknown as number
}

// 停止检查
stop() {
  clearInterval(this.timer)
}

// 对比
compare() {
  if (this.newConfig.hash === this.oldConfig.hash) {
    this.dispatch("notUpdate")
  } else {
    this.oldConfig = this.newConfig
    this.newConfig = {}
    this.dispatch("update")
  }
}

// 触发事件
dispatch(key: "notUpdate" | "update") {
  this.callbacks[key]?.forEach((callback: AnyMethod) => {
    callback()
  })
}
  1. 支持用户自定义更新和未更新的事件回调
js 复制代码
on(key: "notUpdate" | "update", callback: AnyMethod) {
  ;(this.callbacks[key] || (this.callbacks[key] = [])).push(callback)
}

off(key: "notUpdate" | "update", callback: AnyMethod) {
  const index = (this.callbacks[key] || (this.callbacks[key] = [])).findIndex(
    (item) => item === callback
  )
  if (index !== -1) {
    this.callbacks[key].splice(index, 1)
  }
}
  1. 默认添加应用更新弹窗提醒
js 复制代码
export class AppUpdate {
    // 初始化
    constructor({
      url = "config.json",
      interval = 30000,
      locate,
      custom = false,
    }) {
      // 国际化,默认为当前浏览器语言
      if (locate) {
        i18n.changeLanguage(locate)
      }
      this.url = url
      this.interval = interval
      // 初次获取config文件hash值
      this.init()
      // 开始轮询
      this.check()
      // 添加默认提醒事件,自定义设置可设置custom: true
      if (!custom) {
        this.on("update", () => {
          if (!this.modal) {
            this.modal = Modal.confirm({
              title: i18n.t("updateModelTitle"),
              content: i18n.t("updateModelContent"),
              style: {
                top: 200,
              },
              okText: i18n.t("comfirm"),
              cancelText: i18n.t("cancel"),
              onOk: () => {
                window.location.reload()
              },
              onCancel: () => {
                this.modal = null
              },
            })
          }
        })
      }
    }
  /* ... */
}

如何使用

安装

js 复制代码
// npm
npm i unplugin-app-update -S
// pnpm
pnpm i unplugin-app-update -S
// yarn
yarn add unplugin-app-update

Webpack

js 复制代码
// webpack.config.js
const appUpdate = require("unplugin-app-update/webpack")

module.exports = {
  /* ... */
  plugins: [
    appUpdate('/path/to/config.json'),
  ],
}

Vite

js 复制代码
// vite.config.ts
import appUpdate from "unplugin-app-update/vite"

export default defineConfig({
  plugins: [
    // default to the dist directory
    appUpdate(),
  ],
})

入口配置

js 复制代码
// main.js or index.js
import { AppUpdate } from "unplugin-app-update"
const appUpdate = new AppUpdate({ /* Options */ })

// 停止轮询
// appUpdate.stop()
// 继续轮询
// appUpdate.check()

// 事件:update, notUpdate
const update = ()=>{
  console.log("Update")
}
// 自定义更新用户提醒
appUpdate.on("update", update)
appUpdate.on("notUpdate", ()=>{
  console.log("Not Update")
})
// 解绑事件
appUpdate.off("update", update)

AppUpdate选项

属性 类型 描述 默认值
url String config.json配置文件url config.json
interval Number 轮询时间间隔 30000
locate zh_CN | en_US 国际化 Default language of browser
custom Boolean 设置为true可删除默认弹出提醒,添加on('update',fn)事件自定义弹出 false

注意

在本地开发时,需要在公共目录中放置一个config.json文件

手动更改哈希值以模拟项目构建

如果webpack或vite调整了公共目录,您应该新建AppUpdate({url:'your/customer/path')

js 复制代码
// /public/config.json
{
  hash: "123456"
}

404错误

在生产和本地开发过程中,配置时经常遇到404错误。找不到json

js 复制代码
// webpack.config.js or vite.config.ts
{
  output: {
    // 修改打包后访问路径
    publicPath: "/",
  }
}

最后

  1. 都看到这了,可以给我的Github仓库点个小小的Star吗?这真的对我很重要!重要!重要!欢迎给我提Issue、共建。
  2. 有兴趣可以加我微信号:vgbire,一起了解更多前端资讯。
相关推荐
Random_index2 小时前
#名词区别篇:npx pnpm npm yarn区别
前端·npm
B.-2 小时前
Remix 学习 - 路由模块(Route Module)
前端·javascript·学习·react·web
不修×蝙蝠3 小时前
Javascript应用(TodoList表格)
前端·javascript·css·html
加勒比海涛4 小时前
ElementUI 布局——行与列的灵活运用
前端·javascript·elementui
你不讲 wood4 小时前
postcss 插件实现移动端适配
开发语言·前端·javascript·css·vue.js·ui·postcss
前端小程4 小时前
使用vant UI实现时间段选择
前端·javascript·vue.js·ui
whyfail4 小时前
React 事件系统解析
前端·javascript·react.js
小tenten5 小时前
js延迟for内部循环方法
开发语言·前端·javascript
幻影浪子6 小时前
Web网站常用测试工具
前端·测试工具
暮志未晚Webgl6 小时前
94. UE5 GAS RPG 实现攻击击退效果
java·前端·ue5