热更新,又名模块热替换:Hot Module Replacement,简称HMR,无需完全刷新整个页面的同时,更新模块。HMR主要用于提升开发体验。
页面的刷新有多种情况:页面级别的刷新,简单粗暴,但是不保留页面状态。即:window.location.reload(),
刀耕火种时代的页面更新都是采用这种方法,写完代码之后页面并不能及时看到效果。需要手动刷新页面,或者触发reload。
后来webpack提供了更为友好的开发体验,只更新局部模块,而不是刷新页面。在代码变更之后自动触发对应的更新。同时保留页面的状态(输入框的值、选择器的选中等)。
我们首先知道webpack热更新原理:
想要了解更详细的可以搜webpack热更新,此处不多做赘述。
我们了解了大概原理之后,就可以在没有webpack的时候自己实现。
浏览器插件的开发在代码编写完之后需要扔到浏览器扩展应用里面才可以看到效果,并且在代码变更之后是需要手动刷新,这时候我们就没有webpack工具来给我们提供热更新的支持。
我在尝试用vite开发浏览器插件的时候,就在思考,我们可以通过热更新一样的原理去帮插件自动更新。
同样的原理:
const ws = new WebSocket('ws://localhost:2333')
ws.onmessage = (event) => {
console.log('reload trigger', event)
let msg = JSON.parse(event.data)
if (msg === 'reload-app') {
chrome.runtime.reload()
}
if(msg === 'reload-window') {
window.location.reload()
}
}
在插件的首页我们与本地建立一个ws的连接,来接受本地代码变更之后发送的消息。
当接收到reload-window时,执行页面的重刷新。
当接收到reload-app时,执行插件的重刷新,这个方法相当于在插件扩展页面点击刷新按钮。
在vite的vite.config.ts配置文件我们加入自定义的hrm plugin
import { defineConfig, loadEnv } from 'vite'
import react from '@vitejs/plugin-react'
import { HRMMiddleware } from './rollup-plugins/hrm'
// https://vitejs.dev/config/
export default ({ mode }) => {
const env = loadEnv(mode, __dirname)
const isDev = env.VITE_NODE_ENV = 'development'
const plugins: any[] = [react()]
if(isDev) plugins.push(HRMMiddleware())
return defineConfig({
build: {
emptyOutDir: false
},
plugins
})
}
当前环境为开发模式的话插入hrm的pulgin。
import { ConfigEnv, UserConfig } from "vite"
import WebSocket, { WebSocketServer } from "ws"
export const HRMMiddleware = () => {
let wsClient: WebSocket | null
let socketServer : WebSocketServer | null
console.log('HRM middleware in', '-----------')
// 发送通知
const send = (msg) => {
console.log('before send meg', msg)
if (!wsClient) return
msg = JSON.stringify(msg)
wsClient.send(msg)
console.log('sended meg', msg)
}
const close = () => {
wsClient && wsClient.close()
wsClient = null
socketServer = null
}
return {
name: 'hrm-plugin',
apply(config: UserConfig, { command }: ConfigEnv) {
// build 且 watch 的情况下插件生效
const canUse = command === 'build' && Boolean(config.build?.watch)
if (canUse) {
// 创建 websocket server
socketServer = new WebSocketServer({ port: 2333 })
socketServer.on('connection', (client) => { wsClient = client })
}
return canUse
},
// popup页面发生变动,重新加载window即可。
closeBundle: () => send('reload-window'),
closeWatcher: () => close()
}
}
设置好了之后,我们在vite开发的时候使用watch模式,当代码变化之后重新打包会触发页面的更新。
不过这里还有个问题就是:
当插件的配置文件变更了之后,不会自动刷新插件。也就是配置不生效。
apply(config: UserConfig, { command }: ConfigEnv) {
// build 且 watch 的情况下插件生效
const canUse = command === 'build' && Boolean(config.build?.watch)
if (canUse) {
// 创建 websocket server
socketServer = new WebSocketServer({ port: 2333 })
socketServer.on('connection', (client) => { wsClient = client })
// public 文件发生变动,需要reload插件。
chokidar
.watch([PUBLIC_DIR, CONTENT_FILE], { ignoreInitial: true })
.on('all', debounce((event, path) => {
console.log(event, path)
if(path.includes('/public/')) {
const dest = resolve(__dirname, `../dist/${path.split('/').pop()}`)
console.log(`copy file ${path} to ${dest}`)
fs.copyFileSync(path, dest)
}
send('reload-app')
}))
}
return canUse
},
这里和webpack一样使用chokidar模块去监听配置文件的变化,然后触发reload-app。