pnpm dev 的背后(vite构建)
pnpm dev 的背后(vite构建)
Breezlipnpm dev (执行启动命令)
执行 pnpm dev 时,pnpm 包管理器会查找 package.json 中 scripts 下的 dev 命令并执行它(通常是 vite / vite serve)
1 | "scripts": { |
这实际上调用了 Vite 的二进制文件
node_modules/.bin/vite(这个会引连到 Vite 的 CLI 模块vite/src/node/cli.ts)接着
CLI 模块使用
[cac](https://github.com/cacjs/cac)库来解析命令行参数(如--port、--open)并根据命令(
serve,build,preview)
1
2
3
4
5 "scripts": {
"dev": "vite",
"build": "vue-tsc -b && vite build",
"preview": "vite preview"
},创建相应的 Vite 开发服务器或执行构建
确定 vite 启动时模式(mode)
Vite 在启动时,会首先确定当前的运行模式,默认为 development(开发)
你也可以通过命令行手动指定模式
1 | vite --mode production |
运行模式决定了 Vite 加载哪些 .env 环境变量文件
加载.env文件
随后,按照以下顺序查找和加载环境变量文件
.env.${mode}.local(如.env.development.local)(最高优先级,且不会提交到 git [.gitignore]).env.${mode}(如.env.d).env.local.env(全局默认配置)
root / env.d.ts
1 | declare const PROD: boolean; |
所有变量都会被注入到 Node.js 的 process.env 中**(process 是 Node.js 的全局对象,所以仅在服务端生效)
插播:Node.js 的 process.env 是个啥
就是一个普通的 JS 对象,而
env它是process的一个属性,专门存环境变量它大概是这样的👇
1 console.log(process.env);
1
2
3
4
5
6
7 {
NODE_ENV: 'development',
PORT: '3000',
DATABASE_URL: 'postgresql://localhost:5432/mydb',
VITE_API_URL: 'https://dev-api.example.com',
SECRET_KEY: 'abcdefg123456'
}你可以把它看成 NodeJS 的备忘录,存着环境,端口,接口,密钥等数据
好了现在回归正题
比如我在某文件需要知道这些参数来判断当前的运行环境👇(我在build脚本文件用的最多)
1 | const isProd = process.env.NODE_ENV === 'production' // 生产环境 |
1 | plugins: [ |
但是!这些变量不能暴露给浏览器,不然任何访问你网站的人F12就能看到数据库密码、API 秘钥等……
过滤出 VITE_ 开头的变量
只有以
VITE_为前缀的变量才会通过import.meta.env暴露给你的客户端源码 (看上面的例子,比如是VITE_API_URL)(Vite 内部会对此做筛选,类似于以下这样)
1
2
3
4
5
6 const clientEnv = {}
for (const [key, value] of Object.entries(process.env)) {
if (key.startsWith('VITE_')) {
clientEnv[key] = value // 只保留 VITE_ 开头的
}
}然后,Vite 把这个
clientEnv对象注入到前端代码中,变成
1
2 import.meta.env.VITE_API_URL // 可访问
import.meta.env.DB_PASSWORD // undefined
注入import.meta.env到客户端
import.meta.env就是由process.env筛出来的,所以和process.env一样,import.meta.env作为客户端的备忘录
| 对比 | process.env |
import.meta.env |
| 运行环境 | Node.js(服务端) | 浏览器(客户端) |
| 使用位置 | Vite 服务器、构建脚本 | Vue/React 组件 |
| 值定义在 | .env 文件 + 操作系统 |
Vite 筛选并注入 |
| 变量范围 | 所有变量 | 只能访问 VITE_ 开头的 |
配置解析与合并
回归主线,处理完环境变量,下一步就是合并配置
Vite 会合并&处理以下的配置,生成最终的构建配置对象
命令行(CLI)
项目设置的命令行参数(优先级最高)
例如 vite --port 5000 --open --host,Vite 会把这些建议解析成一个对象:
1 | const cliOptions = { |
defineConfig
Vite 读取并执行 vite.config.ts,获取导出的 defineConfig
1 | export default defineConfig({ |
结果也就是
1 | const configFromFile = { |
合并
Vite 会把两者合并,CLI 优先级更高
1 | const mergedConfig = { |
config 钩子
在最终配置确定前,插件可以通过插件的 config 钩子修改配置(和 axios 的二次封装是一个道理,会合并你的配置)
1 | // 自定义插件 |
配置结果是
1 | const afterPluginConfig = { |
并且这个最终的配置对象会被冻结(frozen),防止后续被修改
configResolved 钩子
在配置确定后,插件可以根据插件的 configResolved 钩子读取存储最终解析的配置(只读)
1 | const myPlugin = { |
插件系统初始化与构建
Vite 的功能几乎都由插件提供(包括内置插件)
而这些插件就会涉及到排序先行的问题,vite内部规定了什么插件应该优先执行解析
插件排序
排序逻辑
插件会根据 enforce 属性排序执行
enforce: 'pre':在 Vite 核心插件之前调用【代码转换插件,别名处理插件 …】1
2
3
4
5
6
7import react from '@vitejs/plugin-react' // 作用:解析别名、代码转换、重写导入路径
export default defineConfig({
plugins: [
react() // 内部 enforce: 'pre'
]
})>
.jsx文件需要先转成.js文件 Vite 核心才能识别,react要确保在.jsx文件被解析前先处理 JSX 语法无
enforce**(默认):在 Vite 核心插件之后调用【大多数插件 …】1
2
3
4
5
6
7import vue from '@vitejs/plugin-vue' // 作用:把<template>编译成render函数、提取<script>、注入HMR代码
export default defineConfig({
plugins: [
vue() // 默认顺序,无 enforce
]
})> 因为它处理的是已经被分好的单
.vue文件子请求,必须在核心插件之后enforce: 'post':在 Vite 构建插件之后调用【打包后处理插件,生成报告插件 …】1
2
3
4
5
6
7
8
9
10import { visualizer } from 'rollup-plugin-visualizer' // 作用:打包后处理(压缩、混淆等)、生成报告
export default defineConfig({
plugins: [
vue(),
visualizer({
open: true
}) // 默认 enforce: 'post'
]
})>
visualizer插件需要等所有代码打包完成后,才能分析最终的 bundle 大小
这一块由于加了细节写的有点乱,可以先看看下面的 总结
指定模式
插件还可以通过 apply 指定只在特定模式下生效
1 | apply: 'serve' // 只在开发模式(dev)启用 |
1 | const mockPlugin = { |
1 | apply: 'build' // 只在构建模式启用 |
1 | const compressPlugin = { |
总结
| 类型 | enforce |
典型用途 | 真实例子 |
| 预处理 | 'pre' |
路径解析、语法转换 | @vitejs/plugin-react |
| 普通处理 | 默认 | 组件编译、资源处理 | @vitejs/plugin-vue |
| 后处理 | 'post' |
打包分析、压缩 | rollup-plugin-visualizer |
| 模式控制 | apply: 'serve' |
开发专用功能 | Mock 插件 |
| 模式控制 | apply: 'build' |
构建优化 | Gzip 压缩插件 |
依赖预构建
Vite 的开发服务器基于 ESModule,实现了真正的按需编译,但很多 npm 包还是 CommonJS(CJS)格式
为了解决兼容性和性能问题,vite 会进行依赖预构建,主要是为了优化👇
兼容性:将 CommonJS 或 UMD 格式的依赖项转换为 ESM 格式,以便在浏览器中通过
<script type="module">正常加载性能:将有许多内部模块的 ESM 依赖项(如 lodash-es)转换为单个模块,减少浏览器需要发出的 HTTP 请求数
预构建后的依赖会被缓存到 node_modules/.vite 目录下
这是 vite 启动快的一个原因,它只处理你自己写的代码,依赖项已经“预打包”好了
建立开发服务器
Vite 使用 Connect 作为底层框架,创建一个开发服务器
启用功能:
监听指定端口(如
localhost:3000)处理所有静态资源请求(HTML、JS、CSS、图片等)
启动 WebSocket,用于 HMR(热更新)
1
2
3
4
5// 伪代码
const server = connect()
server.use(serveStatic())
server.use(handleModuleRequest) // 处理 .vue, .ts 等
createWebSocketServer() // 用于 HMR
注入中间件
什么是中间件
中间件就是在
浏览器向vite请求文件过程中的 ”中间检查站“一个中间件就是一个函数,类似于这种👇
1
2
3
4
5
6
7
8 function myMiddleware(req, res, next) { // 请求对象,相应对象,放行函数
if (req.url === '/hello') { // 不调用 next(),相当于直接拦截
res.writeHead(200, { 'Content-Type': 'text/plain' })
res.end('Hello from middleware!')
} else {
next() // 放行,继续后续处理
}
}
Vite 的开发服务器底层基于 Connect(一个轻量级 Node.js 框架),而 Connect 的核心就是中间件机制
它的结构就像这样
1 | 浏览器请求 |
Vite 的操作
Vite 的规则是 先注册的插件先执行
1 | // vite.config.ts |
插件可以通过 configureServer 钩子**(插件开发者已定义好)向 Vite 的服务器注册自己的中间件
比如 mock 插件
效果:当你在浏览器访问
/api/users,根本不会去请求后端,而是直接返回 mock 数据
1 | const mockPlugin = { |
⚠️ 如果你在中间件里
return了响应(比如 mock 数据),后面的中间件(包括 Vite 核心)就不会执行
请求处理(核心)
当浏览器请求一个文件(如 App.vue)时,Vite 不是简单地返回文件内容,而是通过 插件系统动态转换,最终返回一段可执行的 JS 模块。而这个过程会产生对子资源的额外请求,这是理解 Vite 处理 .vue 文件的关键
请求拦截
浏览器发起:****GET /src/App.vue
Vite 的开发服务器拦截该请求,不直接读取文件返回,而是 开启模块解析和转换流程
插件流水线执行(核心)
Vite 通过 Rollup 风格的插件接口,依次调用注册的插件钩子,顺序如下:
resolveId(specifier)
解析模块依赖关系时(如 import './App.vue')
调用插件的 resolveId 钩子,确定 './App.vue' 对应的真实路径(如 /project/src/App.vue)
1 | // Vite 内部调用 |
load(id)
模块路径解析后
需要读取原始内容,调用插件的 load 钩子,获取文件原始字符串
1 | // Vite 内部调用 |
transform(code, id)
当对加载的原始内容进行转换时
将 code 和 id 传给所有插件的 transform 钩子,按顺序执行转换
Vite不自己转换
.vue文件,这里是把code交给@vitejs/plugin-vue插件处理注:
@vitejs/plugin-vue在transform阶段会自动注入 HMR 支持
1 | // Vite 内部调用 |
当
@vitejs/plugin-vue插件发现id是.vue文件时,就会介入处理
处理子资源请求
vue文件内部还有分块,也就是template,script,style
浏览器继续发起请求:****GET /src/App.vue?type=template
Vite 再次拦截这个请求,并重复之前的流程
resolveId(specifier)
插件(@vitejs/plugin-vue)识别到 ?type=template,将其标记为一个虚拟模块,返回原样或特殊标识load(id)
load(id)
可能返回空字符串,或不调用(某些插件直接在 transform 处理)
transform(code, id)
当
@vitejs/plugin-vue插件发现特殊标识id,就知道这是模板请求会使用
@vue/compiler-dom将模板编译为render函数(这是vue插件的功能)
1 | // 返回给浏览器的内容 |
流程总结
1 | 浏览器 |
热模块更新
Vite 最大的特点就是拥有极快的热更新(HMR),Vite的热更新本质是 在运行时动态替换模块代码
更新主要包含以下过程
服务器感知
1 | const watcher = chokidar.watch(root, { ... }) // Vite 使用 chokidar 监听文件 |
WebSocket 更新
Vite 服务器
1 | function updateModules(file, modules) { |
浏览器端的
vite/client会监听这个消息
浏览器(客户端)接收
vite/client 注入的代码
1 | ws.onmessage = (event) => { |
热替换(局部更新)
这是通过框架(Vue/React)的渲染机制实现的
如果接触过框架源码就可以理解,就是框架内部根据DOM差异实现热替换的diff算法
所以,Vite 只负责送新模块,而局部更新的实现是源于框架渲染机制
总结
| 详情 | |
| pnpm dev | 查找 package.json 中 scripts 下的 dev 命令并执行 |
| 执行 vite 命令 | 调用 Vite 的二进制文件 node_modules/.bin/vite(引连到 Vite 的 CLI 模块 vite/src/node/cli.ts) |
| 确定 mode | 按顺序查找和加载 .env,注入变量到 process.env,过滤变量到 import.meta.env, |
| 合并插件配置 | 合并 CLI指令 & vite.config.ts配置,通过config,configResolved 自定义插件 |
| 初始化插件系统 | 按照 pre、normal、post 排序插件(可通过 apply: 'serve'/'build' 指定模式 ) |
| 启动服务器 | 创建开发服务器(Koa/Connect 实例) |
| 注册中间件 | 根据 vite.config.ts/plugin 的顺序,调用所有插件的 configureServer 钩子 |
| 浏览器请求文件 | 按需 transform(.vue/.ts → JS) |
| 热更新 | 向浏览器推送新模块,并利用框架机制热替换DOM |




