解密 Vite 的热更新魔法

什么是热更新?(HMR)

HMR,即热模块替换(Hot Module Replacement),用于实现在运行时动态地替换、添加或删除模块。从而实现实时更新应用程序的部分内容,而无需刷新整个页面或重新加载整个应用。

HMR 可以显著提高开发效率,使开发人员能够快速的查看代码和样式的变更效果。

热更新的工作原理

开发过程中的热更新的主要工作原理就是以下几步:

  1. 监听文件变化。
  2. 通知浏览器文件发生变化。
  3. 获取变化文件更新页面。

利用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中的监听文件变化并且进行热更新的代码。

参考文档:

相关推荐
gqkmiss12 分钟前
Chrome 浏览器 131 版本开发者工具(DevTools)更新内容
前端·chrome·浏览器·chrome devtools
Summer不秃18 分钟前
Flutter之使用mqtt进行连接和信息传输的使用案例
前端·flutter
旭日猎鹰22 分钟前
Flutter踩坑记录(二)-- GestureDetector+Expanded点击无效果
前端·javascript·flutter
Viktor_Ye28 分钟前
高效集成易快报与金蝶应付单的方案
java·前端·数据库
hummhumm30 分钟前
第 25 章 - Golang 项目结构
java·开发语言·前端·后端·python·elasticsearch·golang
乐闻x1 小时前
Vue.js 性能优化指南:掌握 keep-alive 的使用技巧
前端·vue.js·性能优化
一条晒干的咸魚1 小时前
【Web前端】创建我的第一个 Web 表单
服务器·前端·javascript·json·对象·表单
Amd7941 小时前
Nuxt.js 应用中的 webpack:compiled 事件钩子
前端·webpack·开发·编译·nuxt.js·事件·钩子
生椰拿铁You1 小时前
09 —— Webpack搭建开发环境
前端·webpack·node.js
狸克先生2 小时前
如何用AI写小说(二):Gradio 超简单的网页前端交互
前端·人工智能·chatgpt·交互