---
title: Monorepo 实战指南：pnpm/Turbo/Nx 与示例
slug: monorepo-practical-guide
date: 2025-11-02
author: Frankie 徐
category: other
tags: ['monorepo', 'pnpm', 'turborepo', 'nx', 'changesets']
description: 系统梳理 monorepo 原理、方案选型与落地实践，附 pnpm/Turborepo/Nx 配置与 CI 发布示例。
permalink: https://www.210k.cc/monorepo-practical-guide
---

写在前面（学习笔记）

- 这是我边搭 monorepo 边做的记录：从最轻的 pnpm workspaces 开始，够用再加 Turbo/Nx。
- 我更在意“能跑通 + 好维护”，所以例子以最小可用为主，后面再逐步增强（缓存、影响分析、发布）。
- 如果只想抄作业：先按“仅 pnpm”跑通，再引入 Turbo 的 `turbo.json`，最后视复杂度考虑 Nx。

一句话版本：先简单起库，跑通 apps 依赖 packages；再加任务编排与缓存；最后把版本与发布（Changesets）纳入流程。

## 为什么选择 Monorepo？

- 共享代码与类型：UI 组件、工具库、API SDK 一处维护，多处复用。
- 一致的工程标准：统一 ESLint/TS/格式化/提交规范，降低认知负担。
- 原子化改动：跨包重构单 PR 完成，避免多仓库同步困难。
- 增量构建与缓存：只构建受影响的包，提速 CI/CD。

典型适用：多前端应用 + 共享包、多服务应用共享 schema/types、设计系统落地等。

---

## 目录结构基线

```
apps/
  web/           # Next.js 应用
  admin/         # 另一个前端或 BFF
packages/
  ui/            # 共享 UI 组件库（无 Next 依赖）
  api-sdk/       # 前后端共享 types/zod schema/客户端
  config/        # eslint/ts/biome 等集中配置
  tsconfig/      # ts 基线配置（可选）
```

原则：包边界清晰、依赖方向单向（apps 依赖 packages；packages 间尽量无环）。

---

## 方案对比与选型建议

- 仅 pnpm workspaces：零上手成本，适合小型到中型；无内建任务编排/缓存。
- pnpm + Turborepo：主流组合；声明任务依赖、远程缓存、影响分析，性价比高。
- pnpm + Nx：特性最全（影响分析、图谱、生成器、插件生态）；配置略重，适合复杂项目。

建议：
- 小团队/中等规模：pnpm + Turbo。
- 复杂依赖图/多语言/更强脚手架：Nx。

---

## 最小可用示例（一）：仅 pnpm workspaces

根目录 `package.json`：

```json
{
  "name": "acme-monorepo",
  "private": true,
  "packageManager": "pnpm@9",
  "workspaces": ["apps/*", "packages/*"],
  "scripts": {
    "build": "pnpm -r --filter ./packages/* run build && pnpm -r --filter ./apps/* run build",
    "dev": "pnpm -r --parallel --filter ./apps/* run dev",
    "test": "pnpm -r run test"
  }
}
```

`pnpm-workspace.yaml`：

```yaml
packages:
  - 'apps/*'
  - 'packages/*'
```

共享 UI 包 `packages/ui/package.json`：

```json
{
  "name": "@acme/ui",
  "version": "0.1.0",
  "type": "module",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" } },
  "scripts": { "build": "tsc -p tsconfig.build.json" },
  "peerDependencies": { "react": ">=18", "react-dom": ">=18" }
}
```

`packages/ui/src/Button.tsx`：

```tsx
import type { ButtonHTMLAttributes } from 'react';

export function Button(props: ButtonHTMLAttributes<HTMLButtonElement>) {
  return <button {...props} />;
}
```

`packages/ui/src/index.ts`：

```ts
export * from './Button';
```

Next.js 应用依赖：`apps/web/package.json`

```json
{
  "name": "web",
  "private": true,
  "dependencies": { "next": "^14", "react": "^18", "react-dom": "^18", "@acme/ui": "workspace:*" },
  "scripts": { "dev": "next dev", "build": "next build", "start": "next start" }
}
```

页面使用：`apps/web/app/page.tsx`

```tsx
import { Button } from '@acme/ui';

export default function Page() {
  return <Button onClick={() => alert('Hi')}>Click</Button>;
}
```

---

## 示例（二）：pnpm + Turborepo

安装 `turbo` 后新增 `turbo.json`：

```json
{
  "$schema": "https://turbo.build/schema.json",
  "pipeline": {
    "build": { "dependsOn": ["^build"], "outputs": ["dist/**", ".next/**"] },
    "test": { "dependsOn": ["^build"], "outputs": [] },
    "dev": { "cache": false }
  }
}
```

根脚本：

```json
{
  "scripts": {
    "build": "turbo run build",
    "test": "turbo run test",
    "dev": "turbo run dev --parallel"
  }
}
```

优势：声明式任务图、内置缓存与远程缓存（可接入 Vercel Remote Cache）。

