[PNPM Workspace] 搭建Monorepo工程
pnpm + workspace
前置知识
思考🤔:什么是工作空间?
答案:工作空间可以看作是一个共享的区域,所有用于工作的资源都可以从这个区域获取到。
生活中工作空间
在这个工作空间中,通常会包含与工作相关的所有工具和资源,比如办公桌、电脑、文具和文件柜等。这个工作空间是一个集中完成特定任务的地方,所有需要用到的东西都可以在这里找到,方便你高效地完成工作。

软件开发中的工作空间
在软件开发中,工作空间通常指一个用于组织和管理项目文件、资源和工具的逻辑容器。它通常是一个文件夹结构,用于将相关的项目文件、代码、设置和其他资源集中放置在一起。
工作空间的概念在不同的编程语言和开发工具中可能略有不同,但其基本目标都是提供一个集中式环境,以帮助开发者管理和协同开发多个项目。主要功能包括:
- 组织和管理项目文件
- 跨项目共享设置和工具
- 支持协同开发
pnpm中的工作空间
在 pnpm 中,工作空间就是一个管理多个包的环境,它通过独特的依赖管理方式极大地提高了效率。pnpm 的工作空间支持符号链接和硬链接机制,使得不同包之间能够高效地共享依赖,同时保证每个包的独立性。
pnpm工作空间特点:
- 高效的依赖管理
- 节省磁盘空间
- 跨项目的高效协作
pnpm 的工作空间为大型 Monorepo 项目提供了一个强大而灵活的开发环境,使得管理和开发多个包变得更加简单和高效。
pnpm中定义工作空间
在根目录有一个 pnpm-workspace.yaml 的文件,该文件用于定义哪些包会被包含在 workspace 工作空间中,默认情况下,所有子目录下的所有包都会被包含在 workspace 里面。
示例:
packages:
# packages/ 下所有子包,但是不包括子包下面的包
- 'packages/*'
# components/ 下所有的包,包含子包下面的子包
- 'components/**'
# 排除 test 目录
- '!**/test/**'
注意这里表示包范围的语法使用的是 Glob 表示法。
实战演练
创建基于 pnpm + workspace 的 Monorepo 工程,并在工程中封装一个公共的函数库。
安装依赖到工作空间里面:
pnpm add <包名> --workspace-root
or
pnpm add <包名> -w
安装工作空间的一个包到工作空间另一个包里面:
pnpm add <包名B> --workspace --filter <包名A>
该命令表示将 B 包安装到 A 包里面,也就是说 B 包成为了 A 包的一个依赖。其中 B 包后面的 --workspace 参数表示该包来自于工作空间,而非 npm 远程仓库,--filter 表示安装到 A 包里面。
搭建内部组件库
前置准备
pnpm create vue@latest
搭建项目时的选项如下:

思考🤔:组件要做单元测试,为什么 Add Vitest for Unit Testing 选择了 No 呢?
答案:
App.vue基本结构
<template>
<h1>公司内部组件</h1>
</template>
<script setup lang="ts"></script>
<style scoped>
.row {
margin-bottom: 20px;
width: 800px;
display: flex;
justify-content: space-evenly;
}
.footer {
padding: 0 8px;
font-size: 12px;
text-align: left;
}
.level {
color: #9199a1;
margin-bottom: 8px;
}
.price {
color: #f01414;
}
.footer-java {
display: flex;
justify-content: space-between;
font-size: 12px;
padding: 0 8px;
}
</style>
main.ts
// import './assets/main.css'
import { createApp } from 'vue'
import App from './App.vue'
import './assets/fonts/font.scss'
const app = createApp(App)
app.mount('#app')
工作空间安装 sass-embedded
pnpm add sass-embedded -D -w
组件开发
Button 组件是一个比较容易的组件,下面是一些重点:
- 接收一组 Props
- 模板中根据不同 Props 挂载不同的样式类
- 组件样式的书写
- 支持插槽
- 支持图标
Card 组件也是一个比较容易的组件:
- 图片是必传 Props
- 图片描述传递的方式有两种
- 通过 summary Props 传递
- 通过 slot 进行传递
- 可以传递 footer 这个 slot,这就涉及到具名插槽
Dialog 组件整体分为 3 个部分:
- 头部:支持一个具名 slot 以及一个关闭对话框的按钮
- 内容区域:支持一个默认的 slot 插槽
- 底部:同样支持一个具名的 slot
另外,对话框的显示与否,取决于父级组件传递进来的 visible 这个 Props. 支持一个自定义事件 close,当点击关闭按钮的时候,会触发父组件所传递的 close 回调方法。
组件测试
安装两个依赖:
pnpm add @vue/test-utils jsdom -D -w
- @vue/test-utils: Vue官方提供的一个测试实用工具库,主要用于测试 Vue 组件。例如提供了 mount 和 shallowMount 方法,用于将组件挂载到虚拟 DOM 中。
- mount 方法会完整地渲染组件及其子组件
- shallowMount 方法则只渲染组件自身,并将子组件替换为占位符
- jsdom: 模拟浏览器环境下测试,当测试中涉及到 DOM 操作、浏览器 API(如 window、document)等内容时,可以配置在此环境下进行测试
在书写测试用例之前,需要先在配置文件中做相应的配置,这里会涉及到两个方面的配置:
- tsconfig.json
- vitest.config.ts
接下来书写测试用例:
- Button.spec.ts
- Card.spec.ts
- Dialog.spec.ts
最后在 package.json 中添加测试脚本:
"scripts": {
"test:unit": "vitest",
// ...
},
组件的打包与引用
库模式
库模式指的是将应用打包成一个依赖库,方便其他应用来使用。因此和普通打包是有一定区别的:
- 入口文件:
- 普通应用:html 文件
- 库模式:不包含 html 文件,入口文件是一个 js 文件
- 输出格式:
- 普通应用:一般是浏览器环境
- 库模式:通常需要支持多种模块系统
- 外部依赖:
- 普通应用:需要一起打包进去
- 库模式:通常需要将外部依赖(vue、react)排除掉
在库模式(lib)中,我们可以定义入口点、库的名称、输出文件名,以及如何处理外部依赖。这些配置确保你的库被打包成适用于不同消费场景的格式,如 ES 模块或 UMD 格式。
举一个例子:
my-lib/
├── lib/
│ ├── main.js // 库的入口文件
│ ├── Foo.vue // Vue 组件
│ └── Bar.vue // 另一个 Vue 组件
├── index.html // 用于开发测试的 HTML 文件
├── package.json
└── vite.config.js // Vite 配置文件
Vite 配置文件 (vite.config.js)
// vite.config.js
import { resolve } from 'path'
import { defineConfig } from 'vite'
export default defineConfig({
build: {
lib: {
entry: resolve(__dirname, 'lib/main.js'),
name: 'MyLib',
fileName: (format) => `my-lib.${format}.js`
},
rollupOptions: {
external: ['vue'],
output: {
globals: {
vue: 'Vue'
},
},
},
},
})
-
entry: 指定库的入口文件。
-
name: name 是用来指定你的库在 UMD 和 IIFE 构建格式下的全局变量名称。
- 当你的库被加载时,如果是在一个没有模块系统的环境(例如直接在浏览器中通过 <script> 标签引入),这个名称将成为全局变量,通过这个名称可以访问到你的库。
- 如果你设置 name: 'MyLib',在浏览器环境中加载时,可以通过 window.MyLib 访问到你的库。
-
fileName: 输出文件的命名规则。
-
external: 告诉 rollup 不要将 Vue 打包进库,因为我们假设用户环境已有 Vue。
-
globals: globals 用于指定外部依赖在 UMD 和 IIFE 构建格式下的全局变量名称。
- 当你的库依赖某些外部库(如 Vue),你需要告诉构建工具这些库在目标环境中的全局变量名称,以确保在没有模块系统的环境中正确引用这些依赖。
- 如果你的库依赖 vue,并且 globals 中配置了 vue: 'Vue',在目标环境中,你的库会假定 Vue 是一个已经存在的全局变量。
构建输出
执行 vite build 后,输出目录可能如下所示:
my-lib/
├── dist/
│ ├── my-lib.es.js // ES 模块格式
│ ├── my-lib.umd.js // UMD 格式
│ └── assets/ // 包含所有静态资源,如编译后的 CSS
└── ...
package.json 配置
{
"name": "my-lib",
"type": "module",
"files": ["dist"],
"main": "./dist/my-lib.umd.js",
"module": "./dist/my-lib.es.js",
"exports": {
".": {
"import": "./dist/my-lib.es.js",
"require": "./dist/my-lib.umd.js"
}
}
}
这里的配置确保了无论是使用 require 还是 import,使用者都能正确地加载到适当格式的文件。