🏗️ 从 0 到 1 搭建 pnpm + Workspaces + Turborepo Monorepo 保姆级指南
本指南面向新人,从空目录开始,手把手带你搭建一个基于 pnpm Workspaces + Turborepo 的 monorepo 项目。 最终项目结构:
apps/web(Vue 3 应用)+packages/shared(共享工具包),全程使用 TypeScript。
📋 最终项目结构预览
搭建完成后的目录结构如下:
csharp
work/
├── package.json # 根项目配置(定义 monorepo 入口脚本)
├── pnpm-workspace.yaml # pnpm 工作区配置(声明子包目录)
├── turbo.json # Turborepo 任务管道配置
├── tsconfig.base.json # TypeScript 基础配置(供子包继承)
├── .gitignore # Git 忽略规则
├── 搭建流程.md # 本文档
├── apps/
│ └── web/ # Vue 3 + Vite + TypeScript 应用
│ ├── package.json
│ ├── tsconfig.json
│ ├── vite.config.ts
│ ├── index.html
│ └── src/
│ ├── main.ts # Vue 应用入口
│ └── App.vue # 根组件
└── packages/
└── shared/ # 共享工具包(@work/shared)
├── package.json
├── tsconfig.json
└── src/
└── index.ts # 导出工具函数
🔧 环境准备
1. 安装 Node.js
前往 Node.js 官网 下载并安装 LTS 版本(推荐 v20 或更高)。
安装完成后,打开终端验证:
bash
node --version
# 预期输出: v20.x.x 或更高
2. 安装 pnpm
pnpm 是一个快速、节省磁盘空间的包管理器,支持 Workspaces(工作区),是搭建 monorepo 的首选工具。
bash
# 方式一:使用 npm 全局安装
npm install -g pnpm
# 方式二:使用 Node.js 自带的 corepack(推荐)
corepack enable
corepack prepare pnpm@latest --activate
验证安装:
bash
pnpm --version
# 预期输出: 12.x.x 或更高
💡 为什么要用 pnpm 而不是 npm/yarn?
- 节省磁盘空间:pnpm 使用硬链接机制,相同的包只存储一份
- 严格的依赖管理:默认不允许访问未声明的依赖(避免幽灵依赖问题)
- 原生 Workspaces 支持:无需额外插件即可管理 monorepo
🚀 第一步:初始化项目
1.1 创建项目目录
bash
mkdir work
cd work
1.2 初始化 package.json
bash
pnpm init
这会生成一个默认的 package.json。我们需要手动修改它,使其成为 monorepo 的根配置:
jsonc
{
"name": "work-monorepo", // 项目名称
"version": "0.0.0", // 版本号
"private": true, // 标记为私有,防止误发布到 npm
"packageManager": "pnpm@12.10.1", // 指定包管理器版本(用于 corepack 自动切换)
"scripts": {
"dev": "turbo run dev", // 启动所有子包的开发服务器
"build": "turbo run build", // 构建所有子包
"lint": "turbo run lint" // 代码检查所有子包
},
"devDependencies": {
"turbo": "^2.3.3", // Turborepo 构建系统
"typescript": "^5.7.2" // TypeScript 编译器
}
}
关键字段说明:
| 字段 | 说明 |
|---|---|
private: true |
根项目不发布到 npm,仅作为 monorepo 容器 |
packageManager |
配合 corepack 使用,团队成员自动使用相同版本的 pnpm |
scripts |
所有脚本通过 turbo run 委托给 Turborepo 执行,实现任务编排和缓存 |
devDependencies |
全局开发依赖,所有子包共享 |
📦 第二步:配置 pnpm Workspaces
2.1 创建 pnpm-workspace.yaml
在项目根目录创建 pnpm-workspace.yaml 文件:
yaml
packages:
- "apps/*"
- "packages/*"
onlyBuiltDependencies:
- esbuild
配置说明:
| 字段 | 说明 |
|---|---|
packages |
声明工作区目录,apps/* 和 packages/* 表示这两个目录下的每个子目录都是一个独立的工作区包 |
onlyBuiltDependencies |
允许运行构建脚本的第三方依赖列表。pnpm 出于安全考虑默认拦截依赖的 postinstall 脚本,这里显式允许 esbuild(Vite 的底层依赖) |
💡 通配符规则:
apps/*匹配apps/web、apps/admin等所有 apps 下的子目录packages/*匹配packages/shared、packages/ui等所有 packages 下的子目录- 你也可以用
apps/**来匹配多层嵌套目录
⚠️ pnpm 12 重要变更: 在 pnpm 12 之前,onlyBuiltDependencies配置写在package.json的pnpm字段中。 pnpm 12 开始,该配置已迁移到pnpm-workspace.yaml,package.json中的pnpm字段不再被读取。
2.2 创建 .gitignore
bash
# 依赖
node_modules
.pnpm-store
# 构建产物
dist
build
*.tsbuildinfo
# Turborepo 缓存
.turbo
# 环境变量
.env
.env.*
!.env.example
# 日志
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# 编辑器
.vscode/*
!.vscode/extensions.json
.idea
# 系统文件
.DS_Store
Thumbs.db
🛠️ 第三步:安装全局开发依赖
在根目录执行以下命令,安装 Turborepo 和 TypeScript 到根项目的 devDependencies:
bash
pnpm install
💡 为什么不用
pnpm add -Dw turbo typescript? 因为我们已经在package.json的devDependencies中声明了 turbo 和 typescript, 直接执行pnpm install即可安装。-Dw参数的含义是:
-D:安装为开发依赖(devDependencies)-w:安装到工作区根目录(workspace root)
预期输出:
diff
Scope: all 3 workspace projects
devDependencies:
+ turbo 2.11.7
+ typescript 5.9.3
⚠️ 关于 esbuild 构建脚本警告: 安装时可能会看到
ERR_PNPM_IGNORED_BUILDS警告,提示 esbuild 的构建脚本被忽略。 这是 pnpm 的安全特性。我们已经通过pnpm-workspace.yaml中的onlyBuiltDependencies配置解决了此问题。即使看到此警告也不影响实际使用,因为 esbuild 的二进制文件会通过 optionalDependencies 自动安装。如果警告持续出现,可以手动执行pnpm approve-builds来批准构建脚本。
⚡ 第四步:配置 Turborepo
4.1 创建 turbo.json
在项目根目录创建 turbo.json:
jsonc
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"], // 先构建上游依赖包
"outputs": ["dist/**"] // 缓存构建产物
},
"dev": {
"dependsOn": ["^build"], // 开发前先构建依赖
"cache": false, // 开发服务器不缓存
"persistent": true // 标记为长驻进程
},
"lint": {
"dependsOn": ["^build"], // lint 前先构建依赖
"outputs": [] // lint 无产物输出
}
}
}
逐字段说明:
| 字段 | 说明 |
|---|---|
$schema |
JSON Schema 地址,提供编辑器自动补全和校验 |
tasks |
任务管道定义(Turborepo v2+ 推荐字段名,旧版使用 pipeline) |
dependsOn: ["^build"] |
^ 表示先执行上游依赖包的同名任务。例如 apps/web 的 build 会先触发 @work/shared 的 build |
outputs |
声明构建产物路径,Turbo 会缓存这些文件,下次构建时直接复用 |
cache: false |
禁用缓存(开发服务器不需要缓存) |
persistent: true |
标记为长驻进程,Turbo 不会等待它退出 |
💡 Turborepo 的核心价值:
- 任务编排:自动按依赖拓扑顺序执行任务(先构建 shared,再构建 web)
- 增量构建:只重新构建发生变化的包
- 构建缓存:缓存构建产物,重复构建秒级完成
- 并行执行:无依赖关系的任务并行执行
📝 第五步:配置 TypeScript 基础配置
5.1 创建 tsconfig.base.json
在项目根目录创建 tsconfig.base.json:
jsonc
{
"compilerOptions": {
"target": "ES2020", // 编译目标 JS 版本
"module": "ESNext", // 模块系统
"moduleResolution": "bundler", // 模块解析策略(适配 Vite 等打包工具)
"baseUrl": ".", // 路径解析基准目录
"lib": ["ES2020", "DOM", "DOM.Iterable"], // 类型库
"strict": true, // 开启所有严格类型检查
"esModuleInterop": true, // 兼容 CommonJS 模块的默认导入
"skipLibCheck": true, // 跳过第三方库的类型检查(加快编译)
"forceConsistentCasingInFileNames": true, // 强制文件名大小写一致
"resolveJsonModule": true, // 允许导入 JSON 文件
"isolatedModules": true, // 每个文件独立编译(适配打包工具)
"declaration": true, // 生成 .d.ts 类型声明文件
"declarationMap": true, // 生成声明文件的 source map
"sourceMap": true, // 生成 source map
"paths": {
"@work/*": ["packages/*/src"] // 路径别名,@work/shared 映射到 packages/shared/src
}
}
}
关键配置说明:
| 配置项 | 说明 |
|---|---|
moduleResolution: "bundler" |
适配现代打包工具(Vite/webpack)的模块解析策略,支持 package.json 的 exports 字段 |
baseUrl: "." |
设置路径解析的基准目录,paths 别名依赖此配置 |
paths |
路径别名映射,@work/shared 会解析到 packages/shared/src/index.ts |
strict: true |
开启严格模式,包括 null 检查、隐式 any 检查等 |
declaration: true |
生成类型声明文件,供引用方获取类型提示 |
💡 为什么需要 baseUrl?
paths别名需要baseUrl作为基准目录来解析相对路径。 虽然 TypeScript 5.0+ 在moduleResolution: "bundler"模式下理论上可以不设 baseUrl, 但为了兼容性和稳定性,建议显式设置。
⚠️ 注意:此文件仅作为 base 被子包 extends,不直接参与编译,因此不设置 include/exclude。
📚 第六步:创建共享包 packages/shared
共享包是 monorepo 的核心优势之一------多个应用可以复用同一份代码。
6.1 创建目录结构
bash
packages/shared/
├── package.json # 包配置
├── tsconfig.json # TypeScript 配置(继承根配置)
└── src/
└── index.ts # 源码入口
6.2 创建 package.json
jsonc
{
"name": "@work/shared", // 包名,使用 @work scope 命名空间
"version": "0.0.0", // 版本号
"private": true, // 私有包,不发布到 npm
"main": "./src/index.ts", // CommonJS 入口
"types": "./src/index.ts", // 类型声明入口
"exports": {
"types": "./src/index.ts", // TypeScript 类型解析入口
"default": "./src/index.ts" // 默认入口
},
"scripts": {
"build": "tsc" // 使用 TypeScript 编译器构建
}
}
关键字段说明:
| 字段 | 说明 |
|---|---|
name: "@work/shared" |
@work 是 scope(命名空间),shared 是包名。使用 scope 可以避免与公共包冲突 |
exports |
现代包导出配置。types 条件让 TypeScript 正确解析类型,default 条件是运行时入口 |
main / types |
兼容旧版工具的入口配置 |
💡 为什么 exports 中 types 和 default 都指向 src/index.ts? 在 monorepo 内部开发时,我们直接引用源码(而非编译后的 dist), 这样可以获得更好的开发体验(无需先编译再引用,修改即时生效)。 发布到 npm 时,再修改为指向
./dist/index.js和./dist/index.d.ts。
6.3 创建 tsconfig.json
jsonc
{
"extends": "../../tsconfig.base.json", // 继承根目录的基础配置
"compilerOptions": {
"outDir": "./dist", // 编译输出目录
"rootDir": "./src" // 源码根目录
},
"include": ["src"] // 只编译 src 目录下的文件
}
说明:
extends继承根配置,避免重复配置outDir指定编译输出目录(执行pnpm build时生成)rootDir指定源码根目录,确保编译后的目录结构与源码一致include限定编译范围
6.4 创建 src/index.ts
typescript
/**
* 格式化日期为 YYYY-MM-DD 格式
* @param date 日期对象
* @returns 格式化后的日期字符串
*/
export function formatDate(date: Date): string {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
/**
* 生成问候语
* @param name 名字
* @returns 问候语字符串
*/
export function greet(name: string): string {
return `你好,${name}!欢迎来到 Work Monorepo 🎉`;
}
这里导出了两个工具函数:
formatDate:将 Date 对象格式化为YYYY-MM-DD字符串greet:生成问候语字符串
🌐 第七步:创建 Vue 应用 apps/web
7.1 创建目录结构
bash
apps/web/
├── package.json # 应用配置
├── tsconfig.json # TypeScript 配置
├── vite.config.ts # Vite 构建配置
├── index.html # 入口 HTML
└── src/
├── main.ts # Vue 应用入口
└── App.vue # 根组件
7.2 创建 package.json
jsonc
{
"name": "@work/web", // 应用名
"version": "0.0.0",
"private": true,
"type": "module", // 使用 ES Module
"scripts": {
"dev": "vite", // 启动开发服务器
"build": "vue-tsc --noEmit && vite build", // 先类型检查再构建
"preview": "vite preview" // 预览构建产物
},
"dependencies": {
"vue": "^3.5.13", // Vue 3
"@work/shared": "workspace:*" // 引用本地共享包
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.1", // Vite 的 Vue 插件
"vite": "^6.0.5", // Vite 构建工具
"typescript": "^5.7.2", // TypeScript
"vue-tsc": "^2.1.10" // Vue 的类型检查工具
}
}
关键字段说明:
| 字段 | 说明 |
|---|---|
type: "module" |
声明此包使用 ES Module(import/export),Vite 要求 |
@work/shared: "workspace:*" |
核心! 使用 workspace:* 协议引用本地工作区包。pnpm 会自动创建符号链接,无需手动维护路径 |
build 脚本 |
先执行 vue-tsc --noEmit 做类型检查(不输出文件),通过后再执行 vite build 构建 |
💡
workspace:*协议详解:
workspace:*:匹配工作区中任意版本workspace:^1.0.0:匹配工作区中 >= 1.0.0 且 < 2.0.0 的版本workspace:~1.0.0:匹配工作区中 >= 1.0.0 且 < 1.1.0 的版本- 安装后,pnpm 会在
node_modules/@work/shared创建一个符号链接指向packages/shared
7.3 创建 tsconfig.json
jsonc
{
"extends": "../../tsconfig.base.json", // 继承根配置
"compilerOptions": {
"types": ["vite/client"], // 引入 Vite 客户端类型(如 import.meta.env)
"jsx": "preserve" // 保留 JSX 语法,交由 Vue 编译器处理
},
"include": ["src"] // 编译范围
}
说明:
types: ["vite/client"]:提供import.meta.env等 Vite 专用 API 的类型声明jsx: "preserve":Vue 的<script setup lang="ts">需要此配置
7.4 创建 vite.config.ts
typescript
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
// https://vite.dev/config/
export default defineConfig({
plugins: [vue()],
});
说明:
defineConfig:提供配置类型的辅助函数vue():启用 Vue 单文件组件(.vue)支持
7.5 创建 index.html
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Work Web</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
说明:
<div id="app">:Vue 应用的挂载点<script type="module" src="/src/main.ts">:Vite 的入口文件
7.6 创建 src/main.ts
typescript
import { createApp } from 'vue';
import App from './App.vue';
createApp(App).mount('#app');
说明:
createApp(App):创建 Vue 应用实例.mount('#app'):将应用挂载到index.html中的#app元素
7.7 创建 src/App.vue
vue
<script setup lang="ts">
import { greet, formatDate } from '@work/shared';
const greeting = greet('新人');
const today = formatDate(new Date());
</script>
<template>
<div class="container">
<h1>🚀 Work Monorepo</h1>
<p class="greeting">{{ greeting }}</p>
<p class="date">今天是 {{ today }}</p>
<p class="hint">
本页面演示了 <code>apps/web</code> 如何引用 <code>@work/shared</code> 共享包
</p>
</div>
</template>
<style scoped>
.container {
max-width: 640px;
margin: 80px auto;
padding: 40px;
font-family: system-ui, -apple-system, sans-serif;
text-align: center;
color: #333;
}
h1 {
font-size: 2rem;
margin-bottom: 24px;
}
.greeting {
font-size: 1.25rem;
color: #42b883;
margin-bottom: 8px;
}
.date {
font-size: 1rem;
color: #666;
margin-bottom: 24px;
}
.hint {
font-size: 0.875rem;
color: #999;
}
code {
background: #f5f5f5;
padding: 2px 6px;
border-radius: 4px;
font-family: 'Fira Code', monospace;
}
</style>
说明:
<script setup lang="ts">:Vue 3 组合式 API 语法糖,自动导出import { greet, formatDate } from '@work/shared':直接引用共享包的函数,就像引用 npm 包一样<style scoped>:CSS 样式仅作用于当前组件
✅ 第八步:安装依赖与运行验证
8.1 安装所有依赖
在项目根目录执行:
bash
pnpm install
预期输出:
less
Scope: all 3 workspace projects
devDependencies:
+ turbo 2.11.7
+ typescript 5.9.3
dependencies:
+ vue 3.5.13
+ @work/shared ← linked to packages/shared
devDependencies:
+ vite 6.0.5
+ @vitejs/plugin-vue 5.2.1
+ vue-tsc 2.1.10
💡 安装后发生了什么?
- pnpm 读取
pnpm-workspace.yaml,识别出 3 个工作区项目(根项目 + apps/web + packages/shared)- 遇到
@work/shared: "workspace:*"时,自动在apps/web/node_modules/@work/shared创建符号链接指向packages/shared- 将公共依赖(typescript 等)提升到根
node_modules,减少重复安装
8.2 验证构建
bash
pnpm build
预期输出:
sql
$ turbo run build
• turbo 2.11.7
• Packages in scope: @work/shared, @work/web
• Running build in 2 packages
• Remote caching disabled
@work/shared:build: cache miss, executing
@work/shared:build: $ tsc
@work/web:build: cache miss, executing
@work/web:build: $ vue-tsc --noEmit && vite build
@work/web:build: vite v6.x.x building for production...
@work/web:build: transforming...
@work/web:build: ✓ 13 modules transformed.
@work/web:build: rendering chunks...
@work/web:build: computing gzip size...
@work/web:build: dist/index.html 0.41 kB │ gzip: 0.28 kB
@work/web:build: dist/assets/index-xxx.css 0.51 kB │ gzip: 0.28 kB
@work/web:build: dist/assets/index-xxx.js 63.66 kB │ gzip: 25.54 kB
@work/web:build: ✓ built in 362ms
Tasks: 2 successful, 2 total
Cached: 0 cached, 2 total
Time: 2.188s
验证要点:
- ✅ Turbo 识别出 2 个包:
@work/shared和@work/web - ✅ 按依赖顺序执行:先构建
@work/shared(tsc 编译),再构建@work/web(vue-tsc 类型检查 + vite build) - ✅ 两个任务全部成功
💡 Turbo 的构建顺序: 因为
apps/web依赖@work/shared,Turbo 会自动先构建 shared 包,再构建 web 应用。 这就是turbo.json中dependsOn: ["^build"]的作用------^表示上游依赖。
8.3 启动开发服务器
bash
pnpm dev
预期输出:
scss
$ turbo run dev
@work/shared:build: $ tsc
@work/web:dev: cache miss, executing
@work/web:dev: $ vite
VITE v6.x.x ready in xxx ms
➜ Local: http://localhost:5173/
➜ Network: use --host to expose
打开浏览器访问 http://localhost:5173/,你应该看到:
- 🚀 Work Monorepo 标题
- "你好,新人!欢迎来到 Work Monorepo 🎉" 问候语
- 当前日期
这证明 apps/web 成功引用了 @work/shared 共享包的函数!
8.4 再次构建验证缓存
停止开发服务器(Ctrl + C),再次执行构建:
bash
pnpm build
预期输出:
yaml
Tasks: 2 successful, 2 total
Cached: 2 cached, 2 total ← 注意这里变成了 2 cached
Time: 234ms ← 时间大幅减少
💡 Turbo 缓存生效了! 因为代码没有变化,Turbo 直接从缓存中恢复了构建产物,无需重新编译。
❓ 常见问题
Q1: workspace:* 协议是什么意思?
workspace:* 是 pnpm Workspaces 的特殊依赖协议,表示"引用本地工作区中的包,匹配任意版本"。pnpm 会在 node_modules 中创建符号链接,指向本地源码。修改共享包代码后,引用方会立即生效,无需重新安装。
Q2: 安装依赖时报 ERR_PNPM_IGNORED_BUILDS 错误怎么办?
这是 pnpm 12 的安全特性,默认拦截第三方依赖的 postinstall 脚本。解决方法:
-
在
pnpm-workspace.yaml中配置onlyBuiltDependencies(推荐):yamlonlyBuiltDependencies: - esbuild -
手动批准(需要在终端中操作):
bashpnpm approve-builds
注意:即使看到此警告,通常也不影响项目构建和运行,因为 esbuild 的二进制文件会通过 optionalDependencies 自动安装。
Q3: TypeScript 报"找不到模块 @work/shared"怎么办?
检查以下几点:
- 确认
tsconfig.base.json中配置了baseUrl: "."和paths别名 - 确认
packages/shared/package.json中exports字段正确配置了types条件 - 确认执行了
pnpm install,使 workspace 链接生效 - 尝试重启 TypeScript 语言服务(VSCode 中
Ctrl+Shift+P→ "TypeScript: Restart TS Server")
Q4: 如何清除 Turborepo 缓存?
bash
# 清除所有 Turbo 缓存
npx turbo clean
# 或者手动删除 .turbo 目录
Remove-Item -Recurse -Force .turbo # PowerShell
rm -rf .turbo # bash/zsh
Q5: 如何新增一个包(如 packages/ui)?
- 创建目录
packages/ui - 创建
packages/ui/package.json(参考 packages/shared 的格式) - 创建
packages/ui/tsconfig.json(继承根配置) - 创建源码文件
packages/ui/src/index.ts - 在需要引用的应用中添加依赖:
"@work/ui": "workspace:*" - 执行
pnpm install链接新包
pnpm-workspace.yaml 中的 packages/* 通配符会自动识别新包,无需修改配置。
Q6: pnpm 12 中 package.json 的 pnpm 字段不生效?
pnpm 12 开始,原来写在 package.json 中 pnpm 字段的配置(如 onlyBuiltDependencies、overrides 等)已迁移到 pnpm-workspace.yaml。package.json 中的 pnpm 字段不再被读取。
迁移示例:
jsonc
// ❌ 旧方式(package.json 中,pnpm 12 不再读取)
{
"pnpm": {
"onlyBuiltDependencies": ["esbuild"]
}
}
// ✅ 新方式(pnpm-workspace.yaml 中)
onlyBuiltDependencies:
- esbuild
Q7: 多个应用如何共享配置(如 ESLint、Prettier)?
可以创建一个 packages/config 包来存放共享配置:
bash
packages/
├── shared/ # 共享工具函数
└── config/ # 共享配置
├── eslint/
│ └── index.js
└── prettier/
└── index.js
然后在各应用中通过 workspace:* 引用:
jsonc
{
"devDependencies": {
"@work/config": "workspace:*"
}
}
🎉 恭喜!搭建完成
你已经成功搭建了一个基于 pnpm + Workspaces + Turborepo 的 monorepo 项目!
回顾你掌握的知识点:
- ✅ pnpm Workspaces 的工作区配置和包间引用(
workspace:*协议) - ✅ Turborepo 的任务编排、依赖拓扑和构建缓存
- ✅ TypeScript 在 monorepo 中的配置继承和路径别名
- ✅ Vue 3 + Vite 应用的搭建和共享包引用
下一步可以尝试:
- 添加更多应用(如
apps/admin管理后台) - 添加更多共享包(如
packages/ui组件库、packages/config配置包) - 集成 ESLint + Prettier 统一代码风格
- 配置 CI/CD 流水线
- 启用 Turborepo 远程缓存