vite 的整体架构设计

  • Vite 核心采用的是分层解耦架构进行项目的设计和实现吧,主要是分为的是四个层的东西吧,每个模块的职责清晰、依赖明确的特性吧

核心的架构

  • 基础依赖层:提供的是底层能力的支撑实现吧,选用的是社区内部的成熟的已经有的依赖进行实现的讷,比如说 esbuild 或者说 rollup 来实现的吧,解决了自研带来的开发效率的问题以及做了一定的性能优化吧,底层来说的话相比于我们的 webpack 略显不足

  • 核心内核层:vite 的核心逻辑是集合成熟工具,本身只是一个上层的调度接口封装整合来实现的讷,内部处理核心流程

  • 插件扩展层:提供插件化的设计,来实现开发阶段和生产阶段的rollup 和 esbuild 吧

  • 上层的api层:对外暴露的接口层,比如说 CLI,内置的配置文件default的配置

vite 核心使用的是 monorepo 的架构来实现吧,还是比较好用的讷

核心模块的交互关系

  • 上层接口层接受的是用户的输入(CLI 开发,配置文件解析),然后将解析出来的配置形成一个 context 传递给核心的内核

  • 核心内核根据多场景进行开发和生产环境下的调度,负责的是调度 rollup esbuild 或者说是vite 本身的逻辑来执行流程

  • 插件扩展层核心做的是进行关键节点的插入以及实现自定义一些个性化功能的增强吧

  • 最终在上层的接口层进行输出结果给开发服务器和构建产物吧

底层技术探究

  • Vite 技术的选型是:专精工具复用的 底层哲学吧,每个依赖都是对应特定场景的性能和功能需求呢,无冗余选型

核心技术栈的选型

  • esbuild 来实现生产阶段的预构建和开发模块的编译实现吧

  • rollup 生产阶段使用的构建工具吧

  • connect 开发服务器的实现吧,类似于 http-proxy-middleware,底层基于 http 模块来进行开发实现的讷

  • chokidar 文件系统的监听实现讷,触发热更新HMR 和重新编译的实现吧

  • ws websocket的通信服务实现讷,实现服务端和客户端的 HMR 的消息推送吧

底层依赖的协同逻辑

  • 开发阶段:esbuild 负责编译/预构建(追求速度),Connect 负责请求拦截,ws 负责 HMR 通信,chokidar 负责文件监听;

  • 生产阶段:esbuild 辅助预构建,Rollup 负责核心打包(追求质量),最终生成优化后的产物。

Vite 核心哲学

按需编译

  • 基于原生的 ESM 的支持,实现将全量打包改进为了请求驱动的按需编译实现吧,传统的工具在启动的时候打包所有的模块,但是 vite 仅仅是在浏览器请求模块的时候才进行模块的编译,启动速度和项目的提及进行了完善

底层模块拆解实现

模块的管理、依赖处理、请求拦截三个核心来实现的按需编译、高效热更新

模块依赖Module Graph

  • 模块图谱是 vite 追踪模块依赖关系实现按需编译和精准 HMR 的核心吧

  • 其核心作用就是进行记录每个模块的依赖项,编译状态,确保仅当模块依赖变化的时候才会触发重新编译实现讷

import fs from 'node:fs';
import esbuild from 'esbuild';

interface ModuleNode {
    moduleId: string;
    filePath: string;
    deps: Set<ModuleNode>;
    importers: Set<ModuleNode>;
}

class ModuleNodeImpl implements ModuleNode {
    moduleId: string;
    filePath: string;
    deps: Set<ModuleNode> = new Set();
    importers: Set<ModuleNode> = new Set();
    constructor(moduleId: string, filePath: string) {
        this.moduleId = moduleId;
        this.filePath = filePath;
    }
}

class ModuleGraph {
    private modules = new Map<string, ModuleNode>();  // 模块 ID 到模块节点的映射
    private idToModuleMap = new Map<string, ModuleNode>();  // 真是路径的模块节点的映射吧
    private config: any;

    constructor(config: any) {
        this.config = config;
    }

    /**
     * 确保的是入口来自于符合格式的url吧
     */
    async ensureEntryFromUrl(url: string): Promise<ModuleNode> {
        const { moduleId, filePath } = await this.resolveRequest(url, this.config);
        let module = this.modules.get(moduleId);
        if (!module) {
            module = new ModuleNodeImpl(moduleId, filePath);
            this.modules.set(moduleId, module);
            this.idToModuleMap.set(filePath, module);
            // 解析模块依赖递归构建模块图
            await this.parseModuleDependencies(module);
        }
        return module;
    }

    private async resolveRequest(url: string, config: any) {
        const { moduleId, filePath } = await config.resolveRequest(url);
        return { moduleId, filePath };
    }

    private getLoaderByFilePath(filePath: string) {
        return this.config.getLoaderByFilePath(filePath);
    }
    
    // 解析模块依赖(快速且兼容多种格式)
    private async parseModuleDependencies(module: ModuleNodeImpl) {
        const code = fs.readFileSync(module.filePath, 'utf-8');
        // 用esbuild解析模块依赖(快速且兼容多种格式)
        const { imports } = await esbuild.parse(code, {
            sourcefile: module.filePath,
            loader: this.getLoaderByFilePath(module.filePath)
        });
        // 遍历依赖,递归添加到图谱
        for (const imp of imports) {
            const depModule = await this.ensureEntryFromUrl(imp.path);
            module.deps.add(depModule);
            depModule.importers.add(module);
        }
    }
}

模块图谱采用“懒构建”策略,仅在模块被请求时才解析其依赖并添加到图谱,与按需编译逻辑完全对齐;同时通过 deps(依赖项)和 importers(被依赖项)双向关联,实现 HMR 时的依赖链更新通知

依赖预购键

  • 触发条件:首次启动项目package.json 依赖变化、Vite 配置变化、缓存文件损坏或过期;

  • 缓存校验:通过 node_modules/.vite/_metadata.json 记录依赖版本、配置哈希、预构建时间戳,启动时对比这些信息,仅当不匹配时才重新预构建;

  • 冲突处理:当多个依赖引用同一子依赖但版本不同时,Vite 会分别预构建不同版本,避免版本冲突导致的运行时错误


async function optimizeDeps(config: ResolvedConfig) {
  const root = config.root;
  const cacheDir = path.join(root, 'node_modules', '.vite');
  // 1. 扫描项目依赖
  const entryPoints = [config.root + '/index.html'];
  const { dependencies } = await scanDependencies(entryPoints, config);
  // 2. 过滤已缓存且无需更新的依赖
  const metadata = loadMetadata(cacheDir);
  const needOptimize = dependencies.filter(dep => !isCached(dep, metadata));
  if (needOptimize.length === 0) return;
  // 3. 用esbuild预构建:格式转换+模块合并
  await esbuild.build({
    entryPoints: needOptimize,
    outdir: path.join(cacheDir, 'deps'),
    format: 'esm',
    bundle: true, // 合并模块
    splitting: false,
    sourcemap: false,
    external: config.optimizeDeps.exclude, // 排除无需预构建的依赖
    plugins: [
      // 处理CommonJS转ESM
      commonjsPlugin(),
      // 重写模块路径为预构建后的路径
      rewriteImportPlugin(cacheDir)
    ]
  });
  // 4. 更新缓存元数据
  saveMetadata(cacheDir, dependencies, config);
}

Vite 的模块解析逻辑是连接“浏览器请求路径”与“本地文件路径”的核心,需处理别名、裸模块、后缀补全、条件导出等场景,源码入口packages/vite/src/node/resolve.ts

核心解析流程:浏览器请求路径(如 /src/utils.ts)→ 去除查询参数与哈希 → 应用别名配置 → 补全文件后缀.ts.ts.tsx.d.ts)→ 处理裸模块(如 vuenode_modules/vue/dist/vue.esm-browser.js)→ 确定真实文件路径。

关键优化:路径解析结果会缓存到内存,重复请求无需重复解析;同时支持“条件导出”(如 package.json 中的 exports 字段),适配不同环境(开发/生产、浏览器/Node.js)的模块导出

核心执行流程

依赖预构建

  1. 入口扫描:从项目入口(默认 index.html)出发,递归扫描所有 importrequire 语句,区分第三方依赖node_modules 内)与项目源码依赖;

  2. 依赖过滤:根据 optimizeDeps.includeexclude 配置,筛选需要预构建的第三方依赖;

  3. 格式转换:将 CommonJS/UMD 格式的依赖转为 ESM,处理 require 语句为 import,适配浏览器原生加载;

  4. 模块合并:将依赖树中的多个小模块合并为单个 chunk(如 reactreact-dom 合并为 react.js),减少 HTTP 请求数;

  5. 路径重写:将项目中对第三方依赖的引用路径,重写为预构建后的路径(如 import 'vue'import '/node_modules/.vite/deps/vue.js');

  6. 缓存写入:将预构建产物与元数据写入 node_modules/.vite,供后续启动复用。

HMR 的边界场景和降级

  • 不可热更新场景:入口文件index.html)、全局样式文件(如 main.css)、动态导入的模块import() 加载的模块),这类模块变化时会触发页面整页刷新;

  • 循环依赖处理:当模块存在循环依赖时,HMR 会跳过依赖链更新,仅重新加载变化模块,避免无限循环更新;

  • 错误降级:当 HMR 逻辑执行失败(如插件处理错误),Vite 会自动降级为整页刷新,并在控制台输出错误信息,不阻塞开发流程。

生产环境优化

  1. 预构建二次优化:复用开发阶段的预构建结果,但针对生产环境合并更多小模块,移除开发阶段的 HMR 注入代码;

  2. 代码分割:通过 Rollup 的 manualChunks 配置,将第三方依赖与项目源码分割为不同 chunk,实现依赖缓存复用(第三方依赖变化少,可长期缓存);

  3. 兼容性处理:通过 @vitejs/plugin-legacy 生成 ES5 产物,同时注入 core-js polyfill,适配低版本浏览器(如 IE11);

  4. 产物压缩:用 esbuild 压缩 JS/CSS/HTML,比 Terser、cssnano 快 10-20 倍,同时支持压缩级别配置;

  5. sourcemap 优化:根据 build.sourcemap 配置生成不同类型的 sourcemap(如 hidden 仅用于调试,不暴露给生产环境),平衡调试体验与产物体积。

Vite Dev Server

启动流程


async function createServer(config: UserConfig = {}): Promise<ViteDevServer> {
  // 1. 解析并合并配置(用户配置 + 默认配置)
  const resolvedConfig = await resolveConfig(config, 'serve');
  // 2. 依赖预构建(优化第三方依赖,核心性能保障)
  await optimizeDeps(resolvedConfig);
  // 3. 创建 Connect 服务器(轻量 HTTP 框架,支持中间件)
  const app = connect() as Connect.Server;
  // 4. 注册核心中间件(请求拦截、模块编译、静态资源处理)
  app.use(transformMiddleware(resolvedConfig)); // 模块编译中间件
  app.use(serveStaticMiddleware(resolvedConfig)); // 静态资源中间件
  app.use(historyApiFallbackMiddleware(resolvedConfig)); // 路由 fallback
  // 5. 启动 HTTP 服务器,监听端口
  const server = http.createServer(app);
  // 6. 初始化 WebSocket 服务(HMR 实时通信)
  const ws = createWebSocketServer(server, resolvedConfig);
  // 7. 启动文件监听(chokidar),触发文件变化处理
  const watcher = chokidar.watch(resolvedConfig.root, {
    ignored: [/node_modules/, /\.git/], // 忽略无需监听的目录
    persistent: true
  });
  watcher.on('all', (event, file) => handleFileChange(event, file, ws, resolvedConfig));
  // 8. 返回 Dev Server 实例(供外部调用)
  return { app, server, ws, watcher, resolvedConfig };
}
    

模块请求流程

transformMiddleware 是请求处理的核心,负责拦截模块请求、编译模块并返回 ESM 代码,源码简化如下:


function transformMiddleware(config: ResolvedConfig) {
  return async (req: Connect.IncomingMessage, res: Connect.ServerResponse, next: Connect.NextFunction) => {
    const url = req.url!.replace(/\?.*$/, ''); // 去除 URL 参数,简化路径
    // 过滤非模块请求(如图片、HTML、接口请求)
    if (!isModuleRequest(url) || isExternalUrl(url)) return next();
    try {
      // 1. 解析真实文件路径(补全后缀、处理别名、裸模块)
      const { filePath, moduleId } = await resolveRequest(url, config);
      // 2. 读取文件内容
      let code = fs.readFileSync(filePath, 'utf-8');
      // 3. 按文件类型编译(适配不同模块格式)
      if (filePath.endsWith('.vue')) {
        // 编译 Vue 单文件组件(调用 @vue/compiler-sfc)
        code = await compileVueSFC(filePath, code, config);
      } else if (filePath.endsWith('.ts') || filePath.endsWith('.tsx')) {
        // 用 esbuild 编译 TS(速度远超 tsc)
        const { code: compiledCode } = await esbuild.transform(code, {
          loader: 'tsx',
          target: 'esnext',
          sourcemap: config.server.sourcemap
        });
        code = compiledCode;
      } else if (filePath.endsWith('.css')) {
        // 编译 CSS 为 ESM 模块(注入 style 标签)
        code = await compileCSS(code, filePath, config);
      }
      // 4. 注入 HMR 代码(开发阶段专属,支持热更新)
      code = injectHmrCode(code, moduleId, config);
      // 5. 设置响应头,返回编译后的 ESM 代码
      res.setHeader('Content-Type', 'application/javascript; charset=utf-8');
      res.setHeader('X-Vite-Sourcemap', 'true');
      res.end(code);
    } catch (err) {
      // 错误处理:返回格式化的错误信息,便于开发调试
      res.statusCode = 500;
      res.end(formatError(err));
    }
  };
}
    

热更新HMR 流程

服务端逻辑(文件变化处理)

function handleFileChange(event: string, file: string, ws: WebSocketServer, config: ResolvedConfig) {
  // 1. 过滤无效文件变化(如临时文件)
  if (isIgnoredFile(file, config)) return;
  // 2. 编译变化后的文件
  const { code, moduleId } = compileChangedFile(file, config);
  // 3. 确定更新类型(按文件类型区分,如 JS/Vue/CSS)
  const updateType = getUpdateType(file);
  // 4. 通过 WebSocket 推送更新消息给客户端
  ws.send(JSON.stringify({
    type: 'update',
    updates: [{ type: updateType, moduleId, code }]
  }));
}

客户端逻辑

// 浏览器端 HMR 运行时
const ws = new WebSocket(`ws://${location.host}`);
ws.onmessage = async (e) => {
  const data = JSON.parse(e.data);
  if (data.type !== 'update') return;
  for (const update of data.updates) {
    switch (update.type) {
      case 'css-update':
        // CSS 模块:直接注入新样式,覆盖旧样式(无需刷新)
        updateStyle(update.moduleId, update.code);
        break;
      case 'vue-update':
      case 'js-update':
        // JS/Vue 模块:重新加载模块并执行 HMR 钩子
        const newModule = await import(`${update.moduleId}?t=${Date.now()}`);
        if (window.__VITE_HMR_UPDATE__) {
          window.__VITE_HMR_UPDATE__(update.moduleId, newModule);
        }
        break;
    }
  }
};

build 流程

  1. 配置解析与依赖预构建:复用开发阶段的依赖预构建逻辑,但针对生产环境优化(如合并更多小模块);

  2. 生成 Rollup 配置:将 Vite 配置转换为 Rollup 可识别的配置,注入内置插件(如 CSS 提取、静态资源处理);

async function createRollupOptions(config: ResolvedConfig) {
  const rollupOptions: RollupOptions = {
    input: config.build.rollupOptions.input || config.root + '/index.html',
    output: {
      format: 'esm',
      dir: config.build.outDir,
      chunkFileNames: 'assets/[name]-[hash].js',
      entryFileNames: 'assets/[name]-[hash].js',
      assetFileNames: 'assets/[name]-[hash].[ext]'
    },
    plugins: [
      // 注入 Vite 内置 Rollup 插件
      viteRollupPlugin(config),
      cssExtractPlugin(config), // 提取 CSS 为单独文件
      assetHandlingPlugin(config) // 处理静态资源
    ],
    treeshake: true, // 开启 Tree-shaking 剔除无用代码
    cache: config.build.cache // 开启构建缓存
  };
  // 合并用户自定义 Rollup 配置
  return mergeConfig(rollupOptions, config.build.rollupOptions);
}
  1. Rollup 打包:执行 Rollup 构建,完成代码分割、Tree-shaking、模块合并;

  2. 产物优化:通过 esbuild 压缩代码(JS/CSS),生成 sourcemap,处理兼容性;

  3. 产物写入:将优化后的产物写入磁盘,输出构建完成信息