本文是《从零到一:构建现代化企业级 Monorepo 项目实战》系列的第三篇。前两篇我们分析了 Monorepo 的概念和工具选型,这篇文章将手把手带你从零搭建一个完整的项目。
🎯 本文目标
读完这篇文章,你将学会:
- 如何初始化一个 pnpm + Turborepo 项目
- 如何设计合理的包结构
- 如何配置依赖关系
- 如何实现包之间的相互引用
🏗️ 项目架构设计
最终目标
bash
gdu-common/
├── packages/
│ ├── shared/ # 🔧 共享工具库(基础层)
│ ├── utils/ # 📦 工具函数库(基础层)
│ ├── ui/ # 🎨 UI 组件库(依赖 shared + utils)
│ └── controls-sdk/ # 🎮 飞控 SDK(相对独立)
├── docs/ # 📚 文档站点
├── build/ # ⚙️ 构建配置
├── turbo.json # Turborepo 配置
├── pnpm-workspace.yaml # pnpm workspace 配置
└── package.json # 根配置
依赖关系图
graph TD
A[shared 共享库] --> C[ui 组件库]
B[utils 工具库] --> C
D[controls-sdk] -.独立.-> D
E[docs 文档] -.使用.-> C
E -.使用.-> B
E -.使用.-> D
📝 第一步:项目初始化
1.1 创建项目目录
bash
mkdir gdu-common
cd gdu-common
# 初始化 git
git init
# 创建 .gitignore
cat > .gitignore << EOF
node_modules
dist
.turbo
*.log
.DS_Store
.env*
coverage
EOF
1.2 初始化 package.json
bash
pnpm init
编辑 package.json
:
json
{
"name": "gdu-common",
"version": "1.0.0",
"private": true,
"type": "module",
"packageManager": "pnpm@10.13.1",
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev",
"lint": "turbo run lint",
"clean": "turbo run clean"
}
}
1.3 配置 pnpm workspace
创建 pnpm-workspace.yaml
:
yaml
packages:
# packages 目录下的所有包
- packages/*
# 文档站点
- docs
# 构建配置
- build
1.4 安装 Turborepo
bash
pnpm add -Dw turbo
1.5 创建 turbo.json
json
{
"$schema": "https://turbo.build/schema.json",
"ui": "tui",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"],
"cache": true
},
"dev": {
"cache": false,
"persistent": true
},
"lint": {
"cache": true
},
"clean": {
"cache": false
}
}
}
📦 第二步:创建第一个包(shared)
2.1 创建包目录
bash
mkdir -p packages/shared/src
cd packages/shared
2.2 初始化包
bash
pnpm init
编辑 packages/shared/package.json
:
json
{
"name": "@gdu-common/shared",
"version": "1.0.0",
"type": "module",
"main": "./dist/umd/gdushared.umd.js",
"module": "./dist/es/index.mjs",
"types": "./dist/es/index.d.ts",
"exports": {
".": {
"types": "./dist/es/index.d.ts",
"import": "./dist/es/index.mjs",
"require": "./dist/umd/gdushared.umd.js"
}
},
"files": ["dist"],
"scripts": {
"build": "vite build",
"dev": "vite build --watch",
"clean": "rimraf dist .turbo"
}
}
2.3 创建源代码
typescript
// packages/shared/src/index.ts
export const APP_NAME = 'GDU Common'
export const APP_VERSION = '1.0.0'
// packages/shared/src/common/index.ts
export const API_BASE_URL = 'https://api.example.com'
// packages/shared/src/common/test.ts
export function testShared() {
console.log('Shared module works!')
}
2.4 配置 Vite
typescript
// packages/shared/vite.config.ts
import { defineConfig } from 'vite'
import dts from 'vite-plugin-dts'
export default defineConfig({
plugins: [
dts({
outDir: 'dist/es',
entryRoot: 'src',
}),
],
build: {
lib: {
entry: './src/index.ts',
name: 'GduShared',
formats: ['es', 'umd'],
},
rollupOptions: {
output: [
{
format: 'es',
dir: 'dist/es',
entryFileNames: '[name].mjs',
preserveModules: true,
preserveModulesRoot: 'src',
},
{
format: 'umd',
dir: 'dist/umd',
entryFileNames: 'gdushared.umd.js',
name: 'GduShared',
},
],
},
},
})
2.5 测试构建
bash
cd packages/shared
pnpm build
# 查看生成的文件
ls dist/es
# index.mjs index.d.ts common/
ls dist/umd
# gdushared.umd.js
🔧 第三步:创建工具包(utils)
3.1 创建包结构
bash
mkdir -p packages/utils/src
cd packages/utils
pnpm init
3.2 配置 package.json
json
{
"name": "@gdu-common/utils",
"version": "1.0.0",
"type": "module",
"main": "./dist/umd/gduutils.umd.js",
"module": "./dist/es/index.mjs",
"types": "./dist/es/index.d.ts",
"exports": {
".": {
"types": "./dist/es/index.d.ts",
"import": "./dist/es/index.mjs",
"require": "./dist/umd/gduutils.umd.js"
}
},
"scripts": {
"build": "vite build",
"dev": "vite build --watch",
"clean": "rimraf dist .turbo"
},
"peerDependencies": {
"lodash-es": "^4.17.21"
}
}
3.3 创建工具函数
typescript
// packages/utils/src/utils.ts
export function formatDate(date: Date): string {
return date.toISOString().split('T')[0]
}
// packages/utils/src/test.ts
import { debounce } from 'lodash-es'
export function hello(name: string) {
console.log(`Hello, ${name}!`)
}
export const debouncedHello = debounce(hello, 300)
// packages/utils/src/index.ts
export * from './utils'
export * from './test'
3.4 配置 Vite
typescript
// packages/utils/vite.config.ts
import { defineConfig } from 'vite'
import dts from 'vite-plugin-dts'
export default defineConfig({
plugins: [
dts({
outDir: 'dist/es',
entryRoot: 'src',
}),
],
build: {
lib: {
entry: './src/index.ts',
name: 'GduUtils',
formats: ['es', 'umd'],
},
rollupOptions: {
external: ['lodash-es'],
output: [
{
format: 'es',
dir: 'dist/es',
entryFileNames: '[name].mjs',
preserveModules: true,
preserveModulesRoot: 'src',
},
{
format: 'umd',
dir: 'dist/umd',
entryFileNames: 'gduutils.umd.js',
name: 'GduUtils',
globals: {
'lodash-es': 'LodashEs',
},
},
],
},
},
})
🎨 第四步:创建 UI 组件库
4.1 创建包结构
bash
mkdir -p packages/ui/src/components/Button
cd packages/ui
pnpm init
4.2 安装依赖
bash
# 添加 Vue 作为 peer 依赖
pnpm add -D vue
# 添加内部依赖
pnpm add @gdu-common/shared@workspace:* @gdu-common/utils@workspace:*
package.json:
json
{
"name": "@gdu-common/ui",
"version": "1.0.0",
"type": "module",
"main": "./dist/index.umd.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.umd.js"
},
"./dist/index.css": "./dist/index.css"
},
"scripts": {
"build": "vite build",
"dev": "vite build --watch",
"clean": "rimraf dist .turbo"
},
"peerDependencies": {
"vue": "^3.5.0",
"@gdu-common/shared": "workspace:^",
"@gdu-common/utils": "workspace:^"
}
}
4.3 创建第一个组件
vue
<!-- packages/ui/src/components/Button/button.vue -->
<template>
<button @click="handleClick">
<slot />
</button>
</template>
<script setup lang="ts">
import { APP_NAME } from '@gdu-common/shared'
import { hello } from '@gdu-common/utils'
const handleClick = () => {
hello(APP_NAME)
}
</script>
<style scoped lang="scss">
button {
padding: 10px 20px;
background-color: #42b983;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s;
&:hover {
background-color: #369970;
}
}
</style>
typescript
// packages/ui/src/components/Button/index.ts
export { default as Button } from './button.vue'
// packages/ui/src/components/index.ts
export * from './Button'
// packages/ui/src/index.ts
export * from './components'
4.4 配置 Vite(Vue 组件)
typescript
// packages/ui/vite.config.ts
import vue from '@vitejs/plugin-vue'
import { defineConfig } from 'vite'
import dts from 'vite-plugin-dts'
export default defineConfig({
plugins: [
vue(),
dts({
outDir: 'dist',
cleanVueFileName: true,
rollupTypes: true,
}),
],
build: {
lib: {
entry: './src/index.ts',
name: 'GduUI',
formats: ['es', 'umd'],
fileName: format => `index.${format === 'es' ? 'js' : 'umd.js'}`,
},
rollupOptions: {
external: ['vue', '@gdu-common/shared', '@gdu-common/utils'],
output: {
globals: {
vue: 'Vue',
},
assetFileNames: assetInfo => {
if (assetInfo.name?.endsWith('.css')) return 'index.css'
return assetInfo.name || ''
},
},
},
cssCodeSplit: false,
},
})
🔗 第五步:配置 TypeScript
5.1 根目录 tsconfig
json
// tsconfig.base.json - 基础配置
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"resolveJsonModule": true,
"isolatedModules": true,
"paths": {
"@gdu-common/*": ["packages/*/src"]
}
}
}
json
// tsconfig.packages.json - 包通用配置
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"baseUrl": "./",
"outDir": "./dist"
},
"include": ["src"]
}
5.2 各包的 tsconfig
json
// packages/shared/tsconfig.build.json
{
"extends": "../../tsconfig.packages.json"
}
// packages/utils/tsconfig.build.json
{
"extends": "../../tsconfig.packages.json"
}
// packages/ui/tsconfig.build.json
{
"extends": "../../tsconfig.packages.json",
"compilerOptions": {
"baseUrl": "../../",
"paths": {
"@gdu-common/utils": ["packages/utils/src"],
"@gdu-common/shared": ["packages/shared/src"]
}
}
}
🎯 第六步:统一构建配置
6.1 创建共享构建配置
typescript
// build/build.config.ts
import vue from '@vitejs/plugin-vue'
import { type UserConfig } from 'vite'
import dts from 'vite-plugin-dts'
interface BuildOptions {
name?: string
external?: string[]
globals?: Record<string, string>
}
/**
* 生成普通库的构建配置
*/
export function generateConfig(options: BuildOptions = {}): UserConfig {
const { name = 'Library', external = [], globals = {} } = options
return {
plugins: [
dts({
outDir: 'dist/es',
entryRoot: 'src',
}),
],
build: {
lib: {
entry: './src/index.ts',
name,
formats: ['es', 'umd'],
},
rollupOptions: {
external,
output: [
{
format: 'es',
dir: 'dist/es',
entryFileNames: '[name].mjs',
preserveModules: true,
preserveModulesRoot: 'src',
},
{
format: 'umd',
dir: 'dist/umd',
entryFileNames: `${name.toLowerCase()}.umd.js`,
name,
globals,
},
],
},
},
}
}
/**
* 生成 Vue 组件库的构建配置
*/
export function generateVueConfig(options: BuildOptions = {}): UserConfig {
const { name = 'Library', external = [], globals = {} } = options
return {
plugins: [
vue(),
dts({
outDir: 'dist',
cleanVueFileName: true,
rollupTypes: true,
}),
],
build: {
lib: {
entry: './src/index.ts',
name,
formats: ['es', 'umd'],
fileName: format => `index.${format === 'es' ? 'js' : 'umd.js'}`,
},
rollupOptions: {
external: ['vue', ...external],
output: {
globals: {
vue: 'Vue',
...globals,
},
assetFileNames: assetInfo => {
if (assetInfo.name?.endsWith('.css')) return 'index.css'
return assetInfo.name || ''
},
},
},
cssCodeSplit: false,
},
}
}
6.2 简化各包的 vite.config.ts
typescript
// packages/shared/vite.config.ts
import { generateConfig } from '../../build/build.config'
export default generateConfig({
name: 'GduShared',
})
// packages/utils/vite.config.ts
import { generateConfig } from '../../build/build.config'
export default generateConfig({
name: 'GduUtils',
external: ['lodash-es'],
globals: {
'lodash-es': 'LodashEs',
},
})
// packages/ui/vite.config.ts
import { generateVueConfig } from '../../build/build.config'
export default generateVueConfig({
name: 'GduUI',
})
🧪 第七步:测试构建
7.1 构建所有包
bash
# 回到根目录
cd ../..
# 安装所有依赖
pnpm install
# 构建所有包
pnpm build
预期输出:
bash
> gdu-common@1.0.0 build
> turbo run build
• Packages in scope: @gdu-common/shared, @gdu-common/ui, @gdu-common/utils
• Running build in 3 packages
@gdu-common/shared:build: cache miss, executing...
@gdu-common/utils:build: cache miss, executing...
@gdu-common/shared:build: ✓ built in 2.1s
@gdu-common/utils:build: ✓ built in 2.3s
@gdu-common/ui:build: cache miss, executing...
@gdu-common/ui:build: ✓ built in 3.2s
Tasks: 3 successful, 3 total
Cached: 0 cached, 3 total
Time: 7.6s
7.2 再次构建(测试缓存)
bash
pnpm build
预期输出:
bash
Tasks: 3 successful, 3 total
Cached: 3 cached, 3 total ⚡
Time: 450ms >>> FULL TURBO
# 从 7.6s 到 450ms,提升 16.9 倍!
🔗 第八步:包之间的依赖
8.1 在 ui 包中使用 shared 和 utils
我们在第四步已经演示了:
vue
<script setup lang="ts">
import { APP_NAME } from '@gdu-common/shared'
import { hello } from '@gdu-common/utils'
const handleClick = () => {
hello(APP_NAME) // ✅ 可以直接使用!
}
</script>
8.2 依赖解析原理
TypeScript 路径映射:
json
// tsconfig.base.json
{
"compilerOptions": {
"paths": {
"@gdu-common/*": ["packages/*/src"]
}
}
}
pnpm workspace 协议:
json
// packages/ui/package.json
{
"peerDependencies": {
"@gdu-common/shared": "workspace:^", // workspace 协议
"@gdu-common/utils": "workspace:^"
}
}
Turborepo 依赖编排:
json
// turbo.json
{
"tasks": {
"build": {
"dependsOn": ["^build"] // ^ 表示依赖包的 build
}
}
}
8.3 依赖关系验证
bash
# 查看依赖关系
pnpm list -r --depth=0
# 输出
@gdu-common/shared 1.0.0
@gdu-common/utils 1.0.0
@gdu-common/ui 1.0.0 (depends on shared, utils)
📚 第九步:添加文档站点
9.1 安装 VitePress
bash
mkdir docs
cd docs
pnpm init
pnpm add -D vitepress vue
9.2 配置文档
typescript
// docs/.vitepress/config.ts
import { resolve } from 'path'
import { defineConfig } from 'vitepress'
export default defineConfig({
title: 'GDU Common',
description: 'GDU 前端通用组件库和工具集',
vite: {
resolve: {
alias: {
'@gdu-common/ui': resolve(__dirname, '../../packages/ui/src'),
'@gdu-common/utils': resolve(__dirname, '../../packages/utils/src'),
'@gdu-common/shared': resolve(__dirname, '../../packages/shared/src'),
},
},
},
themeConfig: {
nav: [
{ text: '指南', link: '/guide/' },
{ text: '组件', link: '/components/' },
],
},
})
9.3 创建文档内容
markdown
<!-- docs/index.md -->
# GDU Common
企业级前端通用组件库和工具集
## 快速开始
\`\`\`bash
pnpm add @gdu-common/ui @gdu-common/utils
\`\`\`
## 组件示例
\`\`\`vue
<template>
<Button>点击我</Button>
</template>
<script setup>
import { Button } from '@gdu-common/ui'
</script>
\`\`\`
🎯 第十步:完整的项目脚本
10.1 根 package.json
json
{
"name": "gdu-common",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev",
"lint": "turbo run lint",
"lint:fix": "turbo run lint:fix",
"clean": "turbo run clean",
"clean:cache": "rimraf .turbo node_modules/.cache",
"doc:dev": "pnpm --filter @gdu-common/docs dev"
},
"devDependencies": {
"@vitejs/plugin-vue": "^6.0.1",
"rimraf": "^6.0.1",
"turbo": "^2.5.8",
"typescript": "^5.9.2",
"vite": "^7.1.2",
"vite-plugin-dts": "^4.5.4",
"vue": "^3.5.22"
},
"dependencies": {
"lodash-es": "^4.17.21"
}
}
10.2 各包的标准脚本
json
// 每个包都应该有这些脚本
{
"scripts": {
"build": "vite build",
"dev": "vite build --watch",
"clean": "rimraf dist .turbo",
"lint": "eslint . --ext .js,.ts,.vue",
"lint:fix": "eslint . --ext .js,.ts,.vue --fix"
}
}
🚀 第十一步:运行和测试
11.1 开发模式
bash
# 终端1:启动 ui 包的 watch 模式
pnpm --filter @gdu-common/ui dev
# 终端2:启动文档站点
pnpm doc:dev
# 修改 Button 组件 → 自动重建 → 文档站点热更新
# 真正的快速开发体验!⚡
11.2 生产构建
bash
# 构建所有包
pnpm build
# 第一次构建
Tasks: 4 successful, 4 total
Cached: 0 cached, 4 total
Time: 9.2s
# 第二次构建(无修改)
Tasks: 4 successful, 4 total
Cached: 4 cached, 4 total ⚡
Time: 450ms >>> FULL TURBO
# 提升 20.4 倍!🚀
11.3 增量构建测试
bash
# 修改 shared 包
echo "export const NEW_CONST = 'test'" >> packages/shared/src/index.ts
# 再次构建
pnpm build
@gdu-common/shared:build: cache miss, executing...
@gdu-common/utils:build: cache hit ⚡
@gdu-common/ui:build: cache miss (dependency changed)
@gdu-common/docs:build: cache hit ⚡
Tasks: 4 successful, 4 total
Cached: 2 cached, 4 total
Time: 5.1s
# 只重建了受影响的包!
💡 最佳实践
1. 包命名规范
bash
✅ 推荐:
@company/ui
@company/utils
@company/shared
❌ 不推荐:
ui-components
my-utils
shared
2. 依赖层级设计
less
基础层(无依赖)
↓
@gdu-common/shared
工具层(依赖基础层)
↓
@gdu-common/utils
业务层(依赖基础+工具)
↓
@gdu-common/ui
@gdu-common/controls-sdk
应用层
↓
docs
3. 版本管理策略
json
// 内部依赖使用 workspace 协议
{
"dependencies": {
"@gdu-common/utils": "workspace:^"
}
}
// 发布时自动转换为实际版本
{
"dependencies": {
"@gdu-common/utils": "^1.2.3"
}
}
4. 构建优化技巧
json
// turbo.json
{
"tasks": {
"build": {
"inputs": [
"$TURBO_DEFAULT$",
"!{dist,coverage,.turbo}/**", // 排除输出目录
"!**/*.md", // 排除文档
"!**/*.test.{ts,tsx}" // 排除测试
]
}
}
}
🤔 常见问题
Q1: 如何添加新包?
bash
# 1. 创建目录
mkdir -p packages/new-package/src
# 2. 初始化
cd packages/new-package
pnpm init
# 3. 配置 package.json
{
"name": "@gdu-common/new-package",
"version": "1.0.0",
"scripts": {
"build": "vite build"
}
}
# 4. 创建源代码
echo "export const test = 'test'" > src/index.ts
# 5. 返回根目录构建
cd ../..
pnpm build
Q2: 如何在包之间互相引用?
typescript
// 1. 添加依赖
cd packages/ui
pnpm add @gdu-common/utils@workspace:*
// 2. 直接导入使用
import { formatDate } from '@gdu-common/utils'
Q3: 为什么缓存没有命中?
bash
# 检查缓存未命中原因
pnpm build --dry-run
# 常见原因:
1. 源代码改变
2. 依赖改变
3. 配置文件改变(tsconfig、eslint等)
4. 环境变量改变
Q4: 如何调试构建问题?
bash
# 查看详细日志
pnpm build --verbosity=2
# 查看缓存状态
pnpm turbo run build --dry-run
# 清理缓存重试
pnpm clean:cache
pnpm build
📈 性能对比总结
构建性能
场景 | 传统方式 | Turborepo | 提升 |
---|---|---|---|
首次构建 | 45s | 9s | 5x |
完全缓存 | 45s | 0.45s | 100x |
修改1个包 | 45s | 2.3s | 19.6x |
平均构建 | 45s | 3-4s | 11-15x |
磁盘空间
场景 | npm | pnpm | 节省 |
---|---|---|---|
单个项目 | 350MB | 120MB | 65.7% |
5个项目 | 1.75GB | 350MB | 80% |
开发效率
任务 | Multirepo | Monorepo | 提升 |
---|---|---|---|
跨包重构 | 30分钟 | 2分钟 | 15x |
本地联调 | 10分钟 | 即时 | ∞ |
版本发布 | 20分钟 | 5分钟 | 4x |
🎉 完整项目清单
项目结构
lua
gdu-common/
├── packages/
│ ├── shared/
│ │ ├── src/
│ │ │ ├── index.ts
│ │ │ └── common/
│ │ ├── package.json
│ │ ├── tsconfig.build.json
│ │ └── vite.config.ts
│ ├── utils/
│ │ ├── src/
│ │ ├── package.json
│ │ └── vite.config.ts
│ └── ui/
│ ├── src/
│ │ └── components/
│ │ └── Button/
│ ├── package.json
│ └── vite.config.ts
├── docs/
│ ├── .vitepress/
│ │ └── config.ts
│ ├── index.md
│ └── package.json
├── build/
│ ├── build.config.ts
│ └── package.json
├── turbo.json
├── pnpm-workspace.yaml
├── tsconfig.base.json
├── tsconfig.packages.json
├── package.json
└── .gitignore
关键文件检查清单
-
pnpm-workspace.yaml
- workspace 配置 -
turbo.json
- Turborepo 配置 -
tsconfig.base.json
- TS 基础配置 -
tsconfig.packages.json
- 包通用配置 -
build/build.config.ts
- 共享构建配置 - 每个包的
package.json
- 每个包的
vite.config.ts
- 每个包的
tsconfig.build.json
🎁 项目模板
GitHub 仓库
我已经将完整的项目模板开源:
bash
# 克隆模板
git clone https://github.com/Fangxin920915/fang-common-template.git
# 安装依赖
pnpm install
# 开始开发
pnpm dev
🚀 下一步
现在你已经有了一个完整的 Monorepo 项目!接下来我们将:
- 第4篇: 配置代码质量工具(ESLint + Prettier + Stylelint)
- 第5篇: 版本管理和自动发布(Changeset)
- 第6篇: 文档站点优化(VitePress 深度定制)
- 第7篇: CI/CD 自动化(GitLab CI)
- 第8篇: 性能优化和最佳实践
💭 思考题
- 你的项目有多少个包?是否需要 Turborepo?
- 试着用本文的方法搭建一个 Monorepo,需要多长时间?
- 缓存命中率如何?能提升多少倍?
🔗 系列文章
- 📖 上一篇: Monorepo 工具大比拼:为什么我最终选择了 pnpm + Turborepo?
- 📖 下一篇: 《代码质量保障:ESLint + Prettier + Stylelint 完美配置》
- 🏠 专栏首页: 从零到一:构建现代化企业级 Monorepo 项目实战
完整的项目代码已开源,欢迎 star!觉得有帮助的话,请点赞收藏支持一下! 🙏
你在搭建过程中遇到了什么问题?欢迎在评论区讨论! 💬