Appearance
Monorepo
定义
Monorepo 是一种将多个项目代码存储在同一个代码仓库中的开发策略,而不是将每个项目分散到不同的代码仓库中。
主要优势
- 代码共享和复用
- 更容易共享公共代码
- 统一的依赖管理
- 便于抽取公共模块
- 依赖管理
- 统一的版本控制
- 避免依赖冲突
- 简化依赖更新
- 工程化管理
- 统一的构建流程
- 统一的代码规范
- 统一的测试策略
Vue3源码目录结构
packages/
├── compiler-core/ # 编译器核心代码
├── compiler-dom/ # 浏览器平台编译器
├── compiler-sfc/ # 单文件组件编译器
├── compiler-ssr/ # 服务端渲染编译器
├── reactivity/ # 响应式系统
├── runtime-core/ # 运行时核心代码
├── runtime-dom/ # 浏览器运行时
├── runtime-test/ # 测试相关运行时
├── server-renderer/ # 服务端渲染
├── shared/ # 共享工具代码
└── vue/ # 完整版本入口开始搭建项目
1. 创建项目文件夹
bash
mkdir vue3-source-code && cd vue3-source-code2. 安装pnpm
bash
# 安装 pnpm
npm install pnpm -g安装完成后执行:
bash
pnpm -v # 打印出版本号证明安装成功3. 初始化pnpm项目
bash
pnpm init这会生成一个 package.json 文件。
4. 配置 pnpm workspace
在vue3-source-code根目录下创建pnpm-workspace.yaml文件:
yaml
packages:
- 'packages/*' # 所有子包存放在 packages 目录5. 创建一个packages目录,在此目录下创建一个reactivity和vue目录,作为响应式的子包。目录结构为:
vue3-source-code/
├── package.json
├── pnpm-workspace.yaml
├── packages/
│ └── reactivity/
│ └── vue/6. 安装依赖
在 monorepo 中安装依赖和普通项目有所不同,由于每个子包都可以有自己的依赖,所以我们在安装依赖时,需要指定安装到那个包里面,比如我们要安装 typescript
在非 monorepo 项目中:
bash
pnpm install typescript -D但是如果我们在 monorepo 项目中这样安装,会报错,错误如下:
bash
ERR_PNPM_ADDING_TO_ROOT Running this command will add the dependency to the workspace root, which might not be what you want - if you really meant it, make it explicit by running this command again with the -w flag (or --workspace-root). If you don't want to see this warning anymore, you may set the ignore-workspace-root-check setting to true.这个错误信息告诉我们,如果你希望将依赖添加到项目根目录中,需要添加标记 -w 或者 --workspace-root 我们根据提示安装依赖:
bash
pnpm install typescript -D -w注:以上问题,在pnpm的10版本以上,已经不存在,优化了。 不加-w也能下载。pnpm会自动判断,自动寻找。
配置 typescript
json
{
"compilerOptions": {
"target": "ESNext", // 指定 ECMAScript 目标版本
"module": "ESNext", // 指定模块代码生成规范
"moduleResolution": "node", // 指定模块解析策略
"outDir": "dist", // 指定编译输出的目录
"resolveJsonModule": true, // 允许导入 JSON 文件
"strict": false, // 关闭严格模式
"lib": ["ESNext", "DOM"], // 指定要使用的库文件
"paths": {
"@vue/*": ["packages/*/src"]
},
"baseUrl": "./"
}
}配置 esbuild
在根目录下创建scripts/dev.js:
js
import { parseArgs } from 'node:util'
import { resolve, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
import { createRequire } from 'node:module'
import esbuild from 'esbuild'
// 打包开发环境
/**
* 解析命令行参数
*/
const {
values: { format },
positionals,
} = parseArgs({
allowPositionals: true, // 位置参数
options: {
format: {
type: 'string',
short: 'f',
default: 'esm',
description: '打包格式',
},
},
})
console.log('开始打包', format, positionals)
// 创建esm的filename
const __filename = fileURLToPath(import.meta.url)
// 创建esm的__dirname
const __dirname = dirname(__filename)
// 创建require
const require = createRequire(import.meta.url)
const target = positionals.length ? positionals[0] : 'vue'
const entry = resolve(__dirname, `../packages/${target}/src/index.ts`)
console.log('打包入口文件', entry)
/**
* -format cjs or esm
* cjs => reactive.cjs.js
* esm => reactive.esm.js
*/
const outfile = resolve(
__dirname,
`../packages/${target}/dist/${target}.${format}.js`,
)
const pkg = require(`../packages/${target}/package.json`)
console.log('pkg', pkg)
esbuild
.context({
entryPoints: [entry], // 入口文件
outfile, // 输出文件
format, // 输出格式, cjs esm iife
platform: format === 'cjs' ? 'node' : 'browser', // 平台
sourcemap: true, // 开启sourcemap
bundle: true, // 把所有的依赖打包到一个文件中
globalName: pkg.buildOptions.name, // 全局变量名
})
.then(ctx => {
ctx.watch()
})在package.json中,新增脚本命令:
json
{
"name": "vue3-source-code",
"version": "1.0.0",
"description": "",
"type": "module",
"main": "index.js",
"scripts": {
"dev": "node scripts/dev.js reactivity --format esm",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"packageManager": "pnpm@10.12.4",
"devDependencies": {
"@types/node": "^24.0.10",
"esbuild": "^0.25.6",
"prettier": "^3.6.2",
"typescript": "^5.8.3",
"vue": "^3.5.17"
}
}