pnpm Workspaces:前端 monorepo 实战

pnpm Workspaces:前端 monorepo 实战

monorepo 在前端圈越来越流行。pnpm 的 workspaces 功能是目前最轻量的 monorepo 方案,不需要学 Nx 或 Turborepo 那套复杂的工具链。

为什么选 monorepo

在前端项目里,monorepo 的典型场景:

  • 前端应用 + 后端 API 在同一个仓库
  • 多个应用共享组件库、工具库
  • 微前端项目,多个子应用共享依赖

好处:

  • 代码共享方便:直接 import,不用发包
  • 依赖版本统一:所有子项目用同一个 React 版本
  • 重构安全:改了共享库,所有使用方一起改,不会遗漏
  • CI/CD 统一:一个仓库一套流水线

pnpm workspace 基础配置

目录结构

my-project/
  pnpm-workspace.yaml    # workspace 配置
  package.json            # 根 package.json
  packages/
    apps/
      web/                # 前端应用
      admin/              # 管理后台
    libs/
      ui/                 # UI 组件库
      utils/              # 工具函数
      types/              # 类型定义

pnpm-workspace.yaml

packages:
  - 'packages/*'
  - 'packages/apps/*'
  - 'packages/libs/*'

这告诉 pnpm 哪些目录是 workspace 的成员。

根 package.json

{
  "name": "my-monorepo",
  "private": true,
  "scripts": {
    "dev:web": "pnpm --filter web dev",
    "dev:admin": "pnpm --filter admin dev",
    "build": "pnpm -r build",
    "lint": "pnpm -r lint",
    "test": "pnpm -r test"
  },
  "devDependencies": {
    "typescript": "^5.0.0",
    "eslint": "^9.0.0"
  }
}

--filter 指定在哪个子项目运行命令。-r 表示递归,在所有子项目运行。

工作区依赖

引用 workspace 包

// packages/apps/web/package.json
{
  "dependencies": {
    "@my/ui": "workspace:*",
    "@my/utils": "workspace:*",
    "@my/types": "workspace:*"
  }
}

workspace:* 表示使用 workspace 里的最新版本。pnpm 会创建符号链接,不用真正安装。

引用 workspace 包的类型

TypeScript 配置:

// tsconfig.json(根配置)
{
  "references": [
    { "path": "packages/libs/ui" },
    { "path": "packages/libs/utils" },
    { "path": "packages/libs/types" }
  ]
}
// packages/libs/ui/tsconfig.json
{
  "compilerOptions": {
    "composite": true,  // 启用项目引用
    "outDir": "./dist"
  }
}

这样跨包引用时,TypeScript 可以正确推导类型。

常用命令

# 在 web 应用启动开发服务器
pnpm --filter web dev

# 在所有子项目安装依赖
pnpm install

# 在所有子项目运行 build
pnpm -r build

# 按拓扑顺序构建(依赖先构建)
pnpm -r --topological-sort build

# 给指定包添加依赖
pnpm --filter web add react

# 给所有子项目添加依赖
pnpm -r add eslint -D

# 查看 workspace 的依赖关系
pnpm list --depth 0 -r

依赖提升和幽灵依赖

pnpm 使用内容寻址存储和符号链接。每个包只能看到自己声明的依赖(和 npm/yarn 不同)。

// 如果 web 包没有声明 lodash,即使它安装在了 node_modules 里也无法引入
import _ from 'lodash'; // 报错:Cannot find module 'lodash'

这是好事,避免了"幽灵依赖"问题。你需要什么就在 package.json 里声明什么。

如果确实需要在 workspace 里共享一个没有在 package.json 里声明的依赖(比如构建工具),可以用 .npmrc

# .npmrc
public-hoist-pattern[]=*eslint*
public-hoist-pattern[]=*prettier*

版本管理

monorepo 里的包版本管理可以用 changeset

# 安装
pnpm add -Dw @changesets/cli

# 初始化
pnpm changeset init

# 创建变更记录
pnpm changeset
# 交互式选择:哪些包变了,变更是 major/minor/patch

# 版本更新
pnpm changeset version

# 生成 changelog
pnpm changeset publish

构建缓存

monorepo 最大的痛点是构建速度。pnpm 本身没有提供构建缓存,但可以配合 Turborepo 使用:

# 安装 Turborepo
pnpm add -Dw turbo

# turbo.json
{
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**"]
    },
    "dev": {
      "cache": false,
      "persistent": true
    }
  }
}

Turborepo 会缓存构建结果,如果源码没变就跳过构建。在大型 monorepo 里效果很明显。

不需要 monorepo 的情况

monorepo 有成本——配置复杂、CI 变慢、git 历史混乱。以下情况不建议用:

  1. 单项目:一个前端应用,不需要拆分
  2. 团队很小:2-3 人的团队,monorepo 的管理成本大于收益
  3. 项目之间没有关联:只是图方便放在一起

monorepo 适合的是有一定规模、多个关联项目、需要共享代码的团队。如果只是个人博客或者简单的前端项目,单仓库就够了。

pnpm workspace 是最简单的 monorepo 方案,比 Lerna 和 Nx 轻量很多。对于中小型前端 monorepo 来说,pnpm workspace + changeset 就够了。

TypeScriptOther
返回首页