从零搭建 Monorepo:pnpm workspace + Turborepo 实战指南

Building a Monorepo from Scratch: pnpm Workspace and Turborepo Practical Guide

| Sophia Li | 2026-08-02T10:37:35

项目越来越多,维护多个仓库快疯了。这篇记录了我们用 pnpm workspace + Turborepo 搭建 Monorepo 的全过程。

A complete walkthrough of setting up a Monorepo with pnpm workspace and Turborepo to manage multiple projects efficiently.

## 为什么要 Monorepo 我们团队维护着 6 个前端项目:官网、管理后台、H5 活动页、组件库、工具函数库、API SDK。分成 6 个独立仓库后,痛点越来越明显: - 组件库改个 Button,6 个项目要分别升级依赖 - 工具函数库的 TypeScript 类型改了,下游项目编译不报错运行时才崩 - 同一个 ESLint 配置复制粘贴了 6 份,每次改规则要改 6 遍 - 发版要按依赖顺序手动发,搞错顺序就炸 ## 技术选型 | 方案 | 构建编排 | 包管理 | 优点 | 缺点 | |------|---------|--------|------|------| | Lerna | 有 | npm/yarn | 老牌 | 维护缓慢 | | Nx | 强 | 任意 | 功能最全 | 概念多,学习曲线陡 | | Turborepo | 强 | 任意 | 简单快速 | 功能比 Nx 少 | | pnpm workspace | 无 | pnpm | 原生快 | 缺构建编排 | 最终选了 **pnpm workspace + Turborepo** 的组合:pnpm 管依赖,Turborepo 管构建编排。兼顾简洁和实用。 ## 目录结构 ``` idev-frontend/ ├── apps/ │ ├── web/ # 官网(Next.js) │ ├── admin/ # 管理后台(React + Vite) │ └── h5/ # H5 活动页(React + Vite) ├── packages/ │ ├── ui/ # 组件库 │ ├── utils/ # 工具函数库 │ ├── api-sdk/ # API SDK │ ├── tsconfig/ # 共享 TS 配置 │ └── eslint-config/ # 共享 ESLint 配置 ├── pnpm-workspace.yaml ├── turbo.json └── package.json ``` ## 第一步:pnpm workspace 配置 ```yaml # pnpm-workspace.yaml packages: - 'apps/*' - 'packages/*' ``` ```json // 根 package.json { "private": true, "scripts": { "dev": "turbo dev", "build": "turbo build", "lint": "turbo lint", "test": "turbo test" }, "devDependencies": { "turbo": "^2.1.0" } } ``` ## 第二步:Turborepo 构建编排 ```json // turbo.json { "$schema": "https://turbo.build/schema.json", "tasks": { "build": { "dependsOn": ["^build"], "outputs": ["dist/**", ".next/**"] }, "dev": { "cache": false, "persistent": true }, "lint": { "dependsOn": ["^build"] }, "test": { "dependsOn": ["build"] } } } ``` 核心是 `"dependsOn": ["^build"]`——`^` 表示"先构建我依赖的包"。这样 `pnpm build` 一条命令,Turborepo 自动分析依赖关系,按正确顺序并行构建。 ## 第三步:包之间的引用 ```json // apps/web/package.json { "dependencies": { "@idev/ui": "workspace:*", "@idev/utils": "workspace:*", "@idev/api-sdk": "workspace:*" } } ``` `workspace:*` 表示引用本地 workspace 的包,pnpm 会自动创建符号链接。改了组件库的代码,官网立刻能看到变化,不需要发版。 ## 第四步:共享配置 ```javascript // packages/eslint-config/index.js module.exports = { extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'], rules: { '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], 'no-console': ['warn', { allow: ['warn', 'error'] }], }, }; ``` ```json // apps/web/.eslintrc.json { "extends": ["@idev/eslint-config"] } ``` 改一处,全部项目生效。 ## 踩坑记录 **坑 1:幽灵依赖** pnpm 的严格模式不允许使用未声明的依赖。迁移过来后一堆 import 报错,因为之前 npm/yarn 的 hoist 机制让项目"偷"了别的包的依赖。 解决:逐个检查报错,把缺的依赖加到对应包的 `package.json` 里。虽然麻烦,但这是好事——依赖关系终于清晰了。 **坑 2:TypeScript 项目引用** Monorepo 里多个 `tsconfig.json` 的 `paths` 配置容易搞混。最终统一用了 TypeScript 的 Project References: ```json // apps/web/tsconfig.json { "extends": "@idev/tsconfig/nextjs.json", "references": [ { "path": "../../packages/ui" }, { "path": "../../packages/utils" } ] } ``` **坑 3:Turborepo 缓存失效** Turborepo 的远程缓存默认用 Vercel。我们自己搭了缓存服务器(用了开源的 `turbo-remote-cache`),CI 构建时间从 8 分钟降到了 2 分钟。 ## 迁移后的效果 | 指标 | 迁移前 | 迁移后 | |------|--------|--------| | 组件库变更传播 | 手动发版,1-2 天 | 即时,0 秒 | | CI 构建时间 | 8 分钟 x 6 | 2 分钟(总计) | | ESLint 配置维护 | 6 份 | 1 份 | | 新项目搭建 | 2 天 | 30 分钟 | Monorepo 不是银弹,但对于我们这种多项目共享代码的场景,收益非常明显。


## Why Monorepo Managing 6 frontend repos with shared dependencies became painful - component updates took days to propagate, configs were duplicated 6 times, and release ordering was error-prone. ## Tech Choice pnpm workspace for dependency management + Turborepo for build orchestration. Simple yet powerful combination. ## Key Setup Workspace protocol (`workspace:*`) for local package references, Turborepo's `dependsOn: ["^build"]` for automatic dependency-ordered parallel builds, and shared configs for ESLint/TypeScript. ## Results Component updates propagate instantly (vs 1-2 days), CI time dropped from 48 minutes total to 2 minutes, and new project setup reduced from 2 days to 30 minutes.

← Back to News