从零搭建 Monorepo 项目:Vue Ace Admin 架构实践指南

约 9 分钟阅读

前言

Monorepo 架构已经成为现代前端项目的主流组织方式,它能够有效解决代码复用、依赖管理和开发效率等问题。本文将以 Vue Ace Admin 项目为例,详细介绍如何从零搭建一个 Monorepo 项目。

如果你对 Monorepo 架构还不熟悉,建议先阅读 Monorepo 架构总览 了解基本概念。

第一步:项目初始化

1.1 创建项目目录

bash
mkdir vue-ace-admin
cd vue-ace-admin

1.2 初始化根 package.json

bash
pnpm init

编辑 package.json

json
{
  "name": "vue-ace-admin",
  "version": "1.0.0",
  "private": true,
  "type": "module",
  "packageManager": "pnpm@10.14.0",
  "scripts": {
    "dev": "vite",
    "build": "pnpm run build:hooks && pnpm run build:ui && vite build",
    "build:hooks": "pnpm -C packages/hooks build",
    "build:ui": "pnpm -C packages/ui build"
  }
}

关键配置说明:

  • private: true:防止意外发布根包
  • type: "module":使用 ES 模块
  • packageManager:锁定 pnpm 版本

第二步:配置 pnpm Workspace

2.1 创建 pnpm-workspace.yaml

在项目根目录创建 pnpm-workspace.yaml

yaml
packages:
  - 'packages/*'
  - 'docs'

配置说明:

  • packages/*:匹配所有 packages/ 下的子目录
  • docs:文档站点也是独立的包

2.2 验证 Workspace 配置

bash
pnpm install

如果配置正确,pnpm 会识别所有 workspace 包。

第三步:创建包结构

3.1 创建 Hooks 包

bash
mkdir -p packages/hooks/src
cd packages/hooks
pnpm init

编辑 packages/hooks/package.json

json
{
  "name": "@codexlin/ace-admin-hooks",
  "version": "1.0.0",
  "description": "Pure Vue 3 Composition API Hooks",
  "type": "module",
  "main": "./dist/index.js",
  "module": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.js"
    }
  },
  "files": ["dist"],
  "peerDependencies": {
    "vue": "^3.0.0"
  },
  "scripts": {
    "build": "vite build"
  }
}

关键配置:

  • name:使用 scoped 包名,避免命名冲突
  • peerDependencies:声明 Vue 为 peer 依赖,避免重复安装
  • files:只发布 dist 目录
  • exports:定义包的入口点

3.2 创建 UI 组件库包

bash
mkdir -p packages/ui/src
cd packages/ui
pnpm init

编辑 packages/ui/package.json

json
{
  "name": "@codexlin/ace-admin-ui",
  "version": "0.3.0",
  "description": "Production-ready Vue 3 component library",
  "type": "module",
  "main": "./dist/ace-admin-ui.umd.js",
  "module": "./dist/ace-admin-ui.es.js",
  "types": "./dist/types/index.d.ts",
  "style": "./dist/ace-admin-ui.css",
  "exports": {
    ".": {
      "types": "./dist/types/index.d.ts",
      "import": "./dist/ace-admin-ui.es.js",
      "require": "./dist/ace-admin-ui.umd.js"
    },
    "./dist/ace-admin-ui.css": "./dist/ace-admin-ui.css"
  },
  "files": ["dist"],
  "peerDependencies": {
    "ant-design-vue": ">=4.0.0",
    "vue": ">=3.4.0"
  },
  "scripts": {
    "build": "vite build"
  }
}

关键配置:

  • main:UMD 格式入口(浏览器直接使用)
  • module:ESM 格式入口(现代构建工具使用)
  • types:TypeScript 类型定义
  • style:CSS 样式文件入口

第四步:配置 TypeScript

4.1 根目录 tsconfig.json

json
{
  "compilerOptions": {
    "baseUrl": ".",
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "target": "ES2020",
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "strict": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "paths": {
      "@/*": ["./src/*"]
    }
  },
  "include": ["src/**/*", "packages/*/src/**/*"],
  "exclude": ["node_modules", "dist"]
}

4.2 Hooks 包 tsconfig.json

json
{
  "extends": "../../tsconfig.json",
  "compilerOptions": {
    "outDir": "./dist",
    "rootDir": "./src",
    "declaration": true,
    "declarationMap": true,
    "composite": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

关键配置:

  • composite: true:启用项目引用
  • declaration: true:生成类型声明文件
  • extends:继承根配置

4.3 UI 包 tsconfig.json

json
{
  "extends": "../../tsconfig.json",
  "compilerOptions": {
    "outDir": "./dist/types",
    "rootDir": "./src",
    "declaration": true,
    "declarationMap": true,
    "composite": true
  },
  "references": [
    { "path": "../hooks" }
  ],
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

关键配置:

  • references:引用 hooks 包,实现跨包类型检查
  • 当 hooks 包类型变更时,UI 包会自动检测到类型错误

第五步:配置 Vite 构建

5.1 Hooks 包 vite.config.ts

typescript
import { defineConfig } from 'vite'
import dts from 'vite-plugin-dts'

export default defineConfig({
  plugins: [
    dts({
      insertTypesEntry: true,
      copyDtsFiles: true
    })
  ],
  build: {
    lib: {
      entry: 'src/index.ts',
      name: 'AceAdminHooks',
      fileName: 'index',
      formats: ['es']  // 只构建 ESM 格式
    },
    rollupOptions: {
      external: ['vue'],  // 外部化 Vue,不打包
      output: {
        globals: {
          vue: 'Vue'
        }
      }
    }
  }
})

5.2 UI 包 vite.config.ts

typescript
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import dts from 'vite-plugin-dts'
import { resolve } from 'path'

export default defineConfig({
  plugins: [
    vue(),
    vueJsx(),
    dts({
      outDir: 'dist/types',
      insertTypesEntry: true,
      rollupTypes: true
    })
  ],
  build: {
    lib: {
      entry: resolve(__dirname, 'src/index.ts'),
      name: 'AceAdminUi',
      fileName: (format) => `ace-admin-ui.${format}.js`,
      formats: ['es', 'umd']  // 同时构建 ESM 和 UMD
    },
    rollupOptions: {
      external: ['vue', 'ant-design-vue'],
      output: {
        globals: {
          vue: 'Vue',
          'ant-design-vue': 'AntDesignVue'
        },
        assetFileNames: (assetInfo) => {
          if (assetInfo.name?.endsWith('.css')) {
            return 'ace-admin-ui.css'
          }
          return assetInfo.name || 'assets/[name][extname]'
        }
      }
    }
  }
})

关键配置:

  • lib:库模式构建配置
  • external:外部化依赖,减小包体积
  • formats:输出格式(ESM 用于构建工具,UMD 用于浏览器)

第六步:创建包入口文件

6.1 Hooks 包入口

packages/hooks/src/index.ts

typescript
export { useList } from './useList'
export { usePagination } from './usePagination'
export { useDebouncedRef } from './useDebouncedRef'
export { useLoading } from './useLoading'

// 导出类型
export type { UseListOptions, UseListResult } from './useList'
export type { UsePaginationOptions } from './usePagination'

6.2 UI 包入口

packages/ui/src/index.ts

typescript
import type { App } from 'vue'
import ProTable from './pro-table/ProTable.vue'
import ProButton from './pro-button/ProButton.vue'

// 导出组件
export { ProTable, ProButton }

// 导出类型
export type { ProTableProps } from './pro-table/type'
export type { ProButtonProps } from './pro-button/type'

// Vue 插件
export default {
  install(app: App) {
    app.component('ProTable', ProTable)
    app.component('ProButton', ProButton)
  }
}

第七步:配置主应用

7.1 根 package.json 添加依赖

json
{
  "dependencies": {
    "@codexlin/ace-admin-hooks": "workspace:*",
    "@codexlin/ace-admin-ui": "workspace:*",
    "vue": "^3.5.0"
  }
}

workspace:* 的作用:

  • 自动链接到本地 packages/ 目录
  • 开发时直接使用源码
  • 修改立即生效,无需重新安装

7.2 主应用使用包

src/main.ts

typescript
import { createApp } from 'vue'
import { ProTable, ProButton } from '@codexlin/ace-admin-ui'
import { useList } from '@codexlin/ace-admin-hooks'
import '@codexlin/ace-admin-ui/dist/ace-admin-ui.css'

const app = createApp(App)
app.use(ProTable)
app.mount('#app')

第八步:配置 Turbo(可选)

8.1 安装 Turbo

bash
pnpm add -D turbo

8.2 创建 turbo.json

json
{
  "$schema": "https://turbo.build/schema.json",
  "tasks": {
    "build:hooks": {
      "outputs": ["packages/hooks/dist/**"],
      "cache": true
    },
    "build:ui": {
      "dependsOn": ["build:hooks"],
      "outputs": ["packages/ui/dist/**"],
      "cache": true
    },
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**"]
    }
  }
}

Turbo 优势:

  • 依赖管理dependsOn 确保构建顺序
  • 增量构建:只构建变更的包
  • 缓存机制:未变更的包使用缓存

项目结构最佳实践

推荐的目录结构

text
vue-ace-admin/
├── packages/
│   ├── hooks/              # 纯逻辑包
│   │   ├── src/
│   │   ├── package.json
│   │   ├── tsconfig.json
│   │   └── vite.config.ts
│   │
│   └── ui/                 # UI 组件库
│       ├── src/
│       │   ├── components/
│       │   └── index.ts
│       ├── package.json
│       ├── tsconfig.json
│       └── vite.config.ts
│
├── src/                    # 主应用
├── docs/                   # 文档站点
├── pnpm-workspace.yaml     # Workspace 配置
├── turbo.json              # Turbo 配置(可选)
├── tsconfig.json           # 根 TypeScript 配置
└── package.json            # 根 package.json

命名规范

包命名:

  • 使用 scoped 包名:@your-org/package-name
  • 避免命名冲突
  • 便于识别包的作用域

目录命名:

  • packages/:所有包放在这里
  • src/:主应用代码
  • dist/:构建输出(添加到 .gitignore)

常见问题与解决方案

Q1: workspace 包无法识别

问题: pnpm 无法识别 workspace 包

解决方案:

  1. 检查 pnpm-workspace.yaml 配置
  2. 确保每个包都有 package.json
  3. 运行 pnpm install 重新安装

Q2: TypeScript 类型错误

问题: 跨包类型检查失败

解决方案:

  1. 确保 tsconfig.jsoncomposite: true
  2. 使用 references 引用依赖包
  3. 运行 pnpm build 生成类型文件

Q3: 构建顺序问题

问题: UI 包构建时找不到 hooks 包

解决方案:

  1. 使用 Turbo 的 dependsOn 配置
  2. 或手动指定构建顺序:pnpm build:hooks && pnpm build:ui

总结

通过以上步骤,你已经成功搭建了一个基础的 Monorepo 项目。关键要点:

  1. pnpm workspace:实现包之间的依赖管理
  2. TypeScript 项目引用:实现跨包类型检查
  3. Vite 库模式:构建可发布的包
  4. Turbo:优化构建性能

下一步,你可以:

如果你对 Vue 3 企业级工程实践 感兴趣,可以查看我们的 架构实践分类 下的其他文章。

相关文章