详细解析Vite的ESM请求处理流程

262 阅读2分钟

一、项目结构示例

my-project/
├─ node_modules/
├─ src/
│  ├─ main.js        # 入口文件
│  ├─ App.vue        # 根组件
│  ├─ utils/
│  │  └─ math.js     # 工具模块
├─ index.html        # 基础HTML

二、完整请求处理流程

  1. 浏览器请求ESM
    当访问http://localhost:3000时,浏览器加载index.html并解析到:

image.png

```
<script type="module" src="/src/main.js"></script>
```

触发对`main.js`的ESM请求
  1. Vite拦截请求
    Vite服务器拦截到/src/main.js请求,检查发现:

    • 该文件未被预构建(非node_modules内容)
    • 未在内存缓存中找到编译结果
      进入实时编译流程

预构建(Pre-Bundling)

-   仅针对`node_modules`中的第三方依赖
-   将CommonJS/UMD模块转换为ESM格式
-   产物存储在`.vite/deps`目录并建立依赖图谱

实时编译(On-Demand Compilation)

-   处理项目源码(如`/src/`下的文件)
-   按需进行TS转译、Vue SFC编译等操作

  1. 实时编译处理
    Vite对main.js进行以下处理:

    // 原始main.js
    import { createApp } from 'vue'
    import App from './App.vue'
    
    createApp(App).mount('#app')
    
    • 'vue'重写为预构建路径/node_modules/.vite/vue.js

    • App.vue转换为可执行的ESM格式(拆分为script/style/template)

  2. 返回编译结果
    生成带HMR支持的编译后代码:

    import { createApp } from '/node_modules/.vite/vue.js'
    import App from '/src/App.vue?import'
    
    createApp(App).mount('#app')
    
    // 注入HMR客户端代码
    if (import.meta.hot) {
      import.meta.hot.accept()
    }
    

    同时将该结果存入内存缓存

    实际编译效果如下:

    image.png

  3. 后续请求处理
    当浏览器请求App.vue时:

    • 若开发者未修改文件,直接返回缓存结果
    • 若文件已修改,重新编译并更新缓存

三、关键场景示例

场景1:修改工具函数

  1. 修改src/utils/math.js

    - export const add = (a, b) => a + b
    + export const add = (a, b) => a + b + 1
    
  2. Vite检测到文件变更:

    • 使旧缓存失效
    • 仅重新编译math.js及其依赖链
    • 通过HMR通知浏览器更新模块

场景2:添加新依赖

  1. 新增src/composables/useCounter.js

    export default () => {
      const count = ref(0)
      const inc = () => count.value++
      return { count, inc }
    }
    
  2. 首次导入时触发实时编译,后续请求直接读缓存


四、缓存策略对比

文件类型缓存策略示例
预构建依赖强缓存(max-age=1年)/node_modules/.vite/
业务代码协商缓存(304)src/components/*.vue
静态资源根据配置决定public/logo.png

通过这种机制,Vite实现了开发环境下的秒级启动和亚秒级热更新。生产构建时则切换为Rollup的完整打包流程以保证最佳性能。