2801 字
14 分钟
Webpack Monorepo 完整指南:理论与实战

Webpack Monorepo 完整指南:理论与实战#

本文档详细讲解如何使用 Webpack 构建和管理 Monorepo 项目, 包含完整的理论、配置方案、最佳实践和真实案例。


第一章:什么是 Monorepo?#

1.1 Monorepo 的定义#

Monorepo(单一仓库)是什么?

在一个 Git 仓库中管理多个相关的项目/包。

传统方式(多仓库 Multi-repo):
warehouse-ui/          ← 独立仓库 1
├── src/
└── package.json

warehouse-api/         ← 独立仓库 2
├── src/
└── package.json

warehouse-utils/       ← 独立仓库 3
├── src/
└── package.json

问题:
- 管理困难
- 依赖同步复杂
- 跨包共享代码麻烦

---

Monorepo 方式(单一仓库):
warehouse/             ← 一个仓库
├── packages/
│   ├── ui/            ← 包 1
│   ├── api/           ← 包 2
│   ├── utils/         ← 包 3
│   └── common/        ← 公共包
├── package.json
└── .git/

优势:
- 统一管理
- 共享代码容易
- 依赖版本一致
- 原子性更新

1.2 Monorepo 的核心优势#

// 优势 1:代码共享
packages/utils/
└── src/
    └── helpers.ts  // 所有包都可以使用

packages/ui/
└── src/
    └── Button.tsx
    // 可以直接 import { helpers } from '@warehouse/utils'

// 优势 2:依赖版本一致
// 所有包共享 node_modules,保证版本一致

// 优势 3:原子性更新
// 多个包一起更新,不会出现不兼容

// 优势 4:统一构建和测试
// 可以一次性构建所有包

1.3 Monorepo 的应用场景#

✅ 适合 Monorepo:
  - 大型项目,由多个相关包组成
  - UI 组件库 + 应用 + 文档
  - 核心库 + 插件库 + 应用
  - 前端 + 后端 + 共享工具
  - 公司内部多个产品共享代码

❌ 不适合 Monorepo:
  - 完全独立的项目
  - 需要独立发版和版本控制
  - 包之间无共享代码
  - 团队很小,项目很简单

第二章:Monorepo 的实现方案#

2.1 主流的 Monorepo 工具#

// 方案 1:Yarn Workspaces(推荐,最简单)
// 优点:简单易用,官方支持
// 缺点:功能相对基础

// 方案 2:Npm Workspaces(Npm 7+)
// 优点:npm 原生支持,无需额外工具
// 缺点:功能不如 Yarn

// 方案 3:Lerna + Yarn Workspaces(推荐,功能完整)
// 优点:完整的 Monorepo 解决方案
// 缺点:配置稍复杂

// 方案 4:Nx(企业级,功能最强)
// 优点:功能完整,缓存,任务编排
// 缺点:学习成本高,配置复杂

// 方案 5:Turborepo(新兴,性能最好)
// 优点:性能超快,配置简单
// 缺点:相对较新,社区还在增长

本文档重点:Webpack + Yarn Workspaces + Lerna

2.2 Monorepo 的项目结构#

warehouse/                      # 根项目
├── packages/                   # 所有包的目录
│   ├── @warehouse/ui/         # 包 1:UI 组件库
│   │   ├── src/
│   │   ├── dist/
│   │   ├── package.json
│   │   └── webpack.config.js
│   ├── @warehouse/api/        # 包 2:API 层
│   │   ├── src/
│   │   ├── dist/
│   │   ├── package.json
│   │   └── webpack.config.js
│   ├── @warehouse/utils/      # 包 3:工具函数
│   │   ├── src/
│   │   ├── dist/
│   │   ├── package.json
│   │   └── webpack.config.js
│   └── @warehouse/app/        # 包 4:主应用
│       ├── src/
│       ├── dist/
│       ├── package.json
│       └── webpack.config.js
├── .gitignore
├── .npmrc                      # npm 配置
├── lerna.json                  # Lerna 配置
├── package.json                # 根项目配置
├── webpack.config.js           # 共享的 webpack 配置
└── tsconfig.json               # 共享的 TypeScript 配置

第三章:使用 Yarn Workspaces 构建 Monorepo#

3.1 初始化项目#

# 1. 创建项目目录
mkdir warehouse
cd warehouse

# 2. 初始化 npm
npm init -y

# 3. 配置 yarn workspaces
# 修改 package.json

3.2 根项目的 package.json#

{
  "name": "warehouse",
  "version": "1.0.0",
  "private": true,
  "workspaces": [
    "packages/*"
  ],
  "devDependencies": {
    "webpack": "^5.88.0",
    "webpack-cli": "^5.1.4",
    "webpack-dev-server": "^4.15.1",
    "@babel/core": "^7.22.0",
    "@babel/preset-env": "^7.22.0",
    "@babel/preset-react": "^7.22.0",
    "@babel/preset-typescript": "^7.22.0",
    "babel-loader": "^9.1.2",
    "typescript": "^5.0.0",
    "lerna": "^6.0.0"
  }
}

