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

背景

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

点此跳转Github查看源码

功能要求

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

功能实现

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

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

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

    代码如下:

    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

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值
async init() {
  this.oldConfig = await this.getConfig()
}
  1. 开启轮询,对比hash是否变化
// 开始检查
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. 支持用户自定义更新和未更新的事件回调
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. 默认添加应用更新弹窗提醒
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
              },
            })
          }
        })
      }
    }
  /* ... */
}

如何使用

安装

// npm
npm i unplugin-app-update -S
// pnpm
pnpm i unplugin-app-update -S
// yarn
yarn add unplugin-app-update

Webpack

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

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

Vite

// vite.config.ts
import appUpdate from "unplugin-app-update/vite"

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

入口配置

// 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选项

属性类型描述默认值
urlStringconfig.json配置文件urlconfig.json
intervalNumber轮询时间间隔30000
locatezh_CN | en_US国际化Default language of browser
customBoolean设置为true可删除默认弹出提醒,添加on(‘update’,fn)事件自定义弹出false

注意

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

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

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

// /public/config.json
{
  hash: "123456"
}

404错误

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

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

最后

  1. 都看到这了,可以给我的Github仓库点个小小的Star吗?这真的对我很重要!重要!重要!欢迎给我提Issue、共建。
  2. 有兴趣可以加我微信号:vgbire,一起了解更多前端资讯。

在这里插入图片描述

前端构建做成一个**单独的服务(Service)**,是现代 CI/CD 微服务架构中的常见做法。它能显著提升开发效率、加快构建速度,并实现前后端解耦。 --- ## ✅ 目标:把 Superset 前端构建从主镜像中剥离出来,作为一个独立可复用的构建服务 ### 应用场景: - 开发时频繁修改前端代码 - 多个环境(dev/staging/prod)共享同一份构建产物 - 避免每次构建都重新安装 Node.js 依赖 --- ## 🛠️ 方案一:使用 `docker-compose` 启动独立前端构建服务 ### ✅ 文件结构建议 ```bash project/ ├── docker-compose.yml ├── superset-frontend/ │ ├── package.json │ ├── package-lock.json │ └── ... # 所有前端源码 ├── dist/ # 构建输出目录(挂载卷) └── Dockerfile.superset # 主镜像引用 dist/ ``` --- ### ✅ `docker-compose.yml` ```yaml version: '3.8' services: # ================================ # 独立前端构建服务 # ================================ frontend-builder: image: node:18-bullseye-slim working_dir: /app/superset-frontend volumes: - ./superset-frontend:/app/superset-frontend - ../dist:/app/superset-frontend/build # 输出到共享目录 environment: - NODE_OPTIONS=--max-old-space-size=4096 command: > sh -c " npm config set registry https://registry.npmmirror.com && npm config set cache /tmp/npm-cache && echo '✅ Installing dependencies...' && npm ci && echo '🏗️ Building frontend...' && npm run build && echo '🎉 Build completed at $$(date)' && ls -la ./build " # 可选:挂载缓存目录以加速下次构建 tmpfs: - /tmp/npm-cache:exec,mode=1777 # ================================ # 主应用服务(可选) # ================================ superset: build: context: . dockerfile: Dockerfile.superset ports: - "8088:8088" depends_on: - frontend-builder # 挂载已构建好的静态资源 volumes: - ./dist:/app/superset/static/dist ``` --- ### ✅ 使用方法 #### 1. 构建并运行前端服务(仅构建一次) ```bash # 构建并运行前端(生成 dist 文件夹内容) docker-compose run --rm frontend-builder ``` > 这会在本地 `./dist` 目录下生成 `index.html`, `static/js/*.js` 等文件 #### 2. 构建主镜像(复用 dist) 你的 `Dockerfile.superset` 中可以这样写: ```dockerfile FROM apache/superset:5.0.0 USER root # 安装额外 Python 等... # 复制外部构建的前端资源 COPY dist /app/superset/static/dist USER superset ``` #### 3. 构建最终镜像 ```bash docker build -t my-superset:custom . ``` --- ## 🚀 方案二:CI/CD 中作为独立 Job(GitHub Actions 示例) ```yaml name: Build Superset Frontend on: push: paths: - 'superset-frontend/**' jobs: build-frontend: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v3 - name: Use Node.js 18 uses: actions/setup-node@v3 with: node-version: 18 cache: 'npm' - name: Install & Build working-directory: superset-frontend run: | npm config set registry https://registry.npmmirror.com npm ci npm run build - name: Upload Artifact uses: actions/upload-artifact@v3 with: name: superset-dist path: superset-frontend/build ``` 然后在部署阶段下载这个 artifact 并注入到容器中。 --- ## 💡 方案三:打包为专用镜像(高级用法) 你可以创建一个专门用于构建前端的 Docker 镜像: ### `Dockerfile.frontend-builder` ```dockerfile FROM node:18-slim WORKDIR /app # 设置淘宝镜像 RUN npm config set registry https://registry.npmmirror.com # 提供构建脚本 COPY build.sh /usr/local/bin/build-frontend RUN chmod +x /usr/local/bin/build-frontend CMD ["build-frontend"] ``` ### `build.sh` ```bash #!/bin/bash set -e echo "📥 Copying source..." cp -r /src/* ./ echo "📦 Installing dependencies..." npm ci echo "🔨 Building frontend..." npm run build echo "📤 Copying build to output..." cp -r build/* /output/ ls -la /output ``` ### 使用方式 ```bash docker run --rm \ -v $(pwd)/superset-frontend:/src \ -v $(pwd)/dist:/output \ frontend-builder ``` --- ## ✅ 优势总结 | 优势 | 说明 | |------|------| | ⚡ 构建更快 | 不再重复安装 Node.js / npm / 依赖 | | 🔁 解耦清晰 | 前端变 → 只重建前端;后端变 → 复用前端产物 | | 🧪 易于调试 | 可单独测试 `npm run build` 是否成功 | | 📦 可复用 | 多个环境共用同一个 `dist/` | | ☁️ CI 友好 | 支持缓存、分阶段执行 | --- ## ❗ 注意事项 1. **确保 `package.json` 版本与 Superset 主项目一致** 2. 如果你用了 `lerna` 或 `yarn workspace`,需调整路径 3. 构建命令可能不是 `npm run build`,请确认 Superset 的实际构建脚本(通常是 `build:prod`) 查看原始 `package.json` 中的 scripts: ```json "scripts": { "build": "webpack --config webpack.config.js", "build:prod": "cross-env NODE_ENV=production npm run build" } ``` → 推荐使用 `"build:prod"` 而非普通 `build` ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Vgbire

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值