什么是热更新?(HMR)
HMR,即热模块替换(Hot Module Replacement),用于实现在运行时动态地替换、添加或删除模块。从而实现实时更新应用程序的部分内容,而无需刷新整个页面或重新加载整个应用。
HMR 可以显著提高开发效率,使开发人员能够快速的查看代码和样式的变更效果。
热更新的工作原理
开发过程中的热更新的主要工作原理就是以下几步:
- 监听文件变化。
- 通知浏览器文件发生变化。
- 获取变化文件更新页面。
利用vite实现热更新
以下是我看到过的比较生动的例子。利用vite的热更新api做的示例。这个是源码的链接。
热更新的代码如下:
javascript
class HotModuleReloadSetup {
constructor() {
this.modules = {};
this.instances = {};
document.body.addEventListener('hot-module-reload', (event) => {
const { newModule } = event.detail;
this.swapModule(newModule)
});
}
swapModule(newModule) {
const name = newModule.default.name;
const oldModule = this.modules[name];
const oldInstance = this.instances[name]
if (!oldModule) return;
const newInstance = new newModule.default();
this.modules[name] = newModule
this.instances[name] = newInstance
}
import(newModule) {
const newInstance = new newModule.default();
const name = newModule.default.name;
this.modules[name] = newModule
this.instances[name] = newInstance
}
}
export default HotModuleReloadSetup;
export function HMREventHandler(newModule) {
const event = new CustomEvent('hot-module-reload', { detail: { newModule } });
document.body.dispatchEvent(event);
}
首先是在文件的最上方设置文件热更新的监听。
javascript
/**----------------./Draw.js--------------------------------*/
import { HMREventHandler } from './HotModuleReloadSetup.js';
if (import.meta.hot) {
import.meta.hot.accept(HMREventHandler)
}
然后在项目中引用:
javascript
/**----------------主文件--------------------------------*/
// Setup HMR
const hmr = new HotModuleReloadSetup();
// Load a module that will be updated dynamically
hmr.import(await import('./Draw.js'));
// Now we access it through hmr.instances['Draw']
// which will point to the new module when it gets swapped
function draw() {
hmr.instances['Draw'].draw(canvas);
requestAnimationFrame(draw);
}
从上面的例子中看到我们主要是利用了import.meta.hot.accept 来进行热更新的。该方法主要接收被更改的文件模块。在我们的日常项目中其实也是根据这个方法来进行热更的。
那项目中我们并没有引入这个方法,我们是在什么地方监听的呢。其实在vite启动的项目中vite-plugin-vue已经帮我们处理了这个问题。具体代码如下:
javascript
output.push(
`import.meta.hot.accept(mod => {`,
` if (!mod) return`,
` const { default: updated, _rerender_only } = mod`,
` if (_rerender_only) {`,
` __VUE_HMR_RUNTIME__.rerender(updated.__hmrId, updated.render)`,
` } else {`,
` __VUE_HMR_RUNTIME__.reload(updated.__hmrId, updated)`,
` }`,
`})`,
)
当监听到文件发生变更就执行rerender,和reload的方法。rerender以及reload由vue或者react的框架去提供。
接收端我们知道怎么配置了。那监听文件并发送文件变化的配置在什么地方呢,packages/vite/src/node/server/index源码主要在vite文件的这个路径下。
javascript
// 监听文件变更
watcher.on('change', async (file) => {
file = normalizePath(file)
// invalidate module graph cache on file change
moduleGraph.onFileChange(file)
await onHMRUpdate(file, false)
})
// 处理文件热更新
const onHMRUpdate = async (file: string, configOnly: boolean) => {
if (serverConfig.hmr !== false) {
try {
await handleHMRUpdate(file, server, configOnly)
} catch (err) {
ws.send({
type: 'error',
err: prepareError(err),
})
}
}
}
packages/vite/src/node/server/hmr.ts
javascript
export async function handleHMRUpdate(
file: string,
server: ViteDevServer,
configOnly: boolean,
): Promise<void> {
const { ws, config, moduleGraph } = server
const shortFile = getShortName(file, config.root)
const fileName = path.basename(file)
const isConfig = file === config.configFile
const isConfigDependency = config.configFileDependencies.some(
(name) => file === name,
)
const isEnv =
config.inlineConfig.envFile !== false &&
(fileName === '.env' || fileName.startsWith('.env.'))
if (isConfig || isConfigDependency || isEnv) {
// auto restart server
debugHmr?.(`[config change] ${colors.dim(shortFile)}`)
config.logger.info(
colors.green(
`${path.relative(process.cwd(), file)} changed, restarting server...`,
),
{ clear: true, timestamp: true },
)
try {
await server.restart()
} catch (e) {
config.logger.error(colors.red(e))
}
return
}
if (configOnly) {
return
}
debugHmr?.(`[file change] ${colors.dim(shortFile)}`)
// (dev only) 客户端本身无法进行热更新.刷新页面
if (file.startsWith(normalizedClientDir)) {
ws.send({
type: 'full-reload',
path: '*',
})
return
}
const mods = moduleGraph.getModulesByFile(file)
// check if any plugin wants to perform custom HMR handling
const timestamp = Date.now()
const hmrContext: HmrContext = {
file,
timestamp,
modules: mods ? [...mods] : [],
read: () => readModifiedFile(file),
server,
}
for (const hook of config.getSortedPluginHooks('handleHotUpdate')) {
const filteredModules = await hook(hmrContext)
if (filteredModules) {
hmrContext.modules = filteredModules
}
}
if (!hmrContext.modules.length) {
// html file cannot be hot updated 如果是html文件页面刷新
if (file.endsWith('.html')) {
config.logger.info(colors.green(`page reload `) + colors.dim(shortFile), {
clear: true,
timestamp: true,
})
ws.send({
type: 'full-reload',
path: config.server.middlewareMode
? '*'
: '/' + normalizePath(path.relative(config.root, file)),
})
} else {
// loaded but not in the module graph, probably not js
debugHmr?.(`[no modules matched] ${colors.dim(shortFile)}`)
}
return
}
// 模块更新
updateModules(shortFile, hmrContext.modules, timestamp, server)
}
// 模块更新
export function updateModules(
file: string,
modules: ModuleNode[],
timestamp: number,
{ config, ws, moduleGraph }: ViteDevServer,
afterInvalidation?: boolean,
): void {
const updates: Update[] = []
const invalidatedModules = new Set<ModuleNode>()
const traversedModules = new Set<ModuleNode>()
let needFullReload = false
for (const mod of modules) {
const boundaries: { boundary: ModuleNode; acceptedVia: ModuleNode }[] = []
const hasDeadEnd = propagateUpdate(mod, traversedModules, boundaries)
moduleGraph.invalidateModule(
mod,
invalidatedModules,
timestamp,
true,
boundaries.map((b) => b.boundary),
)
if (needFullReload) {
continue
}
if (hasDeadEnd) {
needFullReload = true
continue
}
updates.push(
...boundaries.map(({ boundary, acceptedVia }) => ({
type: `${boundary.type}-update` as const,
timestamp,
path: normalizeHmrUrl(boundary.url),
explicitImportRequired:
boundary.type === 'js'
? isExplicitImportRequired(acceptedVia.url)
: undefined,
acceptedPath: normalizeHmrUrl(acceptedVia.url),
})),
)
}
if (needFullReload) {
config.logger.info(colors.green(`page reload `) + colors.dim(file), {
clear: !afterInvalidation,
timestamp: true,
})
ws.send({
type: 'full-reload',
})
return
}
if (updates.length === 0) {
debugHmr?.(colors.yellow(`no update happened `) + colors.dim(file))
return
}
config.logger.info(
colors.green(`hmr update `) +
colors.dim([...new Set(updates.map((u) => u.path))].join(', ')),
{ clear: !afterInvalidation, timestamp: true },
)
// 发送ws请求热更新。
ws.send({
type: 'update',
updates,
})
}
以上代码就是vite中的监听文件变化并且进行热更新的代码。