3.3 创建包结构#

# 创建包目录
mkdir -p packages/@warehouse/ui
mkdir -p packages/@warehouse/api
mkdir -p packages/@warehouse/utils
mkdir -p packages/@warehouse/app

# 为每个包初始化 package.json
cd packages/@warehouse/ui
npm init -y
# 修改 package.json...

cd ../api
npm init -y
# 修改 package.json...

# 以此类推

3.4 各包的 package.json#

packages/@warehouse/utils/package.json

{
  "name": "@warehouse/utils",
  "version": "1.0.0",
  "description": "Shared utilities",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "scripts": {
    "build": "webpack --mode production",
    "dev": "webpack --mode development --watch"
  },
  "peerDependencies": {},
  "devDependencies": {}
}

packages/@warehouse/ui/package.json

{
  "name": "@warehouse/ui",
  "version": "1.0.0",
  "description": "UI Component Library",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "scripts": {
    "build": "webpack --mode production",
    "dev": "webpack --mode development --watch"
  },
  "dependencies": {
    "@warehouse/utils": "1.0.0",
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  },
  "peerDependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  }
}

packages/@warehouse/app/package.json

{
  "name": "@warehouse/app",
  "version": "1.0.0",
  "description": "Main Application",
  "scripts": {
    "start": "webpack serve --mode development",
    "build": "webpack --mode production"
  },
  "dependencies": {
    "@warehouse/ui": "1.0.0",
    "@warehouse/api": "1.0.0",
    "@warehouse/utils": "1.0.0",
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  }
}

3.5 Webpack 配置#

packages/@warehouse/utils/webpack.config.js

const path = require('path')

module.exports = {
  mode: 'production',
  entry: './src/index.ts',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'index.js',
    libraryTarget: 'commonjs2'
  },
  resolve: {
    extensions: ['.ts', '.tsx', '.js']
  },
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: 'babel-loader',
        exclude: /node_modules/
      }
    ]
  },
  externals: {
    // 不打包 react,让使用者提供
    react: 'react',
    'react-dom': 'react-dom'
  }
}

packages/@warehouse/ui/webpack.config.js

const path = require('path')

module.exports = {
  mode: 'production',
  entry: './src/index.ts',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'index.js',
    libraryTarget: 'commonjs2'
  },
  resolve: {
    extensions: ['.ts', '.tsx', '.js'],
    alias: {
      '@warehouse/utils': path.resolve(__dirname, '../utils/src')
    }
  },
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: 'babel-loader',
        exclude: /node_modules/
      },
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader']
      }
    ]
  },
  externals: {
    react: 'react',
    'react-dom': 'react-dom'
  }
}

packages/@warehouse/app/webpack.config.js

const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  mode: 'production',
  entry: './src/index.tsx',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].[contenthash:8].js'
  },
  devServer: {
    port: 3000,
    open: true,
    historyApiFallback: true
  },
  resolve: {
    extensions: ['.ts', '.tsx', '.js'],
    alias: {
      '@warehouse/utils': path.resolve(__dirname, '../utils/src'),
      '@warehouse/ui': path.resolve(__dirname, '../ui/src'),
      '@warehouse/api': path.resolve(__dirname, '../api/src')
    }
  },
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: 'babel-loader',
        exclude: /node_modules/
      },
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader']
      }
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './src/index.html'
    })
  ]
}

第四章:使用 Lerna 管理 Monorepo#

4.1 Lerna 是什么?#

// Lerna 提供了:
// 1. 统一的版本管理
// 2. 自动化的发布流程
// 3. 依赖关系管理
// 4. 批量命令执行

// 虽然 Yarn Workspaces 可以处理 node_modules,
// 但 Lerna 提供了更高级的功能

4.2 初始化 Lerna#

# 安装 Lerna(已经在根项目的 devDependencies 中)
npm install -D lerna

# 初始化 Lerna
npx lerna init

# 此时会生成 lerna.json

4.3 lerna.json 配置#

{
  "version": "1.0.0",
  "npmClient": "yarn",
  "useWorkspaces": true,
  "packages": [
    "packages/*"
  ],
  "command": {
    "publish": {
      "ignoreChanges": [
        "*.md"
      ],
      "message": "chore(release): publish %s"
    }
  }
}

4.4 常用 Lerna 命令#

# 列出所有包
lerna list

# 批量安装依赖
lerna bootstrap

# 在所有包中执行命令
lerna exec npm run build

# 运行 npm script
lerna run build

# 发布包
lerna publish

# 检查文件变化
lerna changed

