在vite根目录执行pnpm run dev
根目录的指令为
"scripts": {
"dev": "pnpm -r --parallel --filter='./packages/*' run dev",
},
-r或--recursive:递归执行命令--parallel:并行执行命令--filter='./packages/*':只对 packages 目录下的所有包执行命令
递归执行命令(-r 或 --recursive)是指在 pnpm 工作空间中,命令会从当前目录开始,递归地应用到所有子包。
与 --filter 的区别
-r会递归执行所有包--filter='./packages/*'只执行匹配的包- 两者结合使用可以更精确地控制执行范围
整个意思就是 并行的运行,整个package下面包的dev指令 package文件夹下面总共有三个包,依次看一下都发生了什么
- create-vite
- plugin-legacy
- vite
create-vite: pnpm run dev
packages/create-vite
"scripts": {
"dev": "unbuild --stub",
},
"devDependencies": {
"unbuild": "^3.5.0"
}
这里用到了第三方包:unbuild 用作 create-vite 本地打包工具(其实有点像vite了,esbuild、ts、Rollup杂交出来的)
unbuild的github地址: github.com/unjs/unbuil…
--stub 表示不发生实际的编译,开发过程中比较好用
unbuild会有一个单独的文章介绍使用。这里只要知道是打包用的就可以了。
plugin-legacy:pnpm run dev
packages/plugin-legacy
"scripts": {
"dev": "unbuild --stub",
},
"devDependencies": {
"unbuild": "3.4.2",
}
也是使用 unbuild 在本地调试,暂时先略过,重点看一下下面 vite主包的dev执行
vite
packages/vite
"scripts": {
"dev": "tsx scripts/dev.ts",
},
"peerDependencies": {
"tsx": "^4.8.1",
},
"peerDependenciesMeta": {
"tsx": {
"optional": true
},
}
执行dev指令,会使用tsx 运行 dev.ts文件。
import {
mkdirSync,
readFileSync,
readdirSync,
rmSync,
writeFileSync,
} from 'node:fs'
import { type BuildOptions, context } from 'esbuild'
import packageJSON from '../package.json'
rmSync('dist', { force: true, recursive: true })
mkdirSync('dist/node', { recursive: true })
writeFileSync('dist/node/index.d.ts', "export * from '../../src/node/index.ts'")
writeFileSync(
'dist/node/module-runner.d.ts',
"export * from '../../src/module-runner/index.ts'",
)
/*
首先强制删除 dist 目录及其所有内容
然后创建新的 dist/node 目录结构
index.d.ts: 导出 src/node/index.ts 中的所有内容
module-runner.d.ts: 导出 src/module-runner/index.ts 中的所有内容
*/
- 确保构建环境的清洁(通过删除旧的构建文件)
- 创建必要的目录结构
- 生成 TypeScript 类型声明文件,这对于开发时的类型检查和 IDE 支持很重要
典型的开发环境构建脚本,它确保了在开发过程中能够正确地处理 TypeScript 类型定义,使得开发体验更加流畅。
const serverOptions: BuildOptions = {
bundle: true,
platform: 'node',
target: 'node18',
sourcemap: true,
external: [
...Object.keys(packageJSON.dependencies),
...Object.keys(packageJSON.peerDependencies),
...Object.keys(packageJSON.optionalDependencies),
...Object.keys(packageJSON.devDependencies),
],
}
/*
bundle: true
将所有代码和依赖打包成一个文件
这样可以减少文件数量,提高加载效率
platform: 'node'
指定代码运行环境为 Node.js
这会影响 esbuild 如何处理某些特定的 API 和模块
target: 'node18'
指定目标 Node.js 版本为 18
这确保生成的代码与 Node.js 18 兼容
esbuild 会根据这个版本进行相应的代码转换
sourcemap: true
生成 sourcemap 文件
这对于调试非常重要,可以将编译后的代码映射回源代码
在开发过程中特别有用
external 配置
这个配置指定哪些依赖应该被视为外部依赖
外部依赖不会被打包到最终的构建文件中
这里包含了 package.json 中所有类型的依赖:
dependencies: 生产环境必需的依赖
peerDependencies: 对等依赖,需要宿主环境提供的依赖
optionalDependencies: 可选依赖,即使安装失败也不会影响主要功能
devDependencies: 开发环境需要的依赖
*/
- 确保代码能够正确运行在 Node.js 环境中
- 优化构建过程,只打包必要的代码
- 保持外部依赖的独立性
- 提供良好的开发调试体验
const clientOptions: BuildOptions = {
bundle: true,
platform: 'browser',
target: 'es2020',
format: 'esm',
sourcemap: true,
}
/*
bundle: true
与服务器端配置相同,将所有代码打包成一个文件
减少 HTTP 请求数量,提高加载性能
platform: 'browser'
指定代码运行环境为浏览器
这会影响 esbuild 如何处理浏览器特定的 API
与服务器端的 platform: 'node' 形成对比
target: 'es2020'
指定目标 ECMAScript 版本为 2020
这意味着生成的代码会使用 ES2020 的特性
比服务器端的 node18 更现代,因为现代浏览器支持更多新特性
format: 'esm'
指定输出格式为 ES 模块(ECMAScript Modules)
使用 import/export 语法
这是现代浏览器原生支持的模块系统
支持静态分析,有利于 tree-shaking
sourcemap: true
生成 sourcemap 文件
方便在浏览器中调试源代码
对于开发环境特别重要
*/
- 确保代码能在现代浏览器中运行
- 使用现代的模块系统
- 提供良好的开发调试体验
- 优化了代码的加载性能
const watch = async (options: BuildOptions) => {
const ctx = await context(options)
await ctx.watch()
}
/*
const ctx = await context(options)
使用 esbuild 的 context API 创建一个构建上下文
这个上下文包含了构建配置和构建状态
使用 await 等待上下文创建完成
await ctx.watch()
启动文件监听模式
当源文件发生变化时,会自动重新构建
使用 await 等待监听启动完成
*/
- 在开发环境中实现热重载(Hot Reload)
- 当源代码文件发生变化时,自动触发重新构建
- 提高开发效率,不需要手动重新构建
// envConfig
void watch({
entryPoints: ['src/client/env.ts'],
outfile: 'dist/client/env.mjs',
...clientOptions,
})
/*
void watch()
调用之前定义的 watch 函数
使用 void 操作符忽略 Promise 返回值
因为这是一个开发环境的监听任务,不需要等待其完成
entryPoints: ['src/client/env.ts']
指定构建的入口文件
只构建一个文件:src/client/env.ts
outfile: 'dist/client/env.mjs'
指定构建后的输出文件路径
使用 .mjs 扩展名表示这是一个 ES 模块文件
输出到 dist/client 目录下
...clientOptions
展开之前定义的客户端构建选项
*/
- 构建客户端环境配置文件
- 将环境配置打包成浏览器可用的 ES 模块
- 在开发过程中监听文件变化并自动重新构建
// clientConfig
void watch({
entryPoints: ['src/client/client.ts'],
outfile: 'dist/client/client.mjs',
external: ['@vite/env'],
...clientOptions,
})
/*
external: ['@vite/env']
指定外部依赖
这里特别将 @vite/env 标记为外部依赖
这个模块不会被打包到最终文件中
*/
// nodeConfig
void watch({
...serverOptions,
entryPoints: {
cli: 'src/node/cli.ts',
constants: 'src/node/constants.ts',
index: 'src/node/index.ts',
},
outdir: 'dist/node',
format: 'esm',
splitting: true,
chunkNames: '_[name]-[hash]',
// The current usage of require() inside inlined workers confuse esbuild,
// and generate top level __require which are then undefined in the worker
// at runtime. To workaround, we move require call to ___require and then
// back to require on build end.
// Ideally we should move workers to ESM
define: { require: '___require' },
plugins: [
{
name: 'log',
setup(build) {
let first = true
build.onEnd(() => {
for (const file of readdirSync('dist/node')) {
const path = `dist/node/${file}`
const content = readFileSync(path, 'utf-8')
if (content.includes('___require')) {
writeFileSync(path, content.replaceAll('___require', 'require'))
}
}
if (first) {
first = false
console.log('Watching...')
} else {
console.log('Rebuilt')
}
})
},
},
],
})
/*
入口文件配置
entryPoints: {
cli: 'src/node/cli.ts', // 命令行工具入口
constants: 'src/node/constants.ts', // 常量定义
index: 'src/node/index.ts', // 主入口文件
}
指定了三个入口文件• 每个入口文件会生成对应的输出文件
输出配置
outdir: 'dist/node', // 输出目录
format: 'esm', // ES 模块格式
splitting: true, // 启用代码分割
chunkNames: '_[name]-[hash]' // 分块文件命名
所有文件输出到 dist/node 目录
使用 ES 模块格式
启用代码分割以优化加载性能
分块文件使用 _[name]-[hash] 格式命名
define: { require: '___require' }
这是一个特殊处理,用于解决内联 worker 中的 require 问题
将代码中的 require 临时替换为 ___require
在构建结束后再替换回来
自定义插件
plugins: [{
name: 'log',
在每次构建结束时执行
处理所有输出文件中的 ___require 替换
输出构建状态信息
*/
// moduleRunnerConfig
void watch({
...serverOptions,
entryPoints: ['./src/module-runner/index.ts'],
outfile: 'dist/node/module-runner.js',
format: 'esm',
})
// cjsConfig
void watch({
...serverOptions, // 展开服务器端构建选项
entryPoints: ['./src/node/publicUtils.ts'], // 入口文件
outfile: 'dist/node-cjs/publicUtils.cjs', // 输出文件
format: 'cjs', // 输出格式为 CommonJS
banner: { // 文件头部注入的代码
js: `
const { pathToFileURL } = require("node:url")
const __url = pathToFileURL(__filename)`.trimStart(),
},
define: { // 定义替换
'import.meta.url': '__url', // 将 import.meta.url 替换为 __url
},
})
/*
entryPoints: ['./src/node/publicUtils.ts'], // 入口文件
outfile: 'dist/node-cjs/publicUtils.cjs', // 输出文件
format: 'cjs', // CommonJS 格式
指定入口文件为 publicUtils.ts
输出到 dist/node-cjs 目录
使用 CommonJS 模块格式(.cjs 扩展名)
文件头部注入
banner: {
js: `
const { pathToFileURL } = require("node:url")
const __url = pathToFileURL(__filename)`.trimStart(),
}
*/
- ESM 到 CommonJS 的转换
- import.meta.url 的兼容性处理
- 文件路径的正确解析
总结一下
在vite的根目录下面,执行pnpm run dev,会并行的执行 packages/create-vite packages/plugin-legacy packages/vite 三个包下面的dev指令
create-vite包是用来
- 创建新项目
- 提供项目模板
- 初始化项目配置
- 安装依赖
plugin-legacy包是用来
- 为旧浏览器提供兼容性支持
- 生成传统浏览器可用的代码
- 自动添加 polyfills
- 处理现代 JavaScript 特性
packages/vite是Vite 的核心包
- 开发服务器(Dev Server)
- 构建工具(Build Tool)
- 插件系统(Plugin System)
- 模块解析(Module Resolution)
- 热更新(HMR)
- 依赖预构建(Dependency Pre-bundling)
这三个包的关系:
- vite 是核心包,提供基础功能
- create-vite 使用 vite 创建新项目
- plugin-legacy 扩展 vite 的功能,提供兼容性支持
执行dev命令后,会在三个包目录下面各自生成对应的dist文件夹