项目部署更新提醒

128 阅读2分钟

仅记录,参考juejin.cn/post/718545…

注意

由于是通过监测打包后的index.html地址中的hash值(每次打包后都会并不一样)一致性来判断是否更新,因此index.html这个文件是需要nginx配置不缓存的,否则用户就算手动刷新了也不会更新。

import { Notification, Button } from '@arco-design/web-react'
class Updater {
  constructor (options) {
    this.oldScript = []
    this.newScript = []
    this.dispatch = {
      'no-update': [],
      update: []
    }
    this.init() //初始化
    this.timing(options?.timer) //轮询
  }

  async init () {
    const html = await this.getHtml()
    this.oldScript = this.parserScript(html)
  }

  async getHtml () {
    const html = await fetch('/').then(res => res.text()) //读取index html
    return html
  }

  parserScript (html) {
    const reg = new RegExp(/<script(?:\s+[^>]*)?>(.*?)<\/script\s*>/gi) //script正则
    return html.match(reg) //匹配script标签
  }

  //发布订阅通知
  on (key = 'no-update' | 'update', fn) {
    this.dispatch[key].push(fn)
    return this
  }

  compare (oldArr, newArr) {
    const base = oldArr.length
    const arr = Array.from(new Set(oldArr.concat(newArr)))
    //如果新旧length 一样无更新
    if (arr.length === base) {
      this.dispatch['no-update'].forEach(fn => {
        fn(this.newScript)
      })
    } else {
      //否则通知更新
      this.dispatch['update'].forEach(fn => {
        fn(this.newScript)
      })
    }
  }

  timing (time = 10000) {
    //轮询
    setInterval(async () => {
      const newHtml = await this.getHtml()
      this.newScript = this.parserScript(newHtml)
      this.compare(this.oldScript, this.newScript)
    }, time)
  }
}

const updateNotification = () => {
  const updater = new Updater({
    timer: 5000
  })
  // updater.on('no-update', val => {
  //   const id = val
  //   Notification.success({
  //     id,
  //     closable: false,
  //     title: 'Update notification',
  //     content: 'Please refresh for the latest version!',
  //     showIcon: true,
  //     position: 'bottomRight',
  //     duration: 0,
  //     btn: (
  //       <span>
  //         <Button
  //           type="primary"
  //           size="small"
  //           onClick={() => {
  //             window.location.reload()
  //           }}
  //         >
  //           Refresh now
  //         </Button>
  //       </span>
  //     )
  //   })
  // })
  //更新通知
  updater.on('update', val => {
    console.log('update')
    const id = val
    Notification.info({
      id,
      closable: false,
      title: 'Update notification',
      content: 'Please refresh for the latest version!',
      showIcon: true,
      position: 'bottomRight',
      duration: 0,
      btn: (
        <span>
          <Button
            type="primary"
            size="small"
            onClick={() => {
              window.location.reload()
            }}
          >
            Refresh now
          </Button>
        </span>
      )
    })
  })
}

export default updateNotification


优化轮询操作

利用 web worker 多线程,优化轮询操作带来的性能消耗

注意

react 使用worker 直接引用worker.js文件是失效的,原因Webpack 或其他构建工具不能正确处理 Web Worker 文件。默认情况下,Webpack 会将 JavaScript 文件视为模块并进行相应的处理。

解决方法:

  1. 下载webpack worker-loader 插件
// webpack.config
module.exports = {
  // ...
  module: {
    rules: [
      // ...
      {
        test: /\.worker\.js$/,
        use: {
          loader: 'worker-loader',
          options: {
            inline: true, // 将 Worker 作为 Blob 内联到打包后的文件中
            fallback: false // 不使用额外的 Worker 文件
          }
        }
      },
      // ...
    ]
  },
  // ...
};

上述配置告诉 Webpack 对以 .worker.js 结尾的文件使用 worker-loader 进行处理,并将 Web Worker 内联到打包后的文件中。

// App.js
import MyWorker from './worker.js';

function MyComponent() {
  useEffect(() => {
    const worker = new MyWorker();

    worker.onmessage = function (event) {
      // 处理从 Web Worker 返回的结果
      const result = event.data;
      // 更新组件状态或执行其他操作
    };

    worker.postMessage('message');

    return () => {
      worker.terminate();
    };
  }, []);

  // ...
}

注意,我们使用 import MyWorker from './worker.js' 来导入 Web Worker,而不是直接使用文件路径。

  1. 直接在public目录下创建workerjs文件下即可
// worker.js
class Updater {
  ....同上
}

onmessage = function (event) {
  // const data = event.data

  const updater = new Updater()

  //更新通知
  updater.on('update', val => {
    // 将结果发送回主线程
    postMessage(val)
  })
}

// App.js
useEffect(() => {
    var worker = new Worker('worker.js')

    // 向 Web Worker 发送消息
    worker.postMessage(null)

    // 接收 Web Worker 的消息
    worker.onmessage = function (event) {
      const result = event.data
      if (result) {
        // 提示弹窗
        Notification.info({
          id: result,
          closable: false,
          title: 'Update notification',
          content: 'Please refresh for the latest version!',
          showIcon: true,
          position: 'bottomRight',
          duration: 0,
          btn: (
            <span>
              <Button
                type="primary"
                size="small"
                onClick={() => {
                  window.location.reload()
                }}
              >
                Refresh now
              </Button>
            </span>
          )
        })
      }
    }

    return () => {
      worker.terminate()
    }
  }, [])


因为这个轮询逻辑比较简单,所以直接选择第二种方法即可