本文旨在分析vite hmr的实现原理,并附上相关官方源码地址和官网地址。
hmr有两种解释,一种是hot module refresh,模块热更新,指的是监听文件的变化,重新编译文件然后告诉前端刷新整个页面。另一种就是我们今天说的hot module replacement,模块热替换,指的是监听文件的变化,重新编译文件,告诉前端更新的文件,重新加载此文件。
实现原理
Vite 以 原生 ESM 方式服务源码,只需要在浏览器请求源码时先进行转换再返回转换后的源码。基于这种方式,vite hmr的实现要比webpack的hrm实现更简单更快速。
大致过程是:
- 创建一个websocket服务端。
- 创建一个ws client文件,并在html中引入,加载ws client文件。
- 服务端监听文件变化,发送websocket消息,告诉客户端变化类型,变化文件等。
- 客户端接受到消息,根据消息内容决定重新刷新页面还是重新加载变化文件,并执行相关文件注入ws client时设置的hmr hook函数。
源码分析
创建一个websocket服务端。
在createServer函数内调用createWebSocketServer,在createWebSocketServer中调用ws
库创建ws服务端。
import WebSocket from 'ws';
function createWebSocketServer(server, config, httpsOptions?) {
// ...
let wss;
const hmr = typeof config.server.hmr === 'object' && config.server.hmr;
const wsServer = (hmr && hmr.server) || server;
// 创建WebSocket服务,noServer开启无服务器模式
wss = new WebSocket.Server({ noServer: true });
wsServer.on('upgrade', (req, socket, head) => {
wss.handleUpgrade(req, socket, head, (ws) => {
wss.emit('connection', ws, req);
});
});
// ...
return {
send(payload) {
// 发送客户端消息
const stringified = JSON.stringify(payload);
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(stringified);
}
});
},
close() {
return new Promise((resolve, reject) => {
// 关闭服务
wss.close((err) => {
// ...
});
});
},
};
}
创建一个ws client文件,并在html中引入,加载ws client文件。
vite的websocket客户端源码在这里。在处理index.html文件时,把对ws client的引入注入到index.html文件中。浏览器访问index.htm就会加载ws client文件并执行,创建客户端ws,接收ws服务端信息。
过程如下:
-
处理index.html时,在执行vite 独有hook
transformIndexHtml
时把''注入到文件中。(源码地址) -
在参数分析章节中讲过,在解析alias别名参数时,vite内部额外添加了解析 /@vite 别名的配置,并将其指引到当前项目的node_module/vite/dist/client/下。所有当加载/@vite/client时,实际返回的是node_module/vite/dist/client/client.js。
-
在插件分析章节提过,vite内部加入的插件中,clientInjectionsPlugin插件是专门解析ws client文件的,作用就是直接替换掉ws client文件里创建ws 客户端所需要的变量。
-
在ws client 文件中,通过Websocket创建ws客户端,并接受服务端信息。
// client.js // socketProtocol socketHost由clientInjectionsPlugin插件替换 // 创建ws客户端 const socket = new WebSocket(`${socketProtocol}://${socketHost}`, 'vite-hmr') const base = __BASE__ || '/' // 接受服务端信息 socket.addEventListener('message', async ({ data }) => { handleMessage(JSON.parse(data)) })
查看自己本地运行的vite项目,查看页面源码和client文件即可验证。
服务端监听文件变化,发送websocket消息,告诉客户端变化类型,变化文件等。
vite 使用chokidar
来监听文件 ,源码入口:
const watcher = chokidar.watch(root,...args);
watcher.on('change', async (file) => {
if (serverConfig.hmr !== false) {
await handleHMRUpdate(file, server)
}
})
watcher.on('add', (file) => {
handleFileAddUnlink(...args)
})
watcher.on('unlink', (file) => {
handleFileAddUnlink(...args)
})
handleHMRUpdate函数会发送消息给客户端,根据此次修改文件的类型告诉客户端是要刷新还是重新加载文件。
function handleHMRUpdate(...args){
// ... 伪代码
// ... 判断hmr 类型
// 执行plugin.handleHotUpdate hook
for (const plugin of config.plugins) {
if (plugin.handleHotUpdate) {
// ...
}
}
// 发送客户端消息
ws.send({
type: 'update'|'full-reload',
path:'',// 文件修改路径
updates:{}// 此次更新信息
})
}
客户端接受到消息,根据消息内容决定重新刷新页面还是重新加载变化文件,并执行相关文件注入ws client时设置的hmr hook函数。
客户端接收到消息后,根据消息区分是刷新页面还是重新加载文件,加载文件的话是加载css类型还是js类型文件。如果是css,则根据返回的path直接重新加载.
let { path, timestamp } = update
path = path.replace(/\?.*/, '')
// 根据path搜索link标签
// 重新设置link.href,重新加载
// 添加t=${timestamp}是为了避免加载浏览器缓存
const el = (
[].slice.call(
document.querySelectorAll(`link`)
)
).find((e) => e.href.includes(path))
if (el) {
const newPath = `${path}${
path.includes('?') ? '&' : '?'
}t=${timestamp}`
el.href = new URL(newPath, el.href).href
}
如果是js类型文件,则会先重新加载文件,然后执行在当前js文件类注册的hmr hook。
// client.js
if (update.type === 'js-update') {
queueUpdate(fetchUpdate(update))
}
// fetchUpdate 重新加载文件
function fetchUpdate(update){
// ...
const {base,path,timestamp,query}=update
const newMod = await import(base +path.slice(1) +`?import&t=${timestamp}${query ? `&${query}` : ''}`)
// ...
}
// queueUpdate 执行hmr hook 回调
function queueUpdate(){
// ...
;(await Promise.all(loading)).forEach((fn) => fn && fn())
// ...
}
最后
ws client文件没有详细讲解,主要是讲个大体思路。