
前言
Vue 3 的源码由多个模块构成,除了我们常用的核心功能外,还包含了响应式、工具函数等多个独立模块。为了模拟 Vue 官方的开发环境,管理这些分散的模块,我们会采用 Monorepo 架构来进行项目管理,并且使用 pnpm workspace。
强烈建议大家一定要跟着动手编码,如果只是看,很容易停留在"知道"的层面。
什么是 Monorepo?
Monorepo 是一种代码管理方式,指将不同的项目放在单一的代码仓库 (repository) 中,对多个不同的项目进行版本控制。
Monorepo 的特点
- 集中式开发:所有项目的代码都集中在同一个 repository 中。
- 工具共享:因为统一管理,所以 CI/CD、代码风格工具等都可以共享,并且只需配置一次。
- 统一版本控制:在 monorepo 中进行 commit,可以横跨多个子项目。
什么是 pnpm workspace?
pnpm workspace 是 pnpm 包管理工具提供的一个功能,核心目标是可以在 repo 内部安装依赖包,并且共享 node_module
,子项目在 repo 中可以互相引用。
pnpm workspace 的特点
- 依赖提升至根目录:节省磁盘空间。
- 模块共享简单 :用
workspace:*
直接引用。 - 集中管理 :一个命令可以管理所有子项目,例如
pnpm install
→ 安装全部项目的依赖包。
环境搭建
- 我们先创建一个文件夹,执行
pnpm init
。 - 新建
pnpm-workspace.yaml
,并且我们要管理packages
下面的子项目。
YAML
packages:
- 'packages/*'
- 在根目录下新建
tsconfig.json
,这是 TypeScript 的配置文件(感谢 GPT 帮忙写的注释):
JSON
{
"compilerOptions": {
// 编译输出 JavaScript 的目标语法版本
// ESNext:始终输出为最新的 ECMAScript 标准
"target": "ESNext",
// 模块系统类型
// ESNext:使用最新的 ES Modules(import / export)
"module": "ESNext",
// 模块解析策略
// "node":模仿 Node.js 的方式来解析模块 (例如 node_modules, index.ts, package.json 中的 "exports")
"moduleResolution": "node",
// 编译后的输出目录
"outDir": "dist",
// 允许直接导入 JSON 文件,编译器会将其视为一个模块
"resolveJsonModule": true,
// 是否启用严格模式
// false:关闭所有严格类型检查(较为宽松)
"strict": false,
// 编译时需要引入的内置 API 定义文件(lib.d.ts)
// "ESNext":最新 ECMAScript API
// "DOM":浏览器环境的 API,例如 document, window
"lib": ["ESNext", "DOM"],
// 自定义路径映射(Path Mapping)
// "@vue/*" 会映射到 "packages/*/src"
// 例如 import { reactive } from "@vue/reactivity"
// 会被解析到 packages/reactivity/src
"paths": {
"@vue/*": ["packages/*/src"]
},
// 基准目录,用于 `paths` 选项的相对路径解析
"baseUrl": "./"
}
}
-
新建
packages
文件夹,里面会加入许多子项目,包含响应式系统等。 -
执行
pnpm i typescript esbuild @types/node -D -w
,其中-w
表示安装到 workspace 的根目录。 -
执行
pnpm i vue -w
,安装 vue,方便之后进行比较。 -
执行
npx tsc --init
,初始化项目中的 TypeScript 配置。 -
在根目录的
package.json
中加入"type": "module"
。- 这会让 Node.js 默认将
.js
文件视为 ES Module (ESM)。 - 若没有此项设置,
.js
文件则会被当作 CommonJS 模块处理。
- 这会让 Node.js 默认将
-
接下来,我们在
package
文件夹下新建三个子项目目录reactivity
、shared
、vue
,以及下列文件:- 响应式模块 reactivity:
reactivity/src/index.ts
、reactivity/package.json
- 工具函数 shared:
shared/src/index.ts
、shared/package.json
- 核心模块 vue:
vue/src/index.ts
、vue/package.json
- 响应式模块 reactivity:
-
为了让我们的子项目拥有和 Vue 官方包类似的配置,我们先将
node_modules/.pnpm/@vue+reactivity/reactivity/package.json
复制一份到reactivity/package.json
,简化后的内容如下:
JSON
{
"name": "@vue/reactivity",
"version": "1.0.0",
"description": "响应式模块",
"main": "dist/reactivity.cjs.js",
"module": "dist/reactivity.esm.js",
"files": [
"index.js",
"dist"
],
"sideEffects": false,
"buildOptions": {
"name": "VueReactivity",
"formats": [
"esm-bundler",
"esm-browser",
"cjs",
"global"
]
}
}
JSON
{
"name": "@vue/shared",
"version": "1.0.0",
"description": "工具函数",
"main": "dist/shared.cjs.js",
"module": "dist/shared.esm.js",
"files": [
"index.js",
"dist"
],
"sideEffects": false,
"buildOptions": {
"name": "VueShared",
"formats": [
"esm-bundler",
"esm-browser",
"cjs",
"global"
]
}
}
JSON
{
"name": "vue",
"version": "1.0.0",
"description": "vue 核心模块",
"main": "dist/vue.cjs.js",
"module": "dist/vue.esm.js",
"files": [
"dist"
],
"sideEffects": false,
"buildOptions": {
"name": "Vue",
"formats": [
"esm-bundler",
"esm-browser",
"cjs",
"global"
]
}
}
-
执行
pnpm i @vue/shared --workspace --filter @vue/reactivity
将工具函数项目作为依赖安装到响应式模块中。 -
接着在根目录下新建一个
scripts/dev.js
:- 在根目录的
package.json
中加入"dev": "node scripts/dev.js --format esm"
命令。 - 开发时,我们将通过执行此脚本来启动编译。它会使用 esbuild 进行实时编译,并在首次编译后持续监听文件变动。
- 在根目录的
JavaScript
// scripts/dev.js
/**
* 用于打包"开发环境"的脚本
*
* 用法示例:
* node scripts/dev.js --format esm
* node scripts/dev.js -f cjs reactive
*
* - 位置参数(第一个)用于指定要打包的子包名称(对应 packages/<name>)
* - --format / -f 指定输出格式:esm | cjs | iife(默认为 esm)
*/
import { parseArgs } from 'node:util'
import { resolve, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
import esbuild from 'esbuild'
import { createRequire } from 'node:module'
/**
* 解析命令行参数
* allowPositionals: 允许使用位置参数(例如 reactive)
* options.format: 支持 --format 或 -f,类型为字符串,默认为 'esm'
*/
const {
values: { format },
positionals,
} = parseArgs({
allowPositionals: true,
options: {
format: {
type: 'string',
short: 'f',
default: 'esm',
},
},
})
/**
* 在 ESM 模式下创建 __filename / __dirname
* - ESM 中没有这两个全局变量,因此需要通过 import.meta.url 进行转换
*/
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
/**
* 在 ESM 中创建一个 require() 函数
* - 用于加载 CJS 风格的资源(例如 JSON)
*/
const require = createRequire(import.meta.url)
/**
* 解析要打包的目标
* - 如果提供了位置参数,则取第一个;否则默认打包 packages/vue
*/
const target = positionals.length ? positionals[0] : 'vue'
/**
* 入口文件(固定指向 packages/<target>/src/index.ts)
*/
const entry = resolve(__dirname, `../packages/${target}/src/index.ts`)
/**
* 决定输出文件路径
* - 命名约定:<target>.<format>.js
* 例:reactive.cjs.js / reactive.esm.js
*/
const outfile = resolve(__dirname, `../packages/${target}/dist/${target}.${format}.js`)
/**
* 读取目标子包的 package.json
* - 常见做法是从中读取 buildOptions.name,作为 IIFE/UMD 的全局变量名
* - 如果 package.json 中没有 buildOptions,请自行调整
*/
const pkg = require(`../packages/${target}/package.json`)
/**
* 创建 esbuild 编译上下文并进入 watch 模式
* - entryPoints: 打包入口
* - outfile: 打包输出文件
* - format: 'esm' | 'cjs' | 'iife'
* - platform: esbuild 的目标平台('node' | 'browser')
* * 这里示范:如果是 cjs,就倾向于 node;否则视为 browser
* - sourcemap: 方便调试
* - bundle: 将依赖打包进去(输出为单文件)
* - globalName: IIFE/UMD 格式下挂载到 window 上的全局名称(esm/cjs 格式下不会用到)
*/
esbuild
.context({
entryPoints: [entry], // 入口文件
outfile, // 输出文件
format, // 输出格式:esm | cjs | iife
platform: format === 'cjs' ? 'node' : 'browser',// 目标平台:node 或 browser
sourcemap: true, // 生成 source map
bundle: true, // 打包成单文件
globalName: pkg.buildOptions?.name, // IIFE/UMD 会用到;esm/cjs 可忽略
})
.then(async (ctx) => {
// 启用 watch:监听文件变更并自动重新构建
await ctx.watch()
console.log(
`[esbuild] watching "${target}" in ${format} mode → ${outfile}`
)
})
.catch((err) => {
console.error('[esbuild] build context error:', err)
process.exit(1)
})
JSON
{
"name": "vue3-source-code",
"version": "1.0.0",
"description": "",
"main": "index.js",
"type": "module",
"scripts": {
"dev": "node scripts/dev.js reactivity --format esm"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@types/node": "^24.2.1",
"esbuild": "^0.25.9",
"typescript": "^5.9.2"
},
"dependencies": {
"vue": "^3.5.18"
}
}
运行测试
- 在
packages/reactivity/src/index.ts
中编写一个导出函数
TypeScript
export function fn(a, b) {
return a + b;
}
- 执行
pnpm dev
,你应该会在packages/reactivity/dist/reactivity.esm.js
中看到以下内容
JavaScript
// packages/reactivity/src/index.ts
function fn(a, b) {
return a + b;
}
export {
fn
};
那就代表环境搭建成功了!
