Yongzhi
monorepo工程化实践 - 纯前端网站更新通知
前言
先说一下背景,传统单页面应用打包后,index.html
中的script
标签会引入javascript
文件,一般情况下随着内容的修改javascript
文件名也会随着文件内容的不同而产生变化。这就会导致,用户在使用应用过程中网站重新部署,但是用户的index.html
已经请求到客户端,引入的javascript
文件并不会更新。或者用户在点击按钮时,异步加载javascript
文件,如果文件不存在那么就会出现异常。
方案
目前市面上有很多方案都是依赖打包工具开发插件来实现,这样的优点是对业务没有入侵,但是缺点是使用者可能不太明白原理,并且不好排查问题。
所以本项目不会去开发打包工具的插件。目前的思路是每次打包生成一个时间戳,然后将这个时间戳通过属性绑定到body
标签上,项目运行中通过获取项目中绑定
的时间戳,和远程服务所绑定的时间戳来对比,如果不同,则认为项目更新。
实现
首先需要分析一下用户需求
- 轮询间隔
- 请求地址
- 自定义请求参数
- 绑定的
key
值 - 目标节点
interface FecthInit {
method?: 'get' | 'post',
headers?: Headers,
body?: any,
mode?: any
credentials?: any
cache?: any
redirect?: any
referrer?: any
referrerPolicy?: any
integrity?: any
}
interface Initial {
delay?: number // 轮询间隔时间,单位毫秒, 默认30秒
url?: string // 请求地址,默认`${window.origin}?t=${Date.now()}`
init?: FecthInit // 请求参数, 默认{ method: 'get' }
key?: string // 节点属性, 默认 data-hash
target?: HTMLElement // 目标节点,默认body
loop?: boolean // 当检测到变更后,远程地址的value没有变化,是否再次发出通知 默认false
}
另外,我们可以在web worker
中发送请求发送请求。 在web worker
中进行,会有一些优点
-
防止阻塞主线程:当您发送 AJAX 请求时,它将占用主线程,并阻止您应用程序中其他部分的执行。
-
增加资源利用率:Web Workers 使我们不必依赖单个线程来处理所有工作,因此可以更好地分配系统资源,提高了应用程序的响应速度;
-
更好的安全性:由于工作线程是在独立的上下文中执行,所以无法直接修改主线程的数据,可以大大减少可能存在的安全风险。
所有还需要封装一个web worker
的函数,如果您还不太了解web worker
,您可以查看Web Worker 使用教程
web worker
需要加载一个javascript
文件,为了避免文件路径错误,我们选择使用Blob
将文件创建在内存中,通过Blob url
方式加载。
通过闭包的方式来保存web worker
实例,通过实例与主线程交互
const worker = `
self.onmessage = function (e) {
const { url, init } = e.data
const request = () => {
return fetch(url, init)
}
request().then(async res => {
const result = await res.text()
self.postMessage(result)
})
}
`
const greenlet = () => {
const blob = new Blob([worker], { type: 'application/javascript' })
const workerUrl = URL.createObjectURL(blob)
const workerInstance = new Worker(workerUrl)
return (url: string, init: any): Promise<string> => new Promise((resolve, reject) => {
workerInstance.onmessage = e => {
resolve(e.data)
}
workerInstance.onerror = e => {
console.log(e, 'error')
reject(e)
}
workerInstance.postMessage({ url: url, init: init })
})
}
export const useGreenlet = greenlet()
在实际使用场景中,我们需要在页面加载完成后、当前tab激活时以及轮询时进行比较value
。当进行请求地址时代理服务器返回的其实是html
内容,我们可以当作字符串去解析,并且通过正则拿到value
export function useNotification(params: Initial) {
const getCurrentHash = () => {
const target = params.target || document.querySelector('body')
if (!target) return ''
const hash = target.getAttribute('data-hash')
return hash
}
const regex = new RegExp(`${params.key}\\s*=\\s*['"]([^'"]+)['"]`)
let timer: any
let currentHash = getCurrentHash()
const loop = params.loop || false
const useCreateNotify = (notice: boolean, data: Data) => new CustomEvent('siteUpdate', {
bubbles: true,
detail: { data: data, status: notice }
})
const queryNewHash = useGreenlet.bind(null, params.url || `${window.origin}?t=${Date.now()}`, params.init || {
method: 'get'
})
const validateHash = async () => {
const hash = await queryNewHash()
const data = hash.match(regex)
return data ? data[1] || null : null
}
const initEvent = () => {
window.addEventListener('load', windowLoaded)
document.addEventListener('visibilitychange', handleVisibilityChange)
}
const handleVisibilityChange = async () => {
if (document.visibilityState === 'visible') {
const hash = await validateHash()
if (hash !== currentHash) {
dispatchEvent(true, {
siteHash: hash,
currentHash: currentHash
})
} else {
initTimer()
}
} else {
clearInterval(timer)
}
}
const windowLoaded = async () => {
const hash = await validateHash()
if (hash !== currentHash) {
dispatchEvent(true, {
siteHash: hash,
currentHash: currentHash
})
}
}
const dispatchEvent = (status: boolean, data: Data) => {
const notice = useCreateNotify(status, data)
window.dispatchEvent(notice)
if (!loop) {
currentHash = data.siteHash
}
}
const initTimer = () => {
timer = setInterval(async () => {
const hash = await validateHash()
if (hash !== currentHash) {
dispatchEvent(true, {
siteHash: hash,
currentHash: currentHash
})
}
}, params.delay)
}
if (!currentHash) return
initEvent()
initTimer()
}
- getCurrentHash: 获取当前value
- useCreateNotify: 创建自定义事件
- queryNewHash: 获取最新
html
- validateHash: 获取最新
value
- initEvent: 初始化事件
- handleVisibilityChange: tab切换事件
- windowLoaded: 页面加载完成事件
- dispatchEvent: 触发自定义事件
- initTimer: 轮询请求
使用
搭配webpack
您需要安装
html-webpack-plugin
,下面详细说明了如何使用
module.exports = {
...
plugins: [
...,
new HtmlWebpackPlugin({
title: 'title',
hash: new Data().getTime(),
template: path.resolve(__dirname, '../index.html')
})
],
...
}
在您的模版文件中
<html>
...
<body data-hash="<%= htmlWebpackPlugin.options.hash %>">
...
</body>
</html>
在您项目入口位置
import { useNotification } from '@terky/update-notifier'
...
useNotification({
key: 'data-hash'
})
...
window.addEventListener('siteUpdate', function({ detail }) {
if (detail.data) {
// do something
}
})