# 清理所有 dist 目录
lerna exec -- rm -rf dist

4.5 Monorepo 的根项目 npm scripts#

{
  "scripts": {
    "bootstrap": "lerna bootstrap",
    "build": "lerna run build",
    "dev": "lerna run dev --stream",
    "test": "lerna run test",
    "clean": "lerna exec -- rm -rf dist node_modules",
    "publish": "lerna publish"
  }
}

第五章:包之间的依赖管理#

5.1 本地依赖 vs npm 依赖#

// 方式 1:直接引用本地源代码(开发时)
// webpack.config.js 中使用 alias
resolve: {
  alias: {
    '@warehouse/utils': path.resolve(__dirname, '../utils/src')
  }
}

// 方式 2:在 package.json 中声明依赖(生产时)
{
  "dependencies": {
    "@warehouse/utils": "1.0.0"  // 指向本地包
  }
}

// Yarn Workspaces 会自动链接本地包

5.2 处理循环依赖#

// ❌ 循环依赖的情况(要避免)
// @warehouse/ui 依赖 @warehouse/utils
// @warehouse/utils 又依赖 @warehouse/ui
// → 循环依赖!

// 解决方案:重新设计架构
// @warehouse/common   // 新建基础包
// ├─ types.ts
// └─ constants.ts

// @warehouse/utils    // 工具函数
// └─ 依赖 @warehouse/common

// @warehouse/ui       // UI 组件
// └─ 依赖 @warehouse/common 和 @warehouse/utils

// 这样就形成了清晰的依赖树,避免了循环依赖

5.3 共享配置#

根项目下创建 shared-config 包

packages/
└── @warehouse/shared-config/
    ├── webpack.config.base.js  # 共享的 webpack 配置
    ├── tsconfig.json           # 共享的 TypeScript 配置
    ├── babel.config.js         # 共享的 Babel 配置
    └── package.json

packages/@warehouse/shared-config/webpack.config.base.js

const path = require('path')

module.exports = {
  resolve: {
    extensions: ['.ts', '.tsx', '.js'],
  },
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: [
              '@babel/preset-env',
              '@babel/preset-react',
              '@babel/preset-typescript'
            ]
          }
        },
        exclude: /node_modules/
      },
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader']
      }
    ]
  }
}

各包使用共享配置

// packages/@warehouse/ui/webpack.config.js
const baseConfig = require('@warehouse/shared-config/webpack.config.base')
const path = require('path')
const { merge } = require('webpack-merge')

module.exports = merge(baseConfig, {
  mode: 'production',
  entry: './src/index.ts',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'index.js',
    libraryTarget: 'commonjs2'
  }
})

第六章:构建流程#

6.1 完整的构建流程#

# 1. 安装依赖(一次性)
npm install
# 或
yarn install

# 2. 启动开发模式(监听所有包的变化)
npm run dev

# 3. 构建所有包
npm run build

# 4. 测试
npm run test

# 5. 发布
npm run publish

6.2 开发工作流#

// 开发时的流程:

// 1. 修改 @warehouse/utils 的代码
packages/@warehouse/utils/src/helpers.ts
// ↓ 自动重新编译

// 2. 修改 @warehouse/ui 的代码
packages/@warehouse/ui/src/Button.tsx
// ↓ 自动重新编译

// 3. 修改 @warehouse/app 的代码
packages/@warehouse/app/src/App.tsx
// ↓ webpack-dev-server 热更新

// 所有修改立即反映到浏览器中

6.3 构建输出#

warehouse/
└── packages/
    ├── @warehouse/utils/
    │   └── dist/
    │       ├── index.js      # 已编译
    │       └── index.d.ts    # 类型定义
    ├── @warehouse/ui/
    │   └── dist/
    │       ├── index.js
    │       └── index.d.ts
    ├── @warehouse/api/
    │   └── dist/
    │       ├── index.js
    │       └── index.d.ts
    └── @warehouse/app/
        └── dist/
            ├── index.html
            ├── main.abc123de.js
            └── ...

第七章:实战案例:完整的 UI 组件库 Monorepo#

7.1 项目结构#

component-library/
├── packages/
│   ├── @lib/core/           # 核心工具
│   ├── @lib/utils/          # 工具函数
│   ├── @lib/styles/         # 共享样式
│   ├── @lib/components/     # UI 组件
│   ├── @lib/docs/           # 文档站点
│   └── @lib/examples/       # 示例应用
├── lerna.json
├── package.json
└── README.md

7.2 核心包:@lib/components#

packages/@lib/components/src/Button/index.tsx

import React from 'react'
import { getClassName } from '@lib/utils'
import styles from './Button.module.css'

interface ButtonProps {
  variant?: 'primary' | 'secondary'
  size?: 'small' | 'medium' | 'large'
  children: React.ReactNode
  onClick?: () => void
}

