普通视图

发现新文章,点击刷新页面。
今天 — 2026年4月6日首页

vite 是如何加载解析 vite.config.js 配置文件的?

作者 米丘
2026年4月6日 10:11

当我们在终端运行 vite dev,Vite 启动开发服务器的首个关键步骤就是解析配置。本文将深入剖析 Vite 加载配置文件的三种模式。

loadConfigFromFile 的完整流程

loadConfigFromFile 是配置文件加载的核心函数,其完整流程如下:

  1. 确定配置文件路径(自动查找或使用 --config 指定的路径)。
  2. 根据文件后缀和 package.json 中的 type 字段判断模块格式(是否为 ESM)。
  3. 根据 configLoader加载器配置来加载配置文件和转换代码。
    • bundle模式,调用 bundleConfigFile 使用 rolldown 打包配置文件,获取转换后的代码和依赖列表。调用 loadConfigFromBundledFile 将打包后的代码转成配置对象。
    • runner模式,使用 Vite 的 ModuleRunner 动态转换任何文件。它的核心机制是利用 RunnableDevEnvironment 提供的 runner.import 函数,在独立的执行环境中加载并运行模块
    • native模式,利用原生动态引入。
  4. 如果用户导出的是函数,则调用该函数传入 configEnv(包含 commandmode 等参数),获取最终配置对象。
  5. 返回配置对象、配置文件路径以及依赖列表 dependencies
  let { configFile } = config
  if (configFile !== false) {
    // 从文件加载配置
    const loadResult = await loadConfigFromFile(
      configEnv,
      configFile,
      config.root,
      config.logLevel,
      config.customLogger,
      config.configLoader,
    )
    if (loadResult) {
      config = mergeConfig(loadResult.config, config)
      configFile = loadResult.path
      configFileDependencies = loadResult.dependencies
    }
  }

image.png

如果在执行 vite dev 时没有使用 --config 参数指定配置文件,Vite 将按照以下顺序自动查找并加载配置文件。

const DEFAULT_CONFIG_FILES: string[] = [
  'vite.config.js',
  'vite.config.mjs',
  'vite.config.ts',
  'vite.config.cjs',
  'vite.config.mts',
  'vite.config.cts',
]

Vite 提供了三种配置加载机制

当配置文件被定位后,Vite 如何读取并执行它的内容?这取决于 configLoader 配置选项。Vite 提供了三种机制来加载配置文件,默认使用 bundle 模式。

const resolver =
  configLoader === 'bundle'
    ? bundleAndLoadConfigFile // 处理配置文件的预构建
    : configLoader === 'runner'
      ? runnerImportConfigFile // 处理配置文件的运行时导入
      : nativeImportConfigFile // 处理配置文件的原生导入

bundle (默认)

使用打包工具(Rolldown)将配置文件及其依赖打包成一个临时文件,再加载执行。

function bundleAndLoadConfigFile(resolvedPath: string) {
  // 检查是否为 ESM 模块
  const isESM =
    // 在 Deno 环境中运行
    typeof process.versions.deno === 'string' || isFilePathESM(resolvedPath)

  // 配置文件打包
  // 打包过程会处理配置文件的依赖,将其转换为可执行的代码
  const bundled = await bundleConfigFile(resolvedPath, isESM)
  // 配置加载
  const userConfig = await loadConfigFromBundledFile(
    resolvedPath,
    bundled.code,
    isESM,
  )

  return {
    // 加载的用户配置
    configExport: userConfig,
    // 配置文件的依赖项
    dependencies: bundled.dependencies,
  }
}

image.png

image.png

image.png

image.png

image.png

image.png

bundle.code 字符串

import "node:module";
import { defineConfig } from "file:///Users/xxxxx/Documents/code/cloudcode/vue3-vite-cube/node_modules/vite/dist/node/index.js";
import vue from "file:///Users/xxxxx/Documents/code/cloudcode/vue3-vite-cube/node_modules/@vitejs/plugin-vue/dist/index.mjs";
import vueJsx from "file:///Users/xxxxx/Documents/code/cloudcode/vue3-vite-cube/node_modules/@vitejs/plugin-vue-jsx/dist/index.mjs";
import VueRouter from "file:///Users/xxxx/Documents/code/cloudcode/vue3-vite-cube/node_modules/vue-router/dist/unplugin/vite.mjs";
Object.create;
Object.defineProperty;
Object.getOwnPropertyDescriptor;
Object.getOwnPropertyNames;
Object.getPrototypeOf;
Object.prototype.hasOwnProperty;
var vite_config_default = defineConfig({
plugins: [
VueRouter({
routesFolder: "src/pages",
extensions: [".vue"],
dts: "src/typed-router.d.ts",
importMode: "async",
root: process.cwd(),
watch: true
}),
vue(),
vueJsx()
],
resolve: { alias: { "@": "/src" } },
css: { preprocessorOptions: { less: {
additionalData: \`@import "@/styles/variables.less";\`,
javascriptEnabled: true
} } },
mode: "development"
});
//#endregion
export { vite_config_default as default };

//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidml0ZS5jb25maWcuanMiLCJuYW1lcyI6W10sInNvdXJjZXMiOlsiL1VzZXJzL2hhaXlhbi9Eb2N1bWVudHMvY29kZS9jbG91ZGNvZGUvdnVlMy12aXRlLWN1YmUvdml0ZS5jb25maWcudHMiXSwic291cmNlc0NvbnRlbnQiOlsiLy8gaW1wb3J0IHsgZmlsZVVSTFRvUGF0aCwgVVJMIH0gZnJvbSAnbm9kZTp1cmwnXG5pbXBvcnQgeyBkZWZpbmVDb25maWcgfSBmcm9tICd2aXRlJ1xuaW1wb3J0IHZ1ZSBmcm9tICdAdml0ZWpzL3BsdWdpbi12dWUnXG5pbXBvcnQgdnVlSnN4IGZyb20gJ0B2aXRlanMvcGx1Z2luLXZ1ZS1qc3gnXG5pbXBvcnQgdnVlRGV2VG9vbHMgZnJvbSAndml0ZS1wbHVnaW4tdnVlLWRldnRvb2xzJ1xuaW1wb3J0IFZ1ZVJvdXRlciBmcm9tICd2dWUtcm91dGVyL3ZpdGUnXG5cblxuLy8gaHR0cHM6Ly92aXRlLmRldi9jb25maWcvXG5leHBvcnQgZGVmYXVsdCBkZWZpbmVDb25maWcoe1xuICBwbHVnaW5zOiBbXG4gICAgLy8g5b+F6aG76KaB5ZyoIHZ1ZSDmj5Lku7bkuYvliY1cbiAgICBWdWVSb3V0ZXIoe1xuICAgICAgcm91dGVzRm9sZGVyOiAnc3JjL3BhZ2VzJywgLy8g6buY6K6kIHBhZ2VzXG4gICAgICBleHRlbnNpb25zOiBbJy52dWUnXSwgLy8g5Yy56YWN5paH5Lu25ZCO57yAXG4gICAgICBkdHM6ICdzcmMvdHlwZWQtcm91dGVyLmQudHMnLCAvLyDnlJ/miJDnsbvlnovmlofku7ZcbiAgICAgIC8vIOWHuueOsCBSYW5nZUVycm9yOiBNYXhpbXVtIGNhbGwgc3RhY2sgc2l6ZSBleGNlZWRlZFxuICAgICAgLy8gZ2V0Um91dGVOYW1lOiAocm91dGUpID0+IHtcbiAgICAgIC8vICAgY29uc29sZS5sb2coJ2dldFJvdXRlTmFtZScscm91dGUpXG4gICAgICAvLyAgIHJldHVybiByb3V0ZS5uYW1lIHx8IHJvdXRlLnBhdGhcbiAgICAgIC8vIH0sXG5cbiAgICAgICAvLyDmt7vliqDosIPor5XpgInpoblcbiAgICAgIC8vIGxvZ3M6IHRydWUsXG5cbiAgICAgIC8vIHJvdXRlQmxvY2tMYW5nOiAnanNvbjUnLCAvLyDot6/nlLHlnZfor63oqIDvvIzpu5jorqQganNvblxuICAgICAgaW1wb3J0TW9kZTogJ2FzeW5jJyxcbiAgICAgIHJvb3Q6IHByb2Nlc3MuY3dkKCksXG5cbiAgICAgIC8vIOWcqOmFjee9ruaWh+S7tuWGmeWFpeWJje+8jOaJi+WKqOS/ruaUuei3r+eUsemFjee9ru+8iOWmgua3u+WKoOWFqOWxgOi3r+eUseWuiOWNq+OAgeiwg+aVtOi3r+eUseWFg+S/oeaBr+OAgei/h+a7pOi3r+eUseetie+8iVxuICAgICAgLy8gYmVmb3JlV3JpdGVGaWxlczogKGVkaXRlZFJvdXRlcykgPT4ge1xuICAgICAgLy8gICBjb25zb2xlLmxvZygnYmVmb3JlV3JpdGVGaWxlcycsIGVkaXRlZFJvdXRlcylcbiAgICAgIC8vIH0sXG4gICAgICB3YXRjaDogdHJ1ZSwgLy8g5byA5ZCv6Lev55Sx5Z2X5paH5Lu255uR5ZCsXG4gICAgICAvLyDlvIDlkK/lrp7pqozmgKflip/og71cbiAgICAgIC8vIGV4cGVyaW1lbnRhbDoge1xuICAgICAgIFxuICAgICAgLy8gfSxcbiAgICB9KSxcbiAgICB2dWUoKSxcbiAgICB2dWVKc3goKSxcbiAgICAvLyB2dWVEZXZUb29scygpLFxuICBdLFxuICByZXNvbHZlOiB7XG4gICAgLy8gYWxpYXM6IHtcbiAgICAvLyAgICdAJzogZmlsZVVSTFRvUGF0aChuZXcgVVJMKCcuL3NyYycsIGltcG9ydC5tZXRhLnVybCkpXG4gICAgLy8gfSxcbiAgICBhbGlhczoge1xuICAgICAgJ0AnOiAnL3NyYycsXG4gICAgfSxcbiAgICAvLyB0c2NvbmZpZ1BhdGhzOiB0cnVlLCAgLy8g6Ieq5Yqo6K+75Y+WIHRzY29uZmlnIHBhdGhzXG4gIH0sXG4gIGNzczoge1xuICAgIHByZXByb2Nlc3Nvck9wdGlvbnM6IHtcbiAgICAgIGxlc3M6IHtcbiAgICAgICAgYWRkaXRpb25hbERhdGE6IGBAaW1wb3J0IFwiQC9zdHlsZXMvdmFyaWFibGVzLmxlc3NcIjtgLFxuICAgICAgICBqYXZhc2NyaXB0RW5hYmxlZDogdHJ1ZVxuICAgICAgfVxuICAgIH1cbiAgfSxcbiAgbW9kZTogJ2RldmVsb3BtZW50JyxcbiAgLy8gc2VydmVyOiB7XG4gIC8vICAgd3M6IGZhbHNlLFxuICAvLyB9LFxuICAvLyBvcHRpbWl6ZURlcHM6IHtcbiAgLy8gICBpbmNsdWRlOiBbJ3ZpcnR1YWw6dnVlLWluc3BlY3Rvci1wYXRoOmxvYWQuanMnXSxcbiAgLy8gfSxcblxufSkiXSwibWFwcGluZ3MiOiI7Ozs7Ozs7Ozs7O0FBU0EsSUFBQSxzQkFBZSxhQUFhO0NBQzFCLFNBQVM7RUFFUCxVQUFVO0dBQ1IsY0FBYztHQUNkLFlBQVksQ0FBQyxPQUFPO0dBQ3BCLEtBQUs7R0FXTCxZQUFZO0dBQ1osTUFBTSxRQUFRLEtBQUs7R0FNbkIsT0FBTztHQUtSLENBQUM7RUFDRixLQUFLO0VBQ0wsUUFBUTtFQUVUO0NBQ0QsU0FBUyxFQUlQLE9BQU8sRUFDTCxLQUFLLFFBQ04sRUFFRjtDQUNELEtBQUssRUFDSCxxQkFBcUIsRUFDbkIsTUFBTTtFQUNKLGdCQUFnQjtFQUNoQixtQkFBbUI7RUFDckIsRUFDRixFQUNEO0NBQ0QsTUFBTTtDQVFQLENBQUEifQ==

dependencies

[
  "/Users/xxxx/Documents/code/cloudcode/vue3-vite-cube/vite.config.ts",
]

临时文件

image.png

image.png

vue3-vite-cube/node_modules/.vite-temp/vite.config.ts.timestamp-1775361732369-f30607f0da0d6.mjs 文件内容如下:

import "node:module";
import { defineConfig } from "file:///Users/xxxx/Documents/code/cloudcode/vue3-vite-cube/node_modules/vite/dist/node/index.js";
import vue from "file:///Users/xxxx/Documents/code/cloudcode/vue3-vite-cube/node_modules/@vitejs/plugin-vue/dist/index.mjs";
import vueJsx from "file:///Users/xxxx/Documents/code/cloudcode/vue3-vite-cube/node_modules/@vitejs/plugin-vue-jsx/dist/index.mjs";
import VueRouter from "file:///Users/xxxx/Documents/code/cloudcode/vue3-vite-cube/node_modules/vue-router/dist/unplugin/vite.mjs";
Object.create;
Object.defineProperty;
Object.getOwnPropertyDescriptor;
Object.getOwnPropertyNames;
Object.getPrototypeOf;
Object.prototype.hasOwnProperty;
var vite_config_default = defineConfig({
plugins: [
VueRouter({
routesFolder: "src/pages",
extensions: [".vue"],
dts: "src/typed-router.d.ts",
importMode: "async",
root: process.cwd(),
watch: true
}),
vue(),
vueJsx()
],
resolve: { alias: { "@": "/src" } },
css: { preprocessorOptions: { less: {
additionalData: `@import "@/styles/variables.less";`,
javascriptEnabled: true
} } },
mode: "development"
});
//#endregion
export { vite_config_default as default };

//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidml0ZS5jb25maWcuanMiLCJuYW1lcyI6W10sInNvdXJjZXMiOlsiL1VzZXJzL2hhaXlhbi9Eb2N1bWVudHMvY29kZS9jbG91ZGNvZGUvdnVlMy12aXRlLWN1YmUvdml0ZS5jb25maWcudHMiXSwic291cmNlc0NvbnRlbnQiOlsiLy8gaW1wb3J0IHsgZmlsZVVSTFRvUGF0aCwgVVJMIH0gZnJvbSAnbm9kZTp1cmwnXG5pbXBvcnQgeyBkZWZpbmVDb25maWcgfSBmcm9tICd2aXRlJ1xuaW1wb3J0IHZ1ZSBmcm9tICdAdml0ZWpzL3BsdWdpbi12dWUnXG5pbXBvcnQgdnVlSnN4IGZyb20gJ0B2aXRlanMvcGx1Z2luLXZ1ZS1qc3gnXG5pbXBvcnQgdnVlRGV2VG9vbHMgZnJvbSAndml0ZS1wbHVnaW4tdnVlLWRldnRvb2xzJ1xuaW1wb3J0IFZ1ZVJvdXRlciBmcm9tICd2dWUtcm91dGVyL3ZpdGUnXG5cblxuLy8gaHR0cHM6Ly92aXRlLmRldi9jb25maWcvXG5leHBvcnQgZGVmYXVsdCBkZWZpbmVDb25maWcoe1xuICBwbHVnaW5zOiBbXG4gICAgLy8g5b+F6aG76KaB5ZyoIHZ1ZSDmj5Lku7bkuYvliY1cbiAgICBWdWVSb3V0ZXIoe1xuICAgICAgcm91dGVzRm9sZGVyOiAnc3JjL3BhZ2VzJywgLy8g6buY6K6kIHBhZ2VzXG4gICAgICBleHRlbnNpb25zOiBbJy52dWUnXSwgLy8g5Yy56YWN5paH5Lu25ZCO57yAXG4gICAgICBkdHM6ICdzcmMvdHlwZWQtcm91dGVyLmQudHMnLCAvLyDnlJ/miJDnsbvlnovmlofku7ZcbiAgICAgIC8vIOWHuueOsCBSYW5nZUVycm9yOiBNYXhpbXVtIGNhbGwgc3RhY2sgc2l6ZSBleGNlZWRlZFxuICAgICAgLy8gZ2V0Um91dGVOYW1lOiAocm91dGUpID0+IHtcbiAgICAgIC8vICAgY29uc29sZS5sb2coJ2dldFJvdXRlTmFtZScscm91dGUpXG4gICAgICAvLyAgIHJldHVybiByb3V0ZS5uYW1lIHx8IHJvdXRlLnBhdGhcbiAgICAgIC8vIH0sXG5cbiAgICAgICAvLyDmt7vliqDosIPor5XpgInpoblcbiAgICAgIC8vIGxvZ3M6IHRydWUsXG5cbiAgICAgIC8vIHJvdXRlQmxvY2tMYW5nOiAnanNvbjUnLCAvLyDot6/nlLHlnZfor63oqIDvvIzpu5jorqQganNvblxuICAgICAgaW1wb3J0TW9kZTogJ2FzeW5jJyxcbiAgICAgIHJvb3Q6IHByb2Nlc3MuY3dkKCksXG5cbiAgICAgIC8vIOWcqOmFjee9ruaWh+S7tuWGmeWFpeWJje+8jOaJi+WKqOS/ruaUuei3r+eUsemFjee9ru+8iOWmgua3u+WKoOWFqOWxgOi3r+eUseWuiOWNq+OAgeiwg+aVtOi3r+eUseWFg+S/oeaBr+OAgei/h+a7pOi3r+eUseetie+8iVxuICAgICAgLy8gYmVmb3JlV3JpdGVGaWxlczogKGVkaXRlZFJvdXRlcykgPT4ge1xuICAgICAgLy8gICBjb25zb2xlLmxvZygnYmVmb3JlV3JpdGVGaWxlcycsIGVkaXRlZFJvdXRlcylcbiAgICAgIC8vIH0sXG4gICAgICB3YXRjaDogdHJ1ZSwgLy8g5byA5ZCv6Lev55Sx5Z2X5paH5Lu255uR5ZCsXG4gICAgICAvLyDlvIDlkK/lrp7pqozmgKflip/og71cbiAgICAgIC8vIGV4cGVyaW1lbnRhbDoge1xuICAgICAgIFxuICAgICAgLy8gfSxcbiAgICB9KSxcbiAgICB2dWUoKSxcbiAgICB2dWVKc3goKSxcbiAgICAvLyB2dWVEZXZUb29scygpLFxuICBdLFxuICByZXNvbHZlOiB7XG4gICAgLy8gYWxpYXM6IHtcbiAgICAvLyAgICdAJzogZmlsZVVSTFRvUGF0aChuZXcgVVJMKCcuL3NyYycsIGltcG9ydC5tZXRhLnVybCkpXG4gICAgLy8gfSxcbiAgICBhbGlhczoge1xuICAgICAgJ0AnOiAnL3NyYycsXG4gICAgfSxcbiAgICAvLyB0c2NvbmZpZ1BhdGhzOiB0cnVlLCAgLy8g6Ieq5Yqo6K+75Y+WIHRzY29uZmlnIHBhdGhzXG4gIH0sXG4gIGNzczoge1xuICAgIHByZXByb2Nlc3Nvck9wdGlvbnM6IHtcbiAgICAgIGxlc3M6IHtcbiAgICAgICAgYWRkaXRpb25hbERhdGE6IGBAaW1wb3J0IFwiQC9zdHlsZXMvdmFyaWFibGVzLmxlc3NcIjtgLFxuICAgICAgICBqYXZhc2NyaXB0RW5hYmxlZDogdHJ1ZVxuICAgICAgfVxuICAgIH1cbiAgfSxcbiAgbW9kZTogJ2RldmVsb3BtZW50JyxcbiAgLy8gc2VydmVyOiB7XG4gIC8vICAgd3M6IGZhbHNlLFxuICAvLyB9LFxuICAvLyBvcHRpbWl6ZURlcHM6IHtcbiAgLy8gICBpbmNsdWRlOiBbJ3ZpcnR1YWw6dnVlLWluc3BlY3Rvci1wYXRoOmxvYWQuanMnXSxcbiAgLy8gfSxcblxufSkiXSwibWFwcGluZ3MiOiI7Ozs7Ozs7Ozs7O0FBU0EsSUFBQSxzQkFBZSxhQUFhO0NBQzFCLFNBQVM7RUFFUCxVQUFVO0dBQ1IsY0FBYztHQUNkLFlBQVksQ0FBQyxPQUFPO0dBQ3BCLEtBQUs7R0FXTCxZQUFZO0dBQ1osTUFBTSxRQUFRLEtBQUs7R0FNbkIsT0FBTztHQUtSLENBQUM7RUFDRixLQUFLO0VBQ0wsUUFBUTtFQUVUO0NBQ0QsU0FBUyxFQUlQLE9BQU8sRUFDTCxLQUFLLFFBQ04sRUFFRjtDQUNELEtBQUssRUFDSCxxQkFBcUIsRUFDbkIsTUFBTTtFQUNKLGdCQUFnQjtFQUNoQixtQkFBbUI7RUFDckIsRUFDRixFQUNEO0NBQ0QsTUFBTTtDQVFQLENBQUEifQ==
/**
 * 用于从打包后的代码加载 Vite 配置。
 * 它根据模块类型(ESM 或 CommonJS)采用不同的加载策略,确保配置文件能够被正确执行并返回配置对象
 * @param fileName  文件路径
 * @param bundledCode 打包转换后代码
 * @param isESM 是否为 ESM 格式
 * @returns 
 */
async function loadConfigFromBundledFile(
  fileName: string,
  bundledCode: string,
  isESM: boolean,
): Promise<UserConfigExport> {
  // for esm, before we can register loaders without requiring users to run node
  // with --experimental-loader themselves, we have to do a hack here:
  // write it to disk, load it with native Node ESM, then delete the file.
  if (isESM) {
    // Storing the bundled file in node_modules/ is avoided for Deno
    // because Deno only supports Node.js style modules under node_modules/
    // and configs with `npm:` import statements will fail when executed.
    // 查找最近的 node_modules 目录
    let nodeModulesDir =
      typeof process.versions.deno === 'string'
        ? undefined
        : findNearestNodeModules(path.dirname(fileName))

    if (nodeModulesDir) {
      try {
        // 创建临时目录
        // node_modules/.vite-temp/
        await fsp.mkdir(path.resolve(nodeModulesDir, '.vite-temp/'), {
          recursive: true,
        })
      } catch (e) {
        if (e.code === 'EACCES') {
          // If there is no access permission, a temporary configuration file is created by default.
          nodeModulesDir = undefined
        } else {
          throw e
        }
      }
    }
    // 生成 hash 值
    const hash = `timestamp-${Date.now()}-${Math.random().toString(16).slice(2)}`
    // 生成临时文件名
    const tempFileName = nodeModulesDir
      ? path.resolve(
          nodeModulesDir,
          `.vite-temp/${path.basename(fileName)}.${hash}.mjs`,
        )
      : `${fileName}.${hash}.mjs`
      // 写入临时文件
    await fsp.writeFile(tempFileName, bundledCode)
    try {
      // 将文件系统路径转换为 file:// 协议的 URL 对象
      // 原因:ESM 的 import() 语法要求模块标识符为 URL 格式(对于本地文件),不能直接使用文件系统路径
      // 动态加载 ESM 格式配置文件
      // 执行过程:
      // 1、Node.js 读取并执行 tempFileName 指向的文件
      // 2、执行文件中的代码,构建模块的导出
      // 3、生成包含所有导出的模块命名空间对象
      // 4、Promise 解析为该命名空间对象
      return (await import(pathToFileURL(tempFileName).href)).default
    } finally {
      fs.unlink(tempFileName, () => {}) // Ignore errors
    }
  }
  // for cjs, we can register a custom loader via `_require.extensions`
  else {
    // 获取文件扩展名
    const extension = path.extname(fileName)
    // We don't use fsp.realpath() here because it has the same behaviour as
    // fs.realpath.native. On some Windows systems, it returns uppercase volume
    // letters (e.g. "C:\") while the Node.js loader uses lowercase volume letters.
    // See https://github.com/vitejs/vite/issues/12923
    // 获取文件的真实路径
    // 避免 Windows 系统上的路径大小写问题
    const realFileName = await promisifiedRealpath(fileName)
    // 确定加载器扩展名
    // require.extensions 标记已废弃
    const loaderExt = extension in _require.extensions ? extension : '.js'
    const defaultLoader = _require.extensions[loaderExt]!
    // 注册自定义加载器
    _require.extensions[loaderExt] = (module: NodeModule, filename: string) => {
      if (filename === realFileName) {
        // 执行打包后的代码
        ;(module as NodeModuleWithCompile)._compile(bundledCode, filename)
      } else {
        // 使用默认加载器
        defaultLoader(module, filename)
      }
    }
    // clear cache in case of server restart
    // 清除缓存
    delete _require.cache[_require.resolve(fileName)]
    // 加载配置文件
    const raw = _require(fileName)
    // 恢复默认加载器
    _require.extensions[loaderExt] = defaultLoader
    return raw.__esModule ? raw.default : raw
  }
}

runner (实验性)

runner 模式不会创建临时配置文件,而是使用 Vite 的 ModuleRunner 动态转换任何文件。它的核心机制是利用 RunnableDevEnvironment 提供的 runner.import 函数,在独立的执行环境中加载并运行模块。

{
   "start": "vite --configLoader=runner",
}
/**
 * 用于通过 runner 方式导入配置文件。
 * 它使用 runnerImport 函数动态加载配置文件,提取默认导出作为配置对象,并返回配置对象及其依赖项。
 * @param resolvedPath 配置文件路径
 * @returns 
 */
async function runnerImportConfigFile(resolvedPath: string) {
  const { module, dependencies } = await runnerImport<{
    default: UserConfigExport
  }>(resolvedPath)
  return {
    configExport: module.default,
    dependencies,
  }
}

image.png

async function runnerImport<T>(
  moduleId: string,
  inlineConfig?: InlineConfig,
): Promise<RunnerImportResult<T>> {

  // 模块同步条件检查
  const isModuleSyncConditionEnabled = (await import('#module-sync-enabled'))
    .default

  // 配置解析
  const config = await resolveConfig(
    // 合并配置
    mergeConfig(inlineConfig || {}, {
      configFile: false, // 禁用配置文件解析
      envDir: false, // 禁用环境变量目录解析
      cacheDir: process.cwd(), // 缓存目录设置为当前工作目录
      environments: {
        inline: {
          // 指定环境的消费方为服务器端
          consumer: 'server',
          dev: {
            // 启用模块运行器转换
            moduleRunnerTransform: true,
          },
          // 模块解析配置
          resolve: {
            // 启用外部模块解析,将依赖视为外部模块,不进行打包
            // 影响:减少打包体积,提高模块加载速度
            external: true,
            // 清空主字段数组
            // 不使用 package.json 中的主字段进行模块解析
            // 避免因主字段优先级导致的解析问题,确保一致性
            mainFields: [],
            // 指定模块解析条件
            conditions: [
              'node',
              ...(isModuleSyncConditionEnabled ? ['module-sync'] : []),
            ],
          },
        },
      },
    } satisfies InlineConfig),
    'serve', // 确保是 serve 命令
  )
  // 创建可运行的开发环境
  const environment = createRunnableDevEnvironment('inline', config, {
    runnerOptions: {
      hmr: {
        logger: false, // 禁用 HMR 日志记录
      },
    },
    hot: false, // 禁用 HMR
  })
  // 初始化环境
  // 准备模块运行器,确保能够正确加载模块
  await environment.init()
  try {
    // 使用环境的运行器导入模块
    // 模块加载与执行:
    // 1、ModuleRunner 解析 moduleId,处理路径解析
    // 2、加载模块文件内容
    // 3、应用必要的转换(如 ESM 到 CommonJS 的转换)
    // 4、执行模块代码
    // 5、收集模块的依赖项
    const module = await environment.runner.import(moduleId)

    // 获取所有评估过的模块
    const modules = [
      ...environment.runner.evaluatedModules.urlToIdModuleMap.values(),
    ]
    // 过滤出所有外部化模块和当前模块
    // 这些模块不是依赖项,因为它们是 Vite 内部使用的模块
    const dependencies = modules
      .filter((m) => {
        // ignore all externalized modules
        // 忽略没有meta的模块 或者标记为外部化的模块
        if (!m.meta || 'externalize' in m.meta) {
          return false
        }
        // ignore the current module
        // 忽略当前模块,因为它不是依赖项
        return m.exports !== module
      })
      .map((m) => m.file)

    return {
      module,
      dependencies,
    }
  } finally {
    // 关闭环境
    // 释放所有资源,避免内存泄漏等问题
    await environment.close()
  }
}

image.png

module

{
  default: {
    plugins: [
      {
        name: "vue-router",
        enforce: "pre",
        resolveId: {
          filter: {
            id: {
              include: [
                {
                },
                {
                },
                {
                },
              ],
            },
          },
          handler: function(...args) {
            const [id] = args;
            if (!supportNativeFilter(this, key) && !filter(id)) return;
            return handler.apply(this, args);
          },
        },
        buildStart: async buildStart() {
          await ctx.scanPages(options.watch);
        },
        buildEnd: function() {
          ctx.stopWatcher();
        },
        transform: {
          filter: {
            id: {
              include: [
                "/Users/xxxx/Documents/code/cloudcode/vue3-vite-cube/src/pages/**/*.vue",
                {
                },
              ],
              exclude: [
              ],
            },
          },
          handler: function(...args) {
            const [code, id] = args;
            if (plugin.transformInclude && !plugin.transformInclude(id)) return;
            if (!supportNativeFilter(this, key) && !filter(id, code)) return;
            return handler.apply(this, args);
          },
        },
        load: {
          filter: {
            id: {
              include: [
                {
                },
                {
                },
                {
                },
              ],
            },
          },
          handler: function(...args) {
            const [id] = args;
            if (plugin.loadInclude && !plugin.loadInclude(id)) return;
            if (!supportNativeFilter(this, key) && !filter(id)) return;
            return handler.apply(this, args);
          },
        },
        vite: {
          configureServer: function(server) {
            ctx.setServerContext(createViteContext(server));
          },
        },
        configureServer: function(server) {
          ctx.setServerContext(createViteContext(server));
        },
      },
      {
        name: "vite:vue",
        api: {
          options: {
            isProduction: false,
            compiler: null,
            customElement: {
            },
            root: "/Users/xxxx/Documents/code/cloudcode/vue3-vite-cube",
            sourceMap: true,
            cssDevSourcemap: false,
          },
          include: {
          },
          exclude: undefined,
          version: "6.0.5",
        },
        handleHotUpdate: function(ctx) {
          ctx.server.ws.send({
          type: "custom",
          event: "file-changed",
          data: { file: normalizePath(ctx.file) }
          });
          if (options.value.compiler.invalidateTypeCache) options.value.compiler.invalidateTypeCache(ctx.file);
          let typeDepModules;
          const matchesFilter = filter.value(ctx.file);
          if (typeDepToSFCMap.has(ctx.file)) {
          typeDepModules = handleTypeDepChange(typeDepToSFCMap.get(ctx.file), ctx);
          if (!matchesFilter) return typeDepModules;
          }
          if (matchesFilter) return handleHotUpdate(ctx, options.value, customElementFilter.value(ctx.file), typeDepModules);
        },
        config: function(config) {
          const parseDefine = (v) => {
          try {
          return typeof v === "string" ? JSON.parse(v) : v;
          } catch (err) {
          return v;
          }
          };
          return {
          resolve: { dedupe: config.build?.ssr ? [] : ["vue"] },
          define: {
          __VUE_OPTIONS_API__: options.value.features?.optionsAPI ?? parseDefine(config.define?.__VUE_OPTIONS_API__) ?? true,
          __VUE_PROD_DEVTOOLS__: (options.value.features?.prodDevtools || parseDefine(config.define?.__VUE_PROD_DEVTOOLS__)) ?? false,
          __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: (options.value.features?.prodHydrationMismatchDetails || parseDefine(config.define?.__VUE_PROD_HYDRATION_MISMATCH_DETAILS__)) ?? false
          },
          ssr: { external: config.legacy?.buildSsrCjsExternalHeuristics ? ["vue", "@vue/server-renderer"] : [] }
          };
        },
        configResolved: function(config) {
          options.value = {
          ...options.value,
          root: config.root,
          sourceMap: config.command === "build" ? !!config.build.sourcemap : true,
          cssDevSourcemap: config.css?.devSourcemap ?? false,
          isProduction: config.isProduction,
          devToolsEnabled: !!(options.value.features?.prodDevtools || config.define.__VUE_PROD_DEVTOOLS__ || !config.isProduction)
          };
          const _warn = config.logger.warn;
          config.logger.warn = (...args) => {
          if (args[0].match(/\[lightningcss\] '(deep|slotted|global)' is not recognized as a valid pseudo-/)) return;
          _warn(...args);
          };
          transformCachedModule = config.command === "build" && options.value.sourceMap && config.build.watch != null;
        },
        options: function() {
          optionsHookIsCalled = true;
          plugin.transform.filter = { id: {
          include: [...makeIdFiltersToMatchWithQuery(ensureArray(include.value)), /[?&]vue\b/],
          exclude: exclude.value
          } };
        },
        shouldTransformCachedModule: function({ id }) {
          if (transformCachedModule && parseVueRequest(id).query.vue) return true;
          return false;
        },
        configureServer: function(server) {
          options.value.devServer = server;
        },
        buildStart: function() {
          const compiler = options.value.compiler = options.value.compiler || resolveCompiler(options.value.root);
          if (compiler.invalidateTypeCache) options.value.devServer?.watcher.on("unlink", (file) => {
          compiler.invalidateTypeCache(file);
          });
        },
        resolveId: {
          filter: {
            id: [
              {
              },
              {
              },
            ],
          },
          handler: function(id) {
            if (id === EXPORT_HELPER_ID) return id;
            if (parseVueRequest(id).query.vue) return id;
          },
        },
        load: {
          filter: {
            id: [
              {
              },
              {
              },
            ],
          },
          handler: function(id, opt) {
            if (id === EXPORT_HELPER_ID) return helperCode;
            const ssr = opt?.ssr === true;
            const { filename, query } = parseVueRequest(id);
            if (query.vue) {
            if (query.src) return fs.readFileSync(filename, "utf-8");
            const descriptor = getDescriptor(filename, options.value);
            let block;
            if (query.type === "script") block = resolveScript(descriptor, options.value, ssr, customElementFilter.value(filename));
            else if (query.type === "template") block = descriptor.template;
            else if (query.type === "style") block = descriptor.styles[query.index];
            else if (query.index != null) block = descriptor.customBlocks[query.index];
            if (block) return {
            code: block.content,
            map: block.map
            };
            }
          },
        },
        transform: {
          handler: function(code, id, opt) {
            const ssr = opt?.ssr === true;
            const { filename, query } = parseVueRequest(id);
            if (query.raw || query.url) return;
            if (!filter.value(filename) && !query.vue) return;
            if (!query.vue) return transformMain(code, filename, options.value, this, ssr, customElementFilter.value(filename));
            else {
            const descriptor = query.src ? getSrcDescriptor(filename, query) || getTempSrcDescriptor(filename, query) : getDescriptor(filename, options.value);
            if (query.src) this.addWatchFile(filename);
            if (query.type === "template") return transformTemplateAsModule(code, filename, descriptor, options.value, this, ssr, customElementFilter.value(filename));
            else if (query.type === "style") return transformStyle(code, descriptor, Number(query.index || 0), options.value, this, filename);
            }
          },
        },
      },
      {
        name: "vite:vue-jsx",
        config: function(config) {
          const parseDefine = (v) => {
          try {
          return typeof v === "string" ? JSON.parse(v) : v;
          } catch (err) {
          return v;
          }
          };
          const isRolldownVite = this && "rolldownVersion" in this.meta;
          return {
          [isRolldownVite ? "oxc" : "esbuild"]: tsTransform === "built-in" ? { exclude: /\.jsx?$/ } : { include: /\.ts$/ },
          define: {
          __VUE_OPTIONS_API__: parseDefine(config.define?.__VUE_OPTIONS_API__) ?? true,
          __VUE_PROD_DEVTOOLS__: parseDefine(config.define?.__VUE_PROD_DEVTOOLS__) ?? false,
          __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: parseDefine(config.define?.__VUE_PROD_HYDRATION_MISMATCH_DETAILS__) ?? false
          },
          optimizeDeps: isRolldownVite ? { rolldownOptions: { transform: { jsx: "preserve" } } } : {}
          };
        },
        configResolved: function(config) {
          needHmr = config.command === "serve" && !config.isProduction;
          needSourceMap = config.command === "serve" || !!config.build.sourcemap;
          root = config.root;
        },
        resolveId: {
          filter: {
            id: {
            },
          },
          handler: function(id) {
            if (id === ssrRegisterHelperId) return id;
          },
        },
        load: {
          filter: {
            id: {
            },
          },
          handler: function(id) {
            if (id === ssrRegisterHelperId) return ssrRegisterHelperCode;
          },
        },
        transform: {
          order: undefined,
          filter: {
            id: {
              include: {
              },
              exclude: undefined,
            },
          },
          handler: async handler(code, id, opt) {
            const ssr = opt?.ssr === true;
            const [filepath] = id.split("?");
            if (filter(id) || filter(filepath)) {
            const plugins = [[jsx, babelPluginOptions], ...babelPlugins];
            if (id.endsWith(".tsx") || filepath.endsWith(".tsx")) if (tsTransform === "built-in") plugins.push([await import("@babel/plugin-syntax-typescript").then((r) => r.default), { isTSX: true }]);
            else plugins.push([await import("@babel/plugin-transform-typescript").then((r) => r.default), {
            ...tsPluginOptions,
            isTSX: true,
            allowExtensions: true
            }]);
            if (!ssr && !needHmr) plugins.push(() => {
            return { visitor: { CallExpression: { enter(_path) {
            if (isDefineComponentCall(_path.node, defineComponentName)) {
            const callee = _path.node.callee;
            callee.name = `/* @__PURE__ */ ${callee.name}`;
            }
            } } } };
            });
            else plugins.push(() => {
            return { visitor: { ExportDefaultDeclaration: { enter(_path) {
            const unwrappedDeclaration = unwrapTypeAssertion(_path.node.declaration);
            if (isDefineComponentCall(unwrappedDeclaration, defineComponentName)) {
            const declaration = unwrappedDeclaration;
            const nodesPath = _path.replaceWithMultiple([types.variableDeclaration("const", [types.variableDeclarator(types.identifier("__default__"), types.callExpression(declaration.callee, declaration.arguments))]), types.exportDefaultDeclaration(types.identifier("__default__"))]);
            _path.scope.registerDeclaration(nodesPath[0]);
            }
            } } } };
            });
            const result = babel.transformSync(code, {
            babelrc: false,
            ast: true,
            plugins,
            sourceMaps: needSourceMap,
            sourceFileName: id,
            configFile: false
            });
            if (!ssr && !needHmr) {
            if (!result.code) return;
            return {
            code: result.code,
            map: result.map
            };
            }
            const declaredComponents = [];
            const hotComponents = [];
            for (const node of result.ast.program.body) {
            if (node.type === "VariableDeclaration") {
            const names = parseComponentDecls(node, defineComponentName);
            if (names.length) declaredComponents.push(...names);
            }
            if (node.type === "ExportNamedDeclaration") {
            if (node.declaration && node.declaration.type === "VariableDeclaration") hotComponents.push(...parseComponentDecls(node.declaration, defineComponentName).map((name) => ({
            local: name,
            exported: name,
            id: getHash(id + name)
            })));
            else if (node.specifiers.length) {
            for (const spec of node.specifiers) if (spec.type === "ExportSpecifier" && spec.exported.type === "Identifier") {
            if (declaredComponents.find((name) => name === spec.local.name)) hotComponents.push({
            local: spec.local.name,
            exported: spec.exported.name,
            id: getHash(id + spec.exported.name)
            });
            }
            }
            }
            if (node.type === "ExportDefaultDeclaration") {
            if (node.declaration.type === "Identifier") {
            const _name = node.declaration.name;
            if (declaredComponents.find((name) => name === _name)) hotComponents.push({
            local: _name,
            exported: "default",
            id: getHash(id + "default")
            });
            } else if (isDefineComponentCall(unwrapTypeAssertion(node.declaration), defineComponentName)) hotComponents.push({
            local: "__default__",
            exported: "default",
            id: getHash(id + "default")
            });
            }
            }
            if (hotComponents.length) {
            if (needHmr && !ssr && !/\?vue&type=script/.test(id)) {
            let code = result.code;
            let callbackCode = ``;
            for (const { local, exported, id } of hotComponents) {
            code += `\n${local}.__hmrId = "${id}"\n__VUE_HMR_RUNTIME__.createRecord("${id}", ${local})`;
            callbackCode += `\n__VUE_HMR_RUNTIME__.reload("${id}", __${exported})`;
            }
            const newCompNames = hotComponents.map((c) => `${c.exported}: __${c.exported}`).join(",");
            code += `\nimport.meta.hot.accept(({${newCompNames}}) => {${callbackCode}\n})`;
            result.code = code;
            }
            if (ssr) {
            const normalizedId = normalizePath(path.relative(root, id));
            let ssrInjectCode = `\nimport { ssrRegisterHelper } from "${ssrRegisterHelperId}"\nconst __moduleId = ${JSON.stringify(normalizedId)}`;
            for (const { local } of hotComponents) ssrInjectCode += `\nssrRegisterHelper(${local}, __moduleId)`;
            result.code += ssrInjectCode;
            }
            }
            if (!result.code) return;
            return {
            code: result.code,
            map: result.map
            };
            }
          },
        },
      },
    ],
    resolve: {
      alias: {
        "@": "/src",
      },
    },
    css: {
      preprocessorOptions: {
        less: {
          additionalData: "@import \"@/styles/variables.less\";",
          javascriptEnabled: true,
        },
      },
    },
    mode: "development",
  },
}

导出的内容就是 vite.config.js 中配置信息

image.png

ModuleRunner模块运行器

  public async import<T = any>(url: string): Promise<T> {
    // 获取缓存模块
    const fetchedModule = await this.cachedModule(url)
    // 执行模块请求
    return await this.cachedRequest(url, fetchedModule)
  }

image.png

native (实验性)

native 模式直接通过 Node.js 原生的动态 import() 加载配置文件,不经过任何打包步骤。

只能编写纯 JavaScript,可以指定 --configLoader native 来使用环境的原生运行时加载配置文件。

{
   "start": "vite --configLoader=native",
}
  • 它的优点是简单快速,调试时断点可以直接定位到源码,不受临时文件干扰。
  • 但这种模式有一个重要限制:配置文件导入的模块的更新不会被检测到,因此不会自动重启 Vite 服务器
async function nativeImportConfigFile(resolvedPath: string) {
  const module = await import(
    pathToFileURL(resolvedPath).href + '?t=' + Date.now()
  )
  return {
    configExport: module.default,
    dependencies: [],
  }
}

在 native 模式下,由于没有经过打包工具分析依赖,Vite 无法知道配置文件引入了哪些本地模块。因此依赖列表被硬编码为空数组,意味着当配置文件导入的其他本地文件(如 ./utils.js)发生变化时,Vite 不会自动重启服务器。这是 native 模式的重要限制。

三者的区别

image.png

深入解析 Vite dev:从命令行到浏览器热更新的完整旅程

作者 米丘
2026年4月5日 18:21

在前端工程化领域,Vite 凭借其极致的开发体验和强大的构建能力,已成为新一代开发工具链的事实标准。随着 Vite 8 的正式发布,这套工具在性能和架构上再次实现突破——底层打包器统一为 Rust 编写的 Rolldown,开发环境启动速度和热更新响应迈入毫秒级时代。而作为开发者每天最常接触的命令行入口,vite dev 和 vite serve 背后承载着怎样的设计理念?它们又有哪些鲜为人知的细节?本文将为你一一揭晓。

命令本质:开发服务器的统一入口

在 Vite 8 中,vite dev 与 vite serve 实际上是同一个命令的两种不同叫法,二者完全等价。

vite
vite dev
vite serve

之所以保留两个名称,主要是为了兼容过往的习惯(如 serve 源自早期版本,而 dev 更直观地表达开发用途)。

vite 启动通用的命令行参数?

// 定义 Vite 命令行工具
const cli = cac('vite')

cli
  .option('-c, --config <file>', `[string] use specified config file`)
  .option('--base <path>', `[string] public base path (default: /)`, {
    type: [convertBase],
  })
  .option('-l, --logLevel <level>', `[string] info | warn | error | silent`)
  .option('--clearScreen', `[boolean] allow/disable clear screen when logging`)
  .option(
    '--configLoader <loader>',
    `[string] use 'bundle' to bundle the config with Rolldown, or 'runner' (experimental) to process it on the fly, or 'native' (experimental) to load using the native runtime (default: bundle)`,
  )
  .option('-d, --debug [feat]', `[string | boolean] show debug logs`)
  .option('-f, --filter <filter>', `[string] filter debug logs`)
  .option('-m, --mode <mode>', `[string] set env mode`)
# 指定配置文件路径
vite dev --config my.config.js

# 设置公共路径,默认 /
vite dev --base /my-app/

# 设置日志级别
vite dev --logLevel error` # 只输出错误

vite dev --clearScreen # 启用清屏
vite dev --no-clearScreen # 禁用清屏

# `bundle`(默认):使用 Rolldown 将配置文件打包后执行。
# `runner`(实验性):使用动态 `import()` 即时处理配置文件。
# `native`(实验性):使用原生 Node.js 模块加载(需配置文件为 ESM)。
vite dev --configLoader runner # 使用 Rolldown 将配置文件打包后执行

vite dev --debug               # 开启全部调试日志
vite dev --debug vite:hmr      # 仅显示 HMR 相关调试信息

# 指定运行模式(如 `development`、`production`、`staging`)。
# Vite 会加载对应的环境变量文件(例如 `.env.[mode]`),并影响 `import.meta.env.MODE` 的值。
vite dev --mode staging

vite dev 启动接收的命令行参数

在当前目录下启动 Vite 开发服务器。vite dev 和 vite serve 是 vite 的别名。

cli
  .command('[root]', 'start dev server') // default command
  .alias('serve') // the command is called 'serve' in Vite's API
  .alias('dev') // alias to align with the script name
  .option('--host [host]', `[string] specify hostname`, { type: [convertHost] })
  .option('--port <port>', `[number] specify port`)
  .option('--open [path]', `[boolean | string] open browser on startup`)
  .option('--cors', `[boolean] enable CORS`)
  .option('--strictPort', `[boolean] exit if specified port is already in use`)
  .option(
    '--force',
    `[boolean] force the optimizer to ignore the cache and re-bundle`,
  )
  .option(
    '--experimentalBundle',
    `[boolean] use experimental full bundle mode (this is highly experimental)`,
  )
# 指定项目的根目录。如果不提供,默认使用当前工作目录(`process.cwd()`)
vite dev ./my-project

vite dev --host               # 监听所有接口
vite dev --host localhost     # 仅监听本地

vite dev --port 3000

vite dev --open               # 打开 http://localhost:5173/
vite dev --open /admin        # 打开 http://localhost:5173/admin

# 强制依赖优化器忽略缓存,重新预构建所有依赖(`optimizeDeps`)
vite dev --force

# 启用实验性的“全量打包开发模式”(`bundledDev`)
vite dev --experimentalBundle

启用实验性的“全量打包开发模式”,文件会被打包。会减少大量请求。

image.png

命令行执行 vite 后做了什么?

  1. 创建 server 实例
  2. 启动监听端口
async (
  root: string,
  options: ServerOptions & ExperimentalDevOptions & GlobalCLIOptions,
) => {
  filterDuplicateOptions(options)
  // output structure is preserved even after bundling so require()
  // is ok here
  // 动态导入并创建开发服务器
  const { createServer } = await import('./server')
  try {
    const server = await createServer({
      root,
      base: options.base,
      mode: options.mode,
      configFile: options.config,
      configLoader: options.configLoader,
      logLevel: options.logLevel,
      clearScreen: options.clearScreen,
      server: cleanGlobalCLIOptions(options),
      forceOptimizeDeps: options.force,
      experimental: {
        bundledDev: options.experimentalBundle,
      },
    })

    // 校验服务器实例并启动
    if (!server.httpServer) {
      throw new Error('HTTP server not available')
    }

    // 启动 HTTP 服务器监听指定端口
    await server.listen()

    // 输出启动日志
    const info = server.config.logger.info

    const modeString =
    // 非 development 模式,输出环境模式
      options.mode && options.mode !== 'development'
        ? `  ${colors.bgGreen(` ${colors.bold(options.mode)} `)}`
        : ''

    // 启动耗时(计算从 Vite 启动到服务器就绪的时间)
    const viteStartTime = global.__vite_start_time ?? false
    const startupDurationString = viteStartTime
      ? colors.dim(
          `ready in ${colors.reset(
            colors.bold(Math.ceil(performance.now() - viteStartTime)),
          )} ms`,
        )
      : ''
    // 检查是否有已存在的日志输出(避免重复打印)
    const hasExistingLogs =
      process.stdout.bytesWritten > 0 || process.stderr.bytesWritten > 0

    // 输出核心启动日志(Vite 版本 + 模式 + 启动耗时)
    info(
      `\n  ${colors.green(
        `${colors.bold('VITE')} v${VERSION}`,
      )}${modeString}  ${startupDurationString}\n`,
      {
        clear: !hasExistingLogs,
      },
    )

    // 打印服务器访问地址(如 http://localhost:3000/)
    server.printUrls()
    const customShortcuts: CLIShortcut<typeof server>[] = []
    if (profileSession) {
      customShortcuts.push({
        key: 'p',
        description: 'start/stop the profiler',
        async action(server) {
          if (profileSession) {
            await stopProfiler(server.config.logger.info)
          } else {
            const inspector = await import('node:inspector').then(
              (r) => r.default,
            )
            await new Promise<void>((res) => {
              profileSession = new inspector.Session()
              profileSession.connect()
              profileSession.post('Profiler.enable', () => {
                profileSession!.post('Profiler.start', () => {
                  server.config.logger.info('Profiler started')
                  res()
                })
              })
            })
          }
        },
      })
    }
    // 绑定快捷键到服务器(print: true 表示打印快捷键说明)
    server.bindCLIShortcuts({ print: true, customShortcuts })
  } catch (e) {
    const logger = createLogger(options.logLevel)
    logger.error(
      colors.red(`error when starting dev server:\n${inspect(e)}`),
      {
        error: e,
      },
    )
    await stopProfiler(logger.info)
    process.exit(1)
  }
},

image.png

image.png

// 启动 HTTP 服务器监听指定端口
async listen(port?: number, isRestart?: boolean) {
  // 解析主机名
  const hostname = await resolveHostname(config.server.host)
  if (httpServer) {
    httpServer.prependListener('listening', () => {
      // 解析服务器监听的 URL 地址
      server.resolvedUrls = resolveServerUrls(
        httpServer,
        config.server,
        hostname,
        httpsOptions,
        config,
      )
    })
  }
  // 启动 HTTP 服务器
  await startServer(server, hostname, port)
  if (httpServer) {
    // 如果不是重启,配置了 open 选项打开浏览器
    if (!isRestart && config.server.open) server.openBrowser()
  }
  return server
},

createServer 函数做了什么工作?

  1. 参数解析与配置校验。
  2. 服务器基础设施创建(HTTP/WS/中间件/文件监听)。
  3. 多环境(environments)初始化。
  4. 服务器对象构建与向后兼容。
  5. 中间件栈构建。
  6. 文件变化监听与 HMR。
  7. 启动服务器逻辑。
  8. 返回 server 实例。

一、config 解析

  1. 加载配置文件:读取 vite.config.js / vite.config.ts(可通过 --config 指定其他文件)。如果文件是 TypeScript,Vite 会使用 esbuild 或 rolldown 动态编译。
  2. 合并命令行参数:命令行选项优先级高于配置文件。
  3. 应用默认值:补充未提供的选项(如 root 默认为 process.cwd()base 默认为 /)。
  4. 加载环境变量:根据 mode(默认 development)读取 .env 和 .env.[mode] 文件,注入 process.env 和 import.meta.env
  5. 加载插件:收集用户配置中的 plugins 数组,调用每个插件的 config 钩子(允许插件修改配置),最后调用 configResolved 钩子通知插件配置已解析完成。
  6. 生成 ResolvedConfig:输出完整的、只读的配置对象,包含 serverbuildoptimizeDepsenvironments 等字段。

image.png

image.png

二、服务器基础设施创建(HTTP/WS/中间件/文件监听)

  // 3、网络服务构建
  const middlewares = connect() as Connect.Server

  // middlewareMode 为 true 时,不解析 HTTP 服务器,以中间件模式创建;否则解析 HTTP 服务器
  const httpServer = middlewareMode
    ? null
    : await resolveHttpServer(middlewares, httpsOptions)

  // 创建 WebSocket 服务器
  const ws = createWebSocketServer(httpServer, config, httpsOptions)

新建 HTTP 服务

async function resolveHttpServer(
  app: Connect.Server,
  httpsOptions?: HttpsServerOptions,
): Promise<HttpServer> {
  // 如果没有 httpsOptions,创建 HTTP 服务器
  if (!httpsOptions) {
    // http 模块在 net 的基础上增加了 HTTP 协议解析和封装能力。
    // 当你创建一个 HTTP 服务器时,实际底层是一个 net.Server
    const { createServer } = await import('node:http')
    return createServer(app) // 创建 HTTP 服务器
  }

  // 如果有 httpsOptions,创建 HTTPS 服务器
  const { createSecureServer } = await import('node:http2')
  return createSecureServer(
    {
      // Manually increase the session memory to prevent 502 ENHANCE_YOUR_CALM
      // errors on large numbers of requests
      maxSessionMemory: 1000, // 增加会话内存,防止 502 错误
      // Increase the stream reset rate limit to prevent net::ERR_HTTP2_PROTOCOL_ERROR
      // errors on large numbers of requests
      streamResetBurst: 100000, // 增加流重置突发量,防止 net::ERR_HTTP2_PROTOCOL_ERROR 错误
      streamResetRate: 33, // 增加流重置速率,防止 net::ERR_HTTP2_PROTOCOL_ERROR 错误
      ...httpsOptions, // 合并 httpsOptions 选项
      allowHTTP1: true, // 允许 HTTP/1 协议
    },
    // @ts-expect-error TODO: is this correct?
    app,
  )
}

三、 多环境(environments)初始化

Vite 8 引入了多环境(Environments)概念,每个环境(如 clientssr)拥有独立的模块图、插件容器和依赖优化器。

  const environments: Record<string, DevEnvironment> = {}

  // 多环境(Environments)初始化
  await Promise.all(
    Object.entries(config.environments).map(
      async ([name, environmentOptions]) => {
        const environment = await environmentOptions.dev.createEnvironment(
          name,
          config,
          {
            ws,
          },
        )
        environments[name] = environment

        const previousInstance =
          options.previousEnvironments?.[environment.name]
        await environment.init({ watcher, previousInstance })
      },
    ),
  )

四、环境向后兼容

在 Vite 8 引入多环境(environments)之前,Vite 只有一个全局的模块图。升级到 Vite 8 后,每个环境(clientssr)有了自己独立的模块图,但为了不破坏现有的插件和 API,Vite 需要提供一个兼容层,使得老代码依然可以通过 server.moduleGraph 访问模块图。

五、中间件栈构建

  1. 请求计时器(仅 DEBUG 模式)
  2. 拒绝无效请求(过滤包含空格等非法字符的请求)
  3. CORS 中间件(默认启用)
  4. 主机验证(防止 DNS 重绑定攻击)
  5. 用户插件 configureServer 钩子(允许插件注入自定义中间件)
  6. 缓存转换中间件(若未启用 bundledDev
  7. 代理中间件(将 /api 等请求转发到后端服务器)
  8. Base 路径中间件(处理 base 配置)
  9. 编辑器打开支持/__open-in-editor
  10. HMR Ping 处理(响应客户端心跳)
  11. public 目录静态服务(直接返回 public 下的文件)
  12. 转换中间件(核心) :拦截对 .js.vue.ts 等文件的请求,调用插件链进行转换,返回最终代码。
  13. 静态文件服务:返回项目根目录下未被转换的静态资源。
  14. HTML fallback(SPA 模式下,未匹配路径返回 index.html
  15. index.html 转换中间件:注入客户端脚本(/@vite/client)和环境变量。
  16. 404 处理
  17. 错误处理中间件

六、利用 chokidar,文件变化监听

  // 9、文件变更事件处理
  // 监听文件变化事件
  watcher.on('change', async (file) => {
    file = normalizePath(file)
    // 检查是否是 TypeScript 配置文件变化,如果是则重启服务器
    reloadOnTsconfigChange(server, file)

    await Promise.all(
      Object.values(server.environments).map((environment) =>
        // 通知所有环境的插件容器文件已更新
        environment.pluginContainer.watchChange(file, { event: 'update' }),
      ),
    )
    // invalidate module graph cache on file change
    for (const environment of Object.values(server.environments)) {
      environment.moduleGraph.onFileChange(file)
    }
    // 触发热模块替换更新,将变更同步到客户端
    await onHMRUpdate('update', file)
  })

  // 监听文件添加事件
  watcher.on('add', (file) => {
    onFileAddUnlink(file, false)
  })
  // 监听文件删除事件
  watcher.on('unlink', (file) => {
    onFileAddUnlink(file, true)
  })
修改 tsconfig.app.json

image.png

修改 tsconfig.json文件

会全量刷新,执行 location.reload()

4:13:20 PM [vite] changed tsconfig file detected: /Users/xxxx/Documents/code/cloudcode/vue3-vite-cube/tsconfig.json - Clearing cache and forcing full-reload to ensure TypeScript is compiled with updated config values. (x2)

image.png

{
    "type": "custom",
    "event": "file-changed",
    "data": {
        "file": "/Users/xxx/Documents/code/cloudcode/vue3-vite-cube/tsconfig.json"
    }
}
修改 vue 页面的 script setup 块template 块
{
    "type": "custom",
    "event": "file-changed",
    "data": {
        "file": "/Users/xxx/Documents/code/cloudcode/vue3-vite-cube/src/pages/home/index.vue"
    }
}
{
    "type": "update",
    "updates": [
        {
            "type": "js-update",
            "timestamp": 1775036864943,
            "path": "/src/pages/home/index.vue",
            "acceptedPath": "/src/pages/home/index.vue",
            "explicitImportRequired": false,
            "isWithinCircularImport": false
        }
    ]
}

image.png

修改 vue 页面的 style 块

image.png

{
    "type": "custom",
    "event": "file-changed",
    "data": {
        "file": "/Users/xxxxxx/Documents/code/cloudcode/vue3-vite-cube/src/pages/home/index.vue"
    }
}
{
    "type": "update",
    "updates": [
        {
            "type": "js-update",
            "timestamp": 1775037234261,
            "path": "/src/pages/home/index.vue",
            "acceptedPath": "/src/pages/home/index.vue",
            "explicitImportRequired": false,
            "isWithinCircularImport": false
        },
        {
            "type": "js-update",
            "timestamp": 1775037234261,
            "path": "/src/pages/home/index.vue?vue&type=style&index=0&scoped=2c5296db&lang.css",
            "acceptedPath": "/src/pages/home/index.vue?vue&type=style&index=0&scoped=2c5296db&lang.css",
            "explicitImportRequired": false, // 示是否需要显式动态导入新模块
            "isWithinCircularImport": false // 表示是否处于循环依赖中
        }
    ]
}

image.png

七、启动服务器逻辑

真正启动服务器在 cli 中 server.listen 执行。

这里只是重写 listen方法 ,待启动服务器时执行。

  • 调用 server.listen()
  • 监听配置的端口(默认 5173)
  • 启动完成后执行回调
  • 自动打开浏览器(如果配置 server.open
  • 终端打印
  let initingServer: Promise<void> | undefined
  let serverInited = false // 标记服务器是否已初始化

  if (!middlewareMode && httpServer) {
    // overwrite listen to init optimizer before server start
    const listen = httpServer.listen.bind(httpServer)
    // 重写 listen 方法,确保在服务器启动前初始化优化器
    httpServer.listen = (async (port: number, ...args: any[]) => {
      try {
        await initServer(true)
      } catch (e) {
        httpServer.emit('error', e)
        return
      }
      // 调用原始 listen 方法启动服务器
      return listen(port, ...args)
    }) as any
  } else {
    await initServer(false)
  }
  const initServer = async (onListen: boolean) => {
    if (serverInited) return // 如果服务器已初始化,直接返回
    if (initingServer) return initingServer // 如果服务器正在初始化,直接返回

    initingServer = (async function () {
      // 如果没有配置 bundledDev,则在初始化服务器时调用 buildStart 方法
      if (!config.experimental.bundledDev) {
        // For backward compatibility, we call buildStart for the client
        // environment when initing the server. For other environments
        // buildStart will be called when the first request is transformed
        await environments.client.pluginContainer.buildStart()
      }

      // ensure ws server started
      // 确保 WebSocket 服务器已启动
      if (onListen || options.listen) {
        await Promise.all(
          // 确保所有环境的服务器都启动
          Object.values(environments).map((e) => e.listen(server)),
        )
      }

      initingServer = undefined // 清空初始化 Promise
      serverInited = true // 标记服务器已初始化
    })()
    return initingServer
  }

热更新

Vite 的热更新(HMR)基于原生 ES 模块和 WebSocket 实现,能在文件修改后仅更新受影响的模块,无需刷新页面,从而保留应用状态。其原理可分为服务端和客户端两个阶段。

服务端:变化检测与消息推送

一、文件检测

Vite 使用 chokidar 库来监听文件系统的变化。在 _createServer 函数中,会创建一个文件监听器(watcher),监听范围包括:

  • 项目根目录(root
  • 配置文件依赖(config.configFileDependencies
  • 环境变量文件(.env 等)
  • public 目录
(chokidar.watch(
    // config file dependencies and env file might be outside of root
    [
      ...(config.experimental.bundledDev ? [] : [root]),
      ...config.configFileDependencies,
      ...getEnvFilesForMode(config.mode, config.envDir),
      // Watch the public directory explicitly because it might be outside
      // of the root directory.
      ...(publicDir && publicFiles ? [publicDir] : []),
    ],

    resolvedWatchOptions,
  ) as FSWatcher)

二、模块图与依赖分析

1、 模块图的数据结构

  • urlToModuleMap:根据 URL 查找模块节点。
  • fileToModulesMap:根据文件路径查找对应的模块节点(一个文件可能对应多个模块,如 ?import 和 ?url 查询)。
  • 每个模块节点(ModuleNode)记录了:
    • importers:依赖该模块的模块(即父模块)。
    • importedModules:该模块导入的子模块。

2、依赖分析

当文件发生变化时,handleHMRUpdate 会执行以下步骤:

  1. 根据文件路径找到对应的模块节点(moduleGraph.getModulesByFile(file))。
  2. 遍历这些模块节点,收集所有受影响的模块(包括自己以及所有 importers 链上的模块)。
  3. 通过模块图向上追溯,找到所有依赖该模块的模块,直到没有更多依赖者为止

三、重新编译与生成更新消息

对于每个受影响的模块,Vite 调用 environment.transformRequest(url) 重新进行转换。该函数会经过完整的插件链(resolveId → load → transform),生成新的模块代码和 source map,并更新模块图中的 transformResult 缓存。

编译过程中,Vite 会记录一个时间戳(timestamp),用于客户端绕过浏览器缓存。

四、Websocket 推送消息

Vite 开发服务器内置了一个 WebSocket 服务器,用于与客户端通信。当 update 消息生成后,Vite 会通过 WebSocket 将其推送给所有已连接的客户端。

客户端:接收消息并执行更新

一、客户端初始化与 Websocket 连接

1、注入客户端脚本

当浏览器请求 index.html 时,Vite 的 indexHtmlMiddleware 会调用 clientPlugin 的 transformIndexHtml 钩子,在 HTML 中自动注入 <script type="module" src="/@vite/client">,该脚本负责建立 WebSocket 连接,暴露 HMR API。

image.png

2、建立 WebSocket 连接

客户端脚本首先会创建一个 WebSocket 连接指向开发服务器(默认地址 ws://localhost:5173)。同时,它会监听 openmessagecloseerror 等事件。

连接成功后,服务端会发送 { type: 'connected' } 消息,客户端收到后标记为就绪状态。

3、暴露 import.meta.hot API

客户端在全局维护了几个 Map 结构,用于存储每个模块注册的 HMR 回调(acceptdispose 等)。同时,它定义了一个 createHotContext 函数,该函数返回一个包含 acceptdisposeinvalidate 等方法的对象。

二、接收消息与类型分发

客户端 WebSocket 的 message 事件处理函数负责解析 JSON 消息,并根据 type 字段分发到不同的处理逻辑。

客户端 WebSocket 收到消息后,根据 type 进行处理:

  • connected, 标记就绪,可发送预热请求。
  • update:遍历 updates 数组,对每个更新执行热替换。
  • full-reload:调用 location.reload() 刷新页面。
  • prune,
  • custom,自定义事件。
  • error:在页面上显示错误覆盖层。
  • ping,不做处理。
async function handleMessage(payload: HotPayload) {
  switch (payload.type) {
    // WebSocket 和服务器握手成功,打印日志。
    case 'connected':
      console.debug(`[vite] connected.`)
      break
    // JS/CSS 热更新
    case 'update':
      // 通知所有插件 / 监听:马上要热更新了
      // 用于在热更新前执行自定义逻辑,例如刷新页面
      await hmrClient.notifyListeners('vite:beforeUpdate', payload)
      if (hasDocument) {
        // if this is the first update and there's already an error overlay, it
        // means the page opened with existing server compile error and the whole
        // module script failed to load (since one of the nested imports is 500).
        // in this case a normal update won't work and a full reload is needed.
        // 首次更新容错 + 清理错误
        if (isFirstUpdate && hasErrorOverlay()) {
          // 如果页面一打开就报错(编译失败),第一次热更新直接全页刷新,确保能正常运行
          location.reload() // 刚打开页面就报错,直接刷新修复
          return
        } else {
          if (enableOverlay) {
            clearErrorOverlay() // 清空之前的报错
          }
          isFirstUpdate = false
        }
      }
      // 所有文件更新并行处理,速度极快
      await Promise.all(
        payload.updates.map(async (update): Promise<void> => {
          if (update.type === 'js-update') {
            return hmrClient.queueUpdate(update) // 交给核心引擎更新JS
          }

          // css-update
          // this is only sent when a css file referenced with <link> is updated
          const { path, timestamp } = update
          const searchUrl = cleanUrl(path)
          // can't use querySelector with `[href*=]` here since the link may be
          // using relative paths so we need to use link.href to grab the full
          // URL for the include check.
          // 找到页面对应的旧 <link> 标签
          // 页面 <link href="style.css"> 是相对路径
          // e.href 会返回 http://localhost:5173/src/style.css 完整 URL
          const el = Array.from(
            document.querySelectorAll<HTMLLinkElement>('link'),
          ).find(
            (e) =>
              !outdatedLinkTags.has(e) && cleanUrl(e.href).includes(searchUrl),
          )

          if (!el) {
            return
          }

          // 拼接带时间戳的新 CSS 路径
          const newPath = `${base}${searchUrl.slice(1)}${
            searchUrl.includes('?') ? '&' : '?'
          }t=${timestamp}`

          // rather than swapping the href on the existing tag, we will
          // create a new link tag. Once the new stylesheet has loaded we
          // will remove the existing link tag. This removes a Flash Of
          // Unstyled Content that can occur when swapping out the tag href
          // directly, as the new stylesheet has not yet been loaded.
          return new Promise((resolve) => {
            // 克隆新 link 标签,不直接改旧 href
            const newLinkTag = el.cloneNode() as HTMLLinkElement
            newLinkTag.href = new URL(newPath, el.href).href
            const removeOldEl = () => {
              el.remove()
              console.debug(`[vite] css hot updated: ${searchUrl}`)
              resolve()
            }
            // 等新 CSS 加载完成后,再删除旧标签
            newLinkTag.addEventListener('load', removeOldEl)
            newLinkTag.addEventListener('error', removeOldEl)
            // 缓存新标签,避免重复删除
            outdatedLinkTags.add(el)
            // 插入新标签到旧标签后面
            el.after(newLinkTag)
          })
        }),
      )
      // 触发更新完成事件
      // 通知插件 / 框架:热更新完成
      await hmrClient.notifyListeners('vite:afterUpdate', payload)
      break
    //  处理 custom 自定义消息
    case 'custom': {
      await hmrClient.notifyListeners(payload.event, payload.data)

      if (payload.event === 'vite:ws:disconnect') {
        // dom环境,且页面未卸载
        if (hasDocument && !willUnload) {
          console.log(`[vite] server connection lost. Polling for restart...`)
          const socket = payload.data.webSocket as WebSocket
          const url = new URL(socket.url)
          url.search = '' // remove query string including `token`
          await waitForSuccessfulPing(url.href) // 轮询等待服务器重启
          location.reload() // 服务器回来后,自动刷新页面
        }
      }
      break
    }
    // 处理 full-reload 全页刷新
    case 'full-reload':
      await hmrClient.notifyListeners('vite:beforeFullReload', payload)
      if (hasDocument) {
        if (payload.path && payload.path.endsWith('.html')) {
          // if html file is edited, only reload the page if the browser is
          // currently on that page.
          const pagePath = decodeURI(location.pathname)
          const payloadPath = base + payload.path.slice(1)
          if (
            pagePath === payloadPath ||
            payload.path === '/index.html' ||
            (pagePath.endsWith('/') && pagePath + 'index.html' === payloadPath)
          ) {
            pageReload()
          }
          return
        } else {
          pageReload()
        }
      }
      break
    //  处理 prune 清理模块
    case 'prune':
      await hmrClient.notifyListeners('vite:beforePrune', payload)
      await hmrClient.prunePaths(payload.paths)
      break
    // 显示红色错误遮罩
    case 'error': {
      await hmrClient.notifyListeners('vite:error', payload)
      if (hasDocument) {
        const err = payload.err
        if (enableOverlay) {
          createErrorOverlay(err)
        } else {
          console.error(
            `[vite] Internal Server Error\n${err.message}\n${err.stack}`,
          )
        }
      }
      break
    }
    // 处理 ping 消息,心跳检测,不处理任何逻辑
    case 'ping': // noop
      break
    // 处理默认情况
    default: {
      const check: never = payload
      return check
    }
  }
}

三、处理 update 消息(热更新)

  1. 请求新模块(带时间戳),每个 update 对象包含 pathacceptedPathtimestamp 等字段。客户端构造新的 UR,利用 ?t=timestamp 强制绕过浏览器缓存。使用动态 import() 获取模块的导出对象。
  2. 执行 dispose 回调(清理旧资源),在替换模块之前,需要先执行旧模块注册的 dispose 回调(如果有),以便清理定时器、事件监听等。
  3. 找到接受更新的模块,
  4. 针对css处理。如果 update.type === 'css-update',客户端不会通过 import() 请求,而是直接替换页面中的 <link> 或 <style> 标签。
  5. 失败回退(full-reload),如果更新过程中发生错误(例如网络请求失败、回调抛出异常),或者找不到任何接受回调,客户端会发送 full-reload 指令,刷新整个页面以确保应用状态正确。

image.png

image.png

客户端执行 js-update

importUpdatedModule 是 Vite HMR 的模块更新加载器:拼接带时间戳的最新 URL,动态加载新代码 ,循环依赖异常时自动刷新。

  // 普通 ESM 模式
  // 动态加载最新的模块代码 → 解决浏览器缓存 → 处理循环依赖错误
  async function importUpdatedModule({
    acceptedPath, // 要更新的模块路径
    timestamp, // 模块更新时间戳
    explicitImportRequired, // 是否显式导入
    isWithinCircularImport, // 是否在循环依赖里
  }) {
    // 拆分路径
    const [acceptedPathWithoutQuery, query] = acceptedPath.split(`?`)
    const importPromise = import(
      /* @vite-ignore */ // 告诉 vite 不解析这个动态导入,由浏览器负责加载
      base +
      // 移除前导斜杠,确保路径正确
        acceptedPathWithoutQuery.slice(1) +
        // timestamp 用于刷新浏览器缓存,确保加载最新代码
        `?${explicitImportRequired ? 'import&' : ''}t=${timestamp}${
          query ? `&${query}` : ''
        }`
    )
    if (isWithinCircularImport) {
      // 循环依赖, 热更失败 → 自动刷新页面
      importPromise.catch(() => {
        console.info(
          `[hmr] ${acceptedPath} failed to apply HMR as it's within a circular import. Reloading page to reset the execution order. ` +
            `To debug and break the circular import, you can run \`vite --debug hmr\` to log the circular dependency path if a file change triggered it.`,
        )
        pageReload()
      })
    }
    // 返回模块
    return await importPromise
  }

importUpdatedModule 负责用原生 ESM 加载最新代码,通知 Rolldown 运行时更新模块导出,循环依赖异常自动刷新页面。

  // 打包开发模式(bundledDev)
  async function importUpdatedModule({
    url,
    acceptedPath,
    isWithinCircularImport, // 是否在循环依赖里
  }) {
    // 加载新代码,并通知 Rolldown 运行时更新模块
    // import(base + url!) 浏览器原生 ESM 动态导入
    // 浏览器发起网络请求 → 访问 Vite 开发服务器
    // url 已带时间戳 → 强制不缓存,加载最新版
    const importPromise = import(base + url!).then(() =>
      // @ts-expect-error globalThis.__rolldown_runtime__
      // 全局运行时.loadExports
      // __rolldown_runtime__:Rolldown 运行时(Vite 新一代底层打包 / 运行核心)
      // loadExports(acceptedPath)
        // → 告诉运行时:重新收集这个模块的最新导出
        // → 运行时会自动更新所有引用该模块的地方
      globalThis.__rolldown_runtime__.loadExports(acceptedPath),
    )
    // 循环依赖容错
    if (isWithinCircularImport) {
      // 热更失败 → 自动刷新页面
      importPromise.catch(() => {
        console.info(
          `[hmr] ${acceptedPath} failed to apply HMR as it's within a circular import. Reloading page to reset the execution order. ` +
            `To debug and break the circular import, you can run \`vite --debug hmr\` to log the circular dependency path if a file change triggered it.`,
        )
        pageReload()
      })
    }
    return await importPromise
  }

更新文件

  /**
   * 处理 HMR 更新
   * @param type 文件操作类型
   * @param file 文件路径
   */
  const onHMRUpdate = async (
    type: 'create' | 'delete' | 'update',
    file: string,
  ) => {
    // 如果 HMR 已启用,则处理 HMR 更新
    if (serverConfig.hmr !== false) {
      await handleHMRUpdate(type, file, server)
    }
  }

新增文件/删除文件


  /**
   * 处理文件添加或删除
   * @param file 文件路径
   * @param isUnlink 是否删除文件
   */
  const onFileAddUnlink = async (file: string, isUnlink: boolean) => {
    file = normalizePath(file)
    // 「检测文件是否为 tsconfig.json/jsconfig.json,若是则触发服务器重启」
    // 因为这类配置文件变更会影响模块解析规则,必须重启才能生效。
    reloadOnTsconfigChange(server, file)

    await Promise.all(
      // 通知所有环境的插件容器,同步文件变更事件
      Object.values(server.environments).map((environment) =>
        // 对每个环境,调用其插件容器的 watchChange 方法
        // 传递文件路径和事件类型('delete' 或 'create')
        environment.pluginContainer.watchChange(file, {
          event: isUnlink ? 'delete' : 'create',
        }),
      ),
    )

    if (publicDir && publicFiles) {
      if (file.startsWith(publicDir)) {
        const path = file.slice(publicDir.length)
        publicFiles[isUnlink ? 'delete' : 'add'](path)

        // 新增文件时:清理同名模块的 ETag 缓存,保证公共文件优先响应
        // Vite 会为模块生成 ETag(实体标签),用于「ETag 快速路径」—— 客户端请求时,若 ETag 未变,直接返回缓存的模块内容
        if (!isUnlink) {
          // 获取客户端环境的模块图实例
          const clientModuleGraph = server.environments.client.moduleGraph
          // 根据路径 path(如 /image.png)查找模块图中是否存在同名模块
          const moduleWithSamePath =
            await clientModuleGraph.getModuleByUrl(path)

          const etag = moduleWithSamePath?.transformResult?.etag

          // 如果有etag ,则删除。
          // 保证 public 下文件等优先级
          if (etag) {
            // The public file should win on the next request over a module with the
            // same path. Prevent the transform etag fast path from serving the module
            clientModuleGraph.etagToModuleMap.delete(etag)
          }
        }
      }
    }
    // 文件删除时,清理模块依赖图缓存
    if (isUnlink) {
      // invalidate module graph cache on file change
      for (const environment of Object.values(server.environments)) {
        environment.moduleGraph.onFileDelete(file)
      }
    }
    // 触发 HMR 更新,同步变更到客户端
    await onHMRUpdate(isUnlink ? 'delete' : 'create', file)
  }

禁止热更新

  server: {
    ws: false,
  }

修改文件浏览器内容不会自动更新。

image.png

image.png

image.png

重启服务器

什么场景会触发开发服务器重启?

  1. 修改 vite.config.js 配置文件。
  2. 依赖文件修改,如 package.json
  3. 创建/修改 .env 环境文件。
  4. 插件中调用 server.restart

image.png

  // 配置文件、配置文件依赖、环境文件变化时,自动重启服务器
  if (isConfig || isConfigDependency || isEnv) {
    // auto restart server
    debugHmr?.(`[config change] ${colors.dim(shortFile)}`)

    // 打印日志
    config.logger.info(
      colors.green(
        `${normalizePath(
          path.relative(process.cwd(), file),
        )} changed, restarting server...`,
      ),
      { clear: true, timestamp: true },
    )
    try {
      // 重启服务器
      await restartServerWithUrls(server)
    } catch (e) {
      config.logger.error(colors.red(e))
    }
    return
  }

server.restart

重启服务器前,会先关闭服务器(包含 停止HTTP服务,停止Websocket 服务,关闭文件监听,关闭所有环境的 DevEnvironment 实例,释放模块图、插件容器、依赖优化器等资源)。

// 重启 Vite 开发服务器,同时处理并发重启请求,确保同一时间只有一个重启操作在执行。
async restart(forceOptimize?: boolean) {
  // 如果没有重启 Promise,创建一个
  if (!server._restartPromise) {
    // 设置是否强制优化依赖
    server._forceOptimizeOnRestart = !!forceOptimize
    // 重启服务器
    server._restartPromise = restartServer(server).finally(() => {
      // 重启完成后,重置重启 Promise 和强制优化依赖
      server._restartPromise = null
      server._forceOptimizeOnRestart = false
    })
  }
  // 如果存在,说明已经有一个重启操作在进行中,直接返回该 Promise
  return server._restartPromise
},

全量更新

什么场景会触发全量刷新?

  1. 修改index.html文件。
  2. 修改main.ts文件。
  3. 修改路由配置 router/index.ts 文件。

image.png

image.png

{
    "type": "full-reload",
    "triggeredBy": "/Users/xxxxxx/Documents/code/cloudcode/vue3-vite-cube/src/common/utils.ts",
    "path": "*"
}
  // (dev only) the client itself cannot be hot updated.
  // Vite 客户端自身文件变更 → 不能热更 → 必须整页刷新
  if (file.startsWith(withTrailingSlash(normalizedClientDir))) {
    environments.forEach(({ hot }) =>
      hot.send({
        type: 'full-reload',
        path: '*',
        triggeredBy: path.resolve(config.root, file),
      }),
    )
    return
  }

最后

  1. Websoket
  2. vite server 配置项
❌
❌