Monorepo + pnpm workspace 落地实操:Vue 中后台多项目 / 组件库 / 公共包管理|Vue 工程化篇

本文基于 pnpm workspace 实现 Monorepo 架构,从核心设计到落地实操,手把手教你搭建 Vue3 + Vite 中后台多项目、组件库与公共包统一管理方案,解决 Vue 依赖、热更新、循环依赖、TS 类型等工程化高频问题。

📑 文章目录

同学们好,我是 Eugene(尤金),一个拥有多年中后台开发经验的前端工程师~

(Eugene 发音很简单,/juːˈdʒiːn/,大家怎么顺口怎么叫就好)

你是否也有过:明明学过很多技术,一到关键时候却讲不出来、甚至写不出来?

你是否也曾怀疑自己,是不是太笨了,明明感觉会,却总差一口气?

就算想沉下心从头梳理,可工作那么忙,回家还要陪伴家人。

一天只有24小时,时间永远不够用,常常感到力不从心。

技术行业,本就是逆水行舟,不进则退。

如果你也有同样的困扰,别慌。

从现在开始,跟着我一起心态归零 ,利用碎片时间,来一次彻彻底底的基础扫盲

这一次,我们一起慢慢来,扎扎实实变强。

不搞花里胡哨的理论堆砌,只分享看得懂、用得上的前端干货,

咱们一起稳步积累,真正摆脱「面向搜索引擎写代码」的尴尬。


一、开篇:Monorepo 是什么?什么时候用?

一句话解释:Monorepo 是把多个相关项目放在同一个仓库里,通过统一工具管理依赖和构建的一种组织方式。

和 Multirepo 的区别

维度 Monorepo Multirepo
仓库数量 一个仓库多个包 多个仓库多个包
依赖共享 容易,直接 workspace 引用 需要发 npm 或 git submodule
联调改公共代码 改完立刻生效 要发版、再安装
权限隔离 同一仓库,难精细控制 可按项目拆仓库

适合用 Monorepo 的典型场景

  • 多个中后台(管理端 A、管理端 B、运营端等)共用一套组件和工具
  • 公司内部组件库 + 多个业务项目
  • 大量公共包(utils、API、类型定义)被多个项目复用

一句话:多个强关联项目 + 需要频繁联调,就值得考虑 Monorepo。

[⬆ 返回目录](#⬆ 返回目录)


二、场景一:多个中后台项目怎么管?

很多公司会拆成:管理后台、运营后台、客服后台、数据看板等,它们风格接近,但路由、权限、业务不同。

传统做法:每个后台一个仓库

Plain 复制代码
├── admin-a/          # 仓库1
├── admin-b/          # 仓库2
├── admin-c/          # 仓库3
└── common-ui/        # 仓库4(组件库)

问题:改 common-ui 一个按钮,要发版 → 各后台 npm update,联调成本高。

Monorepo 做法 :所有项目放一个仓库,共享 packages 里的公共包。

Plain 复制代码
my-monorepo/
├── apps/
│   ├── admin-a/      # 管理后台 A
│   ├── admin-b/      # 管理后台 B
│   └── dashboard/    # 数据看板
├── packages/
│   ├── ui/           # 组件库
│   ├── utils/        # 公共工具
│   └── api/          # 接口封装
├── pnpm-workspace.yaml
└── package.json

packages/ui 里的组件,各 apps 直接依赖 workspace 包,热更新即可,无需发布。

[⬆ 返回目录](#⬆ 返回目录)


三、场景二:内部组件库怎么抽?

多个后台共用的表格、表单、弹窗、布局等,适合放到 packages/ui

需要解决的问题

  1. 组件库如何被各个 app 引用?
  2. 开发时如何做本地预览(类似 Storybook)?
  3. 如何打包成可发布的 umd/es 产物?

推荐结构:

Plain 复制代码
packages/ui/
├── src/
│   ├── Button/
│   ├── Table/
│   └── index.ts      # 统一导出
├── package.json
├── vite.config.ts    # 或 webpack 配置
└── examples/         # 可选:本地开发示例

每个 app 通过 workspace 引用:

json 复制代码
// apps/admin-a/package.json
{
  "dependencies": {
    "@my/ui": "workspace:*"
  }
}

这样改组件库代码时,各 app 会实时生效,无需发布到 npm。

[⬆ 返回目录](#⬆ 返回目录)


四、场景三:公共包(utils、API、类型)怎么拆?

除组件库外,还有三类常见公共包:

包名 作用 示例
@my/utils 工具函数 日期格式化、防抖、权限判断
@my/api 接口封装 axios 实例、请求拦截、接口方法
@my/types 类型定义 接口返回类型、枚举、业务类型

拆分原则:

  • 工具函数和类型一般无依赖或依赖少,可单独成包
  • API 包可依赖 @my/utils@my/types,形成清晰的依赖链

[⬆ 返回目录](#⬆ 返回目录)


五、工具选型:pnpm workspace + 简单脚本

常见方案:

方案 特点 适合
pnpm workspace 依赖提升、磁盘省空间、配置简单 推荐首选
Yarn workspace 成熟,但 pnpm 更省空间、更快 已有 yarn 的项目
Lerna 老牌,发版能力强 需要复杂发版流程时
Turborepo 增量构建、缓存、并行任务 大型 Monorepo

对中后台场景,pnpm workspace 通常足够:安装快、依赖清晰、配置少。下面示例都用 pnpm。

[⬆ 返回目录](#⬆ 返回目录)


六、从零搭建:完整目录与配置

6.1 目录结构

Plain 复制代码
my-monorepo/
├── apps/
│   ├── admin-a/           # 管理后台 A
│   │   ├── src/
│   │   ├── package.json
│   │   └── vite.config.ts
│   └── admin-b/           # 管理后台 B
│       ├── src/
│       ├── package.json
│       └── vite.config.ts
├── packages/
│   ├── ui/                # 组件库
│   │   ├── src/
│   │   ├── package.json
│   │   └── vite.config.ts
│   ├── utils/             # 工具函数
│   │   ├── src/
│   │   └── package.json
│   ├── api/               # 接口封装
│   │   ├── src/
│   │   └── package.json
│   └── types/             # 类型定义
│       ├── src/
│       └── package.json
├── pnpm-workspace.yaml
├── package.json
├── .npmrc
├── .eslintrc.cjs
└── .prettierrc

[⬆ 返回目录](#⬆ 返回目录)

6.2 根目录 pnpm-workspace.yaml

YAML 复制代码
packages:
  - 'apps/*'
  - 'packages/*'

含义:appspackages 下所有子目录都是 workspace 包。

[⬆ 返回目录](#⬆ 返回目录)

6.3 根目录 package.json

json 复制代码
{
  "name": "my-monorepo",
  "private": true,
  "scripts": {
    "dev:admin-a": "pnpm -C apps/admin-a dev",
    "dev:admin-b": "pnpm -C apps/admin-b dev",
    "dev": "pnpm -r --parallel run dev",
    "build": "pnpm run build:packages && pnpm run build:apps",
    "build:packages": "pnpm -r --filter './packages/*' run build",
    "build:apps": "pnpm -r --filter './apps/*' run build",
    "lint": "eslint . --ext .vue,.ts,.tsx",
    "lint:fix": "eslint . --ext .vue,.ts,.tsx --fix",
    "format": "prettier --write \"**/*.{vue,ts,tsx,js,json}\""
  },
  "devDependencies": {
    "pnpm": "^8.0.0"
  },
  "packageManager": "pnpm@8.15.0"
}

说明:

  • pnpm -C apps/admin-a dev:在 apps/admin-a 下执行 dev
  • pnpm -r run build:在所有 workspace 包中执行 build
  • --filter './packages/*':只对 packages 下包执行

[⬆ 返回目录](#⬆ 返回目录)

6.4 根目录 .npmrc(可选但推荐)

Plain 复制代码
shamefully-hoist=true
strict-peer-dependencies=false

shamefully-hoist:把依赖提升到根 node_modules,避免 Vue 等框架找不到依赖的问题。

[⬆ 返回目录](#⬆ 返回目录)

6.5 包之间的依赖示例

json 复制代码
// packages/utils/package.json
{
  "name": "@my/utils",
  "version": "1.0.0",
  "main": "dist/index.js",
  "module": "dist/index.mjs",
  "types": "dist/index.d.ts",
  "exports": {
    ".": {
      "import": "./dist/index.mjs",
      "require": "./dist/index.js",
      "types": "./dist/index.d.ts"
    }
  },
  "scripts": {
    "build": "vite build"
  }
}
json 复制代码
// packages/api/package.json
{
  "name": "@my/api",
  "version": "1.0.0",
  "dependencies": {
    "@my/utils": "workspace:*",
    "@my/types": "workspace:*",
    "axios": "^1.6.0"
  }
}
json 复制代码
// apps/admin-a/package.json
{
  "name": "admin-a",
  "private": true,
  "dependencies": {
    "@my/ui": "workspace:*",
    "@my/api": "workspace:*",
    "@my/utils": "workspace:*",
    "vue": "^3.4.0"
  },
  "scripts": {
    "dev": "vite",
    "build": "vite build"
  }
}

workspace:* 表示使用当前 workspace 内的最新版本,改完公共包即可在 app 里直接生效。

[⬆ 返回目录](#⬆ 返回目录)

6.6 App 端 Vite 配置(Vue3 + Vite + TS)

在 Monorepo 中,packages 下的内部包可能和 app 共用 Vue、Element Plus 等,需要处理依赖去重和预构建。

ts 复制代码
// apps/admin-a/vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'

export default defineConfig({
  plugins: [vue()],
  resolve: {
    dedupe: ['vue', 'vue-router', 'pinia', 'element-plus'],
    alias: {
      '@': resolve(__dirname, 'src'),
    },
  },
  optimizeDeps: {
    include: ['@my/ui', '@my/utils', '@my/api', '@my/types'],
  },
  server: {
    fs: {
      allow: ['..', '../..'],
    },
    watch: {
      ignored: ['!**/packages/**'],
    },
  },
})

说明:

  • resolve.dedupe:保证 Vue、Pinia、Element Plus 只解析一份实例
  • optimizeDeps.include:把内部包加入预构建,避免开发时解析异常
  • server.fs.allow:允许访问 workspace 根目录
  • server.watch.ignored:监听 packages 变更,保证 HMR 生效

[⬆ 返回目录](#⬆ 返回目录)

6.7 ESLint / Prettier 统一配置

在根目录配置,各子包继承即可。

根目录 .eslintrc.cjs

js 复制代码
module.exports = {
  root: true,
  env: { browser: true, es2021: true, node: true },
  extends: [
    'eslint:recommended',
    'plugin:vue/vue3-recommended',
    'plugin:@typescript-eslint/recommended',
    'prettier',
  ],
  parser: 'vue-eslint-parser',
  parserOptions: {
    parser: '@typescript-eslint/parser',
    ecmaVersion: 'latest',
    sourceType: 'module',
  },
  plugins: ['vue', '@typescript-eslint'],
  ignorePatterns: ['dist', 'node_modules', '**/*.min.js'],
  rules: { 'vue/multi-word-component-names': 'warn' },
}

根目录 .prettierrc

json 复制代码
{
  "semi": false,
  "singleQuote": true,
  "trailingComma": "es5",
  "tabWidth": 2,
  "printWidth": 100
}

根目录安装:

bash 复制代码
pnpm add -Dw eslint prettier eslint-config-prettier eslint-plugin-vue @typescript-eslint/eslint-plugin @typescript-eslint/parser vue-eslint-parser

[⬆ 返回目录](#⬆ 返回目录)

6.8 组件库打包完整配置

Vue3 组件库(Vite Library Mode)
Plain 复制代码
packages/ui/
├── src/
│   ├── Button/Button.vue
│   ├── Table/Table.vue
│   └── index.ts
├── package.json
└── vite.config.ts

packages/ui/package.json

json 复制代码
{
  "name": "@my/ui",
  "version": "1.0.0",
  "type": "module",
  "main": "./dist/index.cjs",
  "module": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "import": "./dist/index.js",
      "require": "./dist/index.cjs",
      "types": "./dist/index.d.ts"
    }
  },
  "files": ["dist"],
  "sideEffects": ["**/*.css"],
  "scripts": { "build": "vite build" },
  "peerDependencies": { "vue": "^3.4.0" },
  "devDependencies": {
    "vite": "^5.0.0",
    "vite-plugin-dts": "^3.6.0",
    "vue": "^3.4.0",
    "@vitejs/plugin-vue": "^5.0.0"
  }
}

packages/ui/vite.config.ts

ts 复制代码
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import dts from 'vite-plugin-dts'
import { resolve } from 'path'

export default defineConfig({
  plugins: [
    vue(),
    dts({ insertTypesEntry: true, outDir: 'dist', include: ['src/**/*.ts', 'src/**/*.vue'] }),
  ],
  build: {
    lib: {
      entry: resolve(__dirname, 'src/index.ts'),
      name: 'MyUI',
      fileName: (format) => (format === 'es' ? 'index.js' : 'index.cjs'),
      formats: ['es', 'cjs'],
    },
    rollupOptions: {
      external: ['vue'],
      output: {
        globals: { vue: 'Vue' },
        assetFileNames: (info) => (info.name === 'style.css' ? 'style.css' : 'assets/[name][extname]'),
      },
    },
    cssCodeSplit: true,
    sourcemap: true,
  },
})
Vue2 组件库(Webpack)
Plain 复制代码
packages/ui-v2/
├── src/
│   ├── Button/Button.vue
│   └── index.ts
├── package.json
└── webpack.config.js

packages/ui-v2/package.json

json 复制代码
{
  "name": "@my/ui-v2",
  "version": "1.0.0",
  "main": "./dist/index.js",
  "module": "./dist/index.esm.js",
  "types": "./dist/index.d.ts",
  "files": ["dist"],
  "scripts": { "build": "webpack --config webpack.config.js" },
  "peerDependencies": { "vue": "^2.6.0" },
  "devDependencies": {
    "vue": "^2.7.0",
    "webpack": "^5.0.0",
    "vue-loader": "^15.10.0",
    "vue-template-compiler": "^2.7.0",
    "ts-loader": "^9.0.0",
    "css-loader": "^6.0.0",
    "vue-style-loader": "^4.0.0"
  }
}

packages/ui-v2/webpack.config.js

js 复制代码
const path = require('path')
const { VueLoaderPlugin } = require('vue-loader')

module.exports = {
  mode: 'production',
  entry: './src/index.ts',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'index.js',
    library: { name: 'MyUIV2', type: 'umd', umdNamedDefine: true },
    globalObject: "typeof self !== 'undefined' ? self : this",
  },
  resolve: { extensions: ['.ts', '.vue', '.js'] },
  externals: { vue: { root: 'Vue', commonjs: 'vue', commonjs2: 'vue', amd: 'vue' } },
  module: {
    rules: [
      { test: /\.ts$/, use: 'ts-loader', exclude: /node_modules/ },
      { test: /\.vue$/, use: 'vue-loader' },
      { test: /\.css$/, use: ['vue-style-loader', 'css-loader'] },
    ],
  },
  plugins: [new VueLoaderPlugin()],
}

[⬆ 返回目录](#⬆ 返回目录)


七、依赖管理:内部包怎么互相引用?

规则

  1. 内部包用 workspace:*,不要用 ^1.0.0 等具体版本
  2. 明确依赖链:api 依赖 utilstypesui 可依赖 utilsapps 按需依赖
  3. 安装依赖:根目录用 pnpm add xxx -w,或 pnpm add xxx --filter admin-a 给指定包安装

常见误区 :在 packages/utilspnpm add lodash,只装到 utils。多个包都要用的话,要么各自声明,要么在根用 pnpm add lodash -w,视是否要统一版本而定。

[⬆ 返回目录](#⬆ 返回目录)


八、构建与发布:怎么打包、怎么发版?

开发阶段:一般不需要单独构建公共包,Vite 会通过 resolve 直接读源码。

生产构建 :先构建 packages,再构建 apps

json 复制代码
{
  "scripts": {
    "build": "pnpm run build:packages && pnpm run build:apps",
    "build:packages": "pnpm -r --filter './packages/*' run build",
    "build:apps": "pnpm -r --filter './apps/*' run build"
  }
}

发布到私有 npm:

bash 复制代码
pnpm publish -r --filter "@my/ui"

8.1 CI/CD:只构建变更的包

使用 Turborepo 做增量构建和缓存:

根目录 turbo.json

json 复制代码
{
  "$schema": "https://turbo.build/schema.json",
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**"],
      "cache": true
    }
  }
}

根目录添加 turbo 和脚本:

json 复制代码
{
  "devDependencies": { "turbo": "^1.10.0" },
  "scripts": {
    "build": "turbo run build",
    "build:filter": "turbo run build --filter='...[origin/main]'"
  }
}

仅用 pnpm 时:

bash 复制代码
pnpm -r --filter './packages/*' run build
pnpm -r --filter '@my/ui...' run build

[⬆ 返回目录](#⬆ 返回目录)


九、常见踩坑与解决方案

9.1 Vue 找不到依赖(如找不到 vue

原因:pnpm 默认非扁平化,子包有时找不到根 node_modules 里的 vue

解决:根 .npmrcshamefully-hoist=true,或各 app 显式声明 vue

[⬆ 返回目录](#⬆ 返回目录)

9.2 改 packages 代码,app 不热更新

可能原因:

  • 公共包未在 package.json 中正确声明依赖
  • Vite 的 optimizeDeps.exclude 排除了内部包,需要检查
  • 开发时指向构建产物而非源码,检查 main / module / exports 是否指向 src

[⬆ 返回目录](#⬆ 返回目录)

9.3 循环依赖

例如:api 依赖 utilsutils 又依赖 api

解决:把公共逻辑放到更底层的包(如 utils),api 只做接口封装。

[⬆ 返回目录](#⬆ 返回目录)

9.4 TypeScript 路径与类型

公共包需要:

  • 正确配置 typesexports.types
  • 构建时生成 .d.ts(Vite 可用 vite-plugin-dts

否则 app 引用时会报类型错误或缺失。

[⬆ 返回目录](#⬆ 返回目录)

9.5 不同 app 的 Vue / Element 版本不一致

建议:在根 package.jsonpnpm overridesresolutions 统一版本,避免多份 Vue/UI 库被打进不同包。

[⬆ 返回目录](#⬆ 返回目录)


十、小结:何时用 Monorepo,何时不用

适合用 Monorepo:

  • 多个中后台 + 共享组件 / 工具
  • 内部组件库需要和业务项目紧密联调
  • 团队在同一套技术栈下协作

可以不急着用:

  • 只有一两个后台,公共代码不多
  • 团队很小,发版联调成本不高
  • 项目之间技术栈差异大、关联弱

[⬆ 返回目录](#⬆ 返回目录)


学习本就是一场持久战,不需要急着一口吃成胖子。哪怕今天你只记住了一点点,这都是实打实的进步。

后续我还会继续用这种大白话、讲实战的方式,带大家扫盲更多前端基础。

关注我,不迷路,咱们把那些曾经模糊的知识点,一个个彻底搞清楚。

如果你觉得这篇内容对你有帮助,不妨点赞+收藏,下次写代码卡壳时,拿出来翻一翻,比搜引擎更靠谱。

我是 Eugene,你的电子学友,我们下一篇干货见~

相关推荐
我命由我123452 小时前
React - 收集表单元素、收集表单元素优化、生命周期(旧)、生命周期(新)
前端·javascript·react.js·前端框架·html·html5·js
We་ct2 小时前
JSX & ReactElement 核心解析
前端·react.js·面试·架构·前端框架·reactjs·个人开发
白中白121382 小时前
杂七杂八补充系列
开发语言·前端·javascript
Xingxing?!2 小时前
Vue2 微信小程序:页面间传递数组
前端·vue.js·uni-app
肉肉不吃 肉2 小时前
代理服务的原理,及Vite 中具体实现方法
前端·vue.js
前端小D2 小时前
作用域/闭包
前端·javascript
前端 贾公子2 小时前
@uni-helper 社区:让 uni-app 拥抱 ESM 时代
开发语言·前端·javascript
大卡拉米2 小时前
ClaudeCode安装及使用
前端·学习
豆豆2 小时前
PageAdmin CMS模板开发详解:HTML转CMS系统的10个核心步骤
前端·html·cms·网站建设·网站制作·自助建站·网站管理系统