export const Button: React.FC<ButtonProps> = ({
  variant = 'primary',
  size = 'medium',
  children,
  onClick
}) => {
  const className = getClassName(
    styles.button,
    styles[variant],
    styles[size]
  )

  return (
    <button className={className} onClick={onClick}>
      {children}
    </button>
  )
}

packages/@lib/components/src/index.ts

export { Button } from './Button'
export { Input } from './Input'
export { Select } from './Select'
// ... 其他组件

7.3 工具包:@lib/utils#

packages/@lib/utils/src/index.ts

export const getClassName = (...classes: (string | undefined)[]) => {
  return classes.filter(Boolean).join(' ')
}

export const debounce = (fn: Function, delay: number) => {
  let timeout: NodeJS.Timeout
  return (...args: any[]) => {
    clearTimeout(timeout)
    timeout = setTimeout(() => fn(...args), delay)
  }
}

export const throttle = (fn: Function, delay: number) => {
  let lastTime = 0
  return (...args: any[]) => {
    const now = Date.now()
    if (now - lastTime >= delay) {
      fn(...args)
      lastTime = now
    }
  }
}

7.4 文档应用:@lib/docs#

packages/@lib/docs/src/App.tsx

import React from 'react'
import { Button, Input, Select } from '@lib/components'

export default function DocsApp() {
  return (
    <div>
      <h1>Component Library</h1>
      
      <section>
        <h2>Button</h2>
        <Button variant="primary">Primary Button</Button>
        <Button variant="secondary">Secondary Button</Button>
      </section>

      <section>
        <h2>Input</h2>
        <Input placeholder="Enter text" />
      </section>
    </div>
  )
}

第八章:最佳实践#

8.1 包的命名规范#

// ✅ 好的命名规范
@warehouse/ui           // 公司名/功能名
@warehouse/api-client   // 使用 kebab-case
@warehouse/utils        // 简洁清晰

// ❌ 不好的命名
warehouse-ui            // 没有作用域
WarehouseUI             // 不用 PascalCase
warehouse_utils         // 不用 snake_case

8.2 版本管理#

# 固定版本(推荐)
# 所有包版本相同,一起发布
lerna publish --exact

# 独立版本
# 每个包可以有不同的版本
lerna publish --independent

8.3 CI/CD 集成#

# .github/workflows/build.yml
name: Build and Test

on: [push, pull_request]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-node@v2
        with:
          node-version: '16'
      - run: npm install
      - run: npm run build
      - run: npm run test

8.4 常见问题解决#

// 问题 1:依赖没有被正确链接
// 解决方案:
yarn install --force
# 或
lerna bootstrap --force-local

// 问题 2:循环依赖
// 解决方案:重新设计架构,避免循环引用

// 问题 3:TypeScript 路径别名不工作
// 解决方案:配置 tsconfig.json
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@warehouse/*": ["./packages/@warehouse/*/src"]
    }
  }
}

第九章:Monorepo 的优化和性能#

9.1 构建缓存#

// webpack.config.js
module.exports = {
  cache: {
    type: 'filesystem',
    buildDependencies: {
      config: [__filename]
    }
  }
}

9.2 增量构建#

# 只构建改变的包
lerna run build --since origin/main

# 构建特定包
lerna run build --scope @warehouse/ui

9.3 并行执行#

# 默认:顺序执行
lerna run build

# 并行执行(最多 4 个并发)
lerna run build --concurrency 4

第十章:总结#

核心要点#

  1. Monorepo 是什么:在一个仓库中管理多个相关的包
  2. 为什么用 Monorepo:代码共享、依赖一致、原子更新
  3. 如何实现:Yarn Workspaces + Lerna
  4. 关键配置:webpack alias、package.json dependencies、tsconfig paths
  5. 最佳实践:清晰的依赖图、避免循环依赖、共享配置

学习路线#

1. 理解 Monorepo 概念 ✓
2. 初始化 Yarn Workspaces ✓
3. 配置 Webpack 为每个包 ✓
4. 设置 Lerna 版本管理 ✓
5. 实施最佳实践 ✓

何时使用 Monorepo#

✅ 适合:
  - UI 组件库 + 应用 + 文档
  - 核心库 + 多个应用
  - 相关的多个项目

❌ 不适合:
  - 完全独立的项目
  - 需要独立版本控制
  - 很小的项目

现在你已经掌握了 Webpack Monorepo 的完整知识! 🎉

Monorepo 是大型项目的强大工具,可以大大提升开发效率。

接下来我们继续学习 Vite 吧! 🚀

Webpack Monorepo 完整指南:理论与实战
https://fuwari.vercel.app/posts/webpack-monorepo-完整指南理论与实战/
作者
Kellen
发布于
2026-02-06
许可协议
CC BY-NC-SA 4.0