---

## 示例（三）：pnpm + Nx（可选）

`nx.json` 与 `project.json` 以包为单位描述任务；开启影响分析：

```json
// nx.json
{
  "extends": "nx/presets/npm.json",
  "tasksRunnerOptions": { "default": { "runner": "nx/tasks-runners/default" } }
}
```

命令示例：

```
npx nx graph            # 交互式依赖图
npx nx affected -t build # 仅构建受影响的包
```

---

## TypeScript 与边界治理

- 基线配置：根放 `tsconfig.base.json`，packages 继承；开启 `strict`、`composite`（便于增量、引用）。
- 路径别名：仅在公共入口导出，禁止跨包内部路径导入。
- ESLint 边界：使用 `import/no-restricted-paths` 或 `eslint-plugin-boundaries` 限制导入层级。

示例 `packages/ui/tsconfig.build.json`：

```json
{
  "extends": "../../tsconfig.base.json",
  "compilerOptions": { "outDir": "dist", "declaration": true, "composite": true },
  "include": ["src/**/*"]
}
```

---

## 版本与发布（Changesets）

选择策略：

- 固定版本（fixed）：所有包同版本；对应用型 Monorepo 友好。
- 独立版本（independent）：库型更常见；改动最小化发布。

快速上手：

```
pnpm add -D @changesets/cli
npx changeset init
```

生成变更：

```
npx changeset        # 选择受影响的包与语义版本
npx changeset version # 生成新版本并更新依赖range
npx changeset publish # 发布到 npm（或内部 registry）
```

CI 提示：在 `main` 合并后由机器人 PR 触发 `version/publish`，保证可追溯。

---

## CI/CD 与增量构建

GitHub Actions（Turbo 示例）：

```yaml
name: ci
on: [push, pull_request]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
        with: { version: 9 }
      - uses: actions/setup-node@v4
        with: { node-version: 22, cache: 'pnpm' }
      - run: pnpm install --frozen-lockfile
      - run: pnpm build    # turbo run build（增量 + 缓存）
      - run: pnpm test
```

要点：

- 任务图声明清晰，避免“脚本地狱”。
- 使用远程缓存可显著提速 PR 校验。

---

## 独立部署与内部包策略（重要）

Monorepo 中的每个 app 都可以“独立部署”。packages 目录下的 `types`、`zod` schema、UI 组件等作为依赖参与构建/打包即可。关键在于“如何把内部包带进部署产物”。

两种常见模式：

- 构建期内联（推荐）
  - 在 CI/平台以整个 monorepo 为上下文安装依赖并构建。
  - app 构建时将 `packages/*` 依赖编译并打包进产物（或按需 external）。
  - Vercel/Netlify/Cloudflare Pages 等都支持 monorepo 项目根安装 + 子目录构建。

- 私有仓库发布
  - 用 Changesets 将内部包发布到私有 npm/内部 registry。
  - app 以普通依赖的形式引入已发布的包，可在完全隔离环境中独立构建/部署。

Next.js app 的关键配置（源码依赖需转译）：

```js
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  transpilePackages: ['@acme/ui', '@acme/api-sdk'],
  experimental: { externalDir: true },
};
module.exports = nextConfig;
```

如果内部包输出构建物（dist），请在包内声明 `exports` 指向 dist，并在根流水线中先构建 packages：

```json
// packages/ui/package.json（片段）
{
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" } },
  "scripts": { "build": "tsc -p tsconfig.build.json" }
}
```

Vercel 部署（示例）：

- Project Root: `apps/web`
- Install Command: `pnpm install`（在仓库根执行）
- Build Command: `pnpm -C apps/web build`（或保持默认）

按 app 限定依赖安装/构建（CI 加速）：

```
pnpm install --filter ./apps/web...
pnpm -r --filter ./packages/* build
pnpm -C apps/web build
```

注意与避坑：

- 源码未转译 → 使用 `transpilePackages`，或让包产出 dist 并通过 `exports` 暴露入口。
- 跨包导入内部文件 → 严禁使用相对路径跨越包边界，只通过公共入口导出。
- Edge 运行时约束 → 内部包避免 Node 专用 API；必要时通过条件导出区分 browser/node。
- peerDependencies → 包若声明了 peer（如 react），app 需显式安装对应版本。

---

## 实战建议与避坑

- 包边界先定律：`packages/ui` 不依赖 Next；`api-sdk` 不依赖具体运行时。
- 第二次复用再抽象：避免过早把实现上提到 shared。
- 保持“受影响分析”有效：适度拆包，避免大而全的巨型包。
- 落地 ADR：记录架构决策（工具、版本策略、边界规范）。

---

## 小结

- 选型从简单开始：pnpm → Turbo → Nx，逐步引入复杂度。
- 用 Changesets 管版本与发布，用 Turbo/Nx 管任务与增量。
- 清晰的包边界 + 统一工程化，让多人协作与长期演进更稳。