Skip to content

Vite 深入原理

Vite 架构、预构建、HMR 实现深度解析

七、Vite 深入原理

7.1 预构建详解

javascript
// 为什么需要预构建?

// 问题 1: CommonJS 不能直接在浏览器运行
// lodash 是 CommonJS,需要转换为 ESM
import _ from 'lodash'  // ❌ 浏览器不认识 CommonJS

// 问题 2: 依赖有大量内部模块
// lodash-es 有 600+ 个内部模块
// 每个模块一个 HTTP 请求,太慢了
import debounce from 'lodash-es/debounce'
// 实际会触发几十个请求 (debounce 的依赖)

// 预构建解决方案
// 1. 用 esbuild 将依赖打包为单个 ESM 文件
// 2. 放到 node_modules/.vite/deps 目录
// 3. 重写 import 路径指向预构建产物

// 预构建流程
/*
启动 Vite


┌─────────────────────────────┐
│ 扫描项目源码中的 bare import │
│ import xxx from 'lodash'    │
└─────────────────────────────┘


┌─────────────────────────────┐
│ 调用 esbuild 打包这些依赖    │
│ 输出到 .vite/deps           │
└─────────────────────────────┘


┌─────────────────────────────┐
│ 生成依赖元数据               │
│ .vite/deps/_metadata.json   │
└─────────────────────────────┘
*/

// 手动控制预构建
export default {
  optimizeDeps: {
    // 强制预构建 (解决动态 import 检测不到的问题)
    include: ['esm-dep > cjs-dep'],
    
    // 排除预构建 (某些包需要在运行时处理)
    exclude: ['my-linked-package'],
    
    // 强制重新预构建
    force: true,
    
    // esbuild 选项
    esbuildOptions: {
      plugins: []
    }
  }
}

7.2 模块热更新 (HMR) 原理

javascript
/*
┌─────────────────────────────────────────────────────────────────┐
│                      Vite HMR 架构                               │
├─────────────────────────────────────────────────────────────────┤
│                                                                   │
│   浏览器                              Vite Server                 │
│  ┌────────────┐                     ┌────────────┐               │
│  │            │   WebSocket 连接     │            │               │
│  │  HMR       │◄───────────────────►│   Watcher  │               │
│  │  Client    │                     │  (chokidar)│               │
│  │            │                     │            │               │
│  └────────────┘                     └────────────┘               │
│       │                                   │                       │
│       │  收到更新消息                      │  文件变化             │
│       │  { type: 'update',               │                       │
│       │    updates: [...] }              │                       │
│       ▼                                   │                       │
│  ┌────────────┐                          │                       │
│  │ import()   │                          │                       │
│  │ 重新请求   │◄─────────────────────────┘                       │
│  │ 变化模块   │                                                   │
│  └────────────┘                                                   │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘
*/

// HMR 边界
// Vue/React 组件天然是 HMR 边界
// 普通 JS 模块需要手动处理

// 手动 HMR API
if (import.meta.hot) {
  // 接受自身更新
  import.meta.hot.accept((newModule) => {
    // newModule 是更新后的模块
    // 手动更新状态
  });
  
  // 接受依赖更新
  import.meta.hot.accept('./dep.js', (newDep) => {
    // dep.js 更新时触发
  });
  
  // 清理副作用
  import.meta.hot.dispose((data) => {
    // 模块更新前调用
    clearInterval(timer);
    data.savedValue = someValue;  // 传递给新模块
  });
  
  // 无法热更新时回退到整页刷新
  import.meta.hot.decline();
  
  // 让父模块处理更新
  import.meta.hot.invalidate();
}

7.3 插件系统

javascript
// Vite 插件 = Rollup 插件 + Vite 特有钩子

// Vite 特有钩子
const myVitePlugin = () => ({
  name: 'my-vite-plugin',
  
  // Vite 特有: 配置解析
  config(config, { command, mode }) {
    // 返回的配置会被合并
    return {
      resolve: {
        alias: { '@': '/src' }
      }
    };
  },
  
  // Vite 特有: 配置解析完成
  configResolved(resolvedConfig) {
    // 存储最终配置
  },
  
  // Vite 特有: 配置开发服务器
  configureServer(server) {
    // 添加自定义中间件
    server.middlewares.use((req, res, next) => {
      if (req.url === '/custom') {
        res.end('custom response');
      } else {
        next();
      }
    });
  },
  
  // Vite 特有: 转换 index.html
  transformIndexHtml(html) {
    return html.replace(
      /<title>(.*?)<\/title>/,
      '<title>Modified Title</title>'
    );
  },
  
  // Vite 特有: HMR 更新处理
  handleHotUpdate({ file, server }) {
    // 自定义 HMR 行为
    if (file.endsWith('.custom')) {
      server.ws.send({
        type: 'custom',
        event: 'custom-update',
        data: { file }
      });
      return [];  // 阻止默认 HMR
    }
  },
  
  // Rollup 钩子 (构建时)
  buildStart() {},
  resolveId(source) {},
  load(id) {},
  transform(code, id) {},
  buildEnd() {},
});

// 插件执行顺序控制
export default {
  plugins: [
    {
      ...myPlugin(),
      enforce: 'pre'  // 在核心插件之前
    },
    corePlugin(),     // 默认顺序
    {
      ...postPlugin(),
      enforce: 'post' // 在核心插件之后
    }
  ]
};

八、高频面试题深度解析

Q1: Webpack 和 Vite 的本质区别是什么?

javascript
/*
核心区别: 开发时的模块处理方式

Webpack 模式:
- 启动前: 分析并打包所有模块
- 启动后: 提供打包后的 bundle
- 改变代码: 重新打包受影响的 chunk
- 本质: 传统打包器思维

Vite 模式:
- 启动前: 仅预构建 node_modules
- 启动后: 按需编译被请求的模块
- 改变代码: 仅重新编译该模块
- 本质: 利用浏览器原生 ESM

根本原因:
浏览器直到 2018 年才广泛支持 ESM
Webpack 诞生于 2012 年,必须自己实现模块系统
Vite 诞生于 2020 年,可以依赖浏览器原生能力
*/

Q2: 为什么 esbuild/SWC 比 Babel/Webpack 快那么多?

javascript
/*
1. 语言层面
   - Babel/Webpack: JavaScript (解释执行)
   - esbuild: Go (编译为原生代码)
   - SWC: Rust (编译为原生代码)
   
2. 并行处理
   - Babel: 单线程
   - esbuild: 利用 Go 的 goroutine 多核并行
   - SWC: 利用 Rust 的 rayon 并行库
   
3. 内存效率
   - JS 工具链: 大量 AST 对象创建和 GC
   - 原生工具: 更直接的内存控制
   
4. 设计取舍
   - Babel: 追求极致的可扩展性,大量运行时判断

前端面试知识库