背景
最近发现很多手机APP和小程序在打开或者使用过程中会弹框提示用户新版本发布,是否更新。我就想着自己能不能实现一个。
功能要求
- 纯前端实现,不限制框架用React、Vue、Angular都可
- 不限制打包工具Webpack、Vite都可使用
- 使用发布订阅模式实现,支持用户自定义更新提醒事件
- 发布到npm,安装后开箱即用
功能实现
功能实现也非常简单,大致实现如下:
- 打包构建的时候拿到webpack或者vite的打包
id
,通过Node的fs
模块将其放到打包(/dist)目录下(最好是可访问的静态资源里面,例如/public/config.json)。
js
// /public/config.json
{
hash: "123456"
}
-
我们需要写一个webpack或者vite插件在打包构建完成后拿到hash生成文件,然后输出到/dist目录里,还好我们有unplugin,一套代码可以适配webpack、vite插件。
注意:Webpack的afterEmit钩子里有提供当前打包构建的hash值,而Vite没有(希望只是我没有找到,有没有大佬告诉我Vite哪个钩子可以获取到打包构建的hash值)所以我是自己生成了uuid作为hash
代码如下:
jsimport { 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) } ) }, } })
-
然后前端通过轮询这个文件,对比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)
}
/* ... */
- 然后我们需要在首次进入页面时加载一次当前配置文件获取初始hash值
js
async init() {
this.oldConfig = await this.getConfig()
}
- 开启轮询,对比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()
})
}
- 支持用户自定义更新和未更新的事件回调
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)
}
}
- 默认添加应用更新弹窗提醒
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: "/",
}
}
最后
- 都看到这了,可以给我的Github仓库点个小小的Star吗?这真的对我很重要!重要!重要!欢迎给我提Issue、共建。
- 有兴趣可以加我微信号:vgbire,一起了解更多前端资讯。