pnpm + workspace
思考 🤔:什么是工作空间?
答案:工作空间可以看作是一个共享的区域,所有用于工作的资源都可以从这个区域获取到。
Monorepo 背后的思想:创建一个公共的空间,服务于多个项目,把多个项目用到的共有的一些东西,提取出来
生活中工作空间
在这个工作空间中,通常会包含与工作相关的所有工具和资源,比如办公桌、电脑、文具和文件柜等。这个工作空间是一个集中完成特定任务的地方,所有需要用到的东西都可以在这里找到,方便你高效地完成工作。
软件开发中的工作空间
在软件开发中,工作空间通常指一个用于组织和管理项目文件、资源和工具的逻辑容器。它通常是一个文件夹结构,用于将相关的项目文件、代码、设置和其他资源集中放置在一起。
工作空间的概念在不同的编程语言和开发工具中可能略有不同,但其基本目标都是提供一个集中式环境,以帮助开发者管理和协同开发多个项目。主要功能包括:
- 组织和管理项目文件
- 跨项目共享设置和工具
- 支持协同开发
pnpm 中的工作空间
在 pnpm 中,工作空间就是一个管理多个包的环境,它通过独特的依赖管理方式极大地提高了效率。pnpm 的工作空间支持符号链接和硬链接机制,使得不同包之间能够高效地共享依赖,同时保证每个包的独立性。
pnpm 工作空间特点:
- 高效的依赖管理
- 节省磁盘空间
- 跨项目的高效协作
pnpm 的工作空间为大型 Monorepo 项目提供了一个强大而灵活的开发环境,使得管理和开发多个包变得更加简单和高效。
pnpm 中定义工作空间
在根目录有一个 pnpm-workspace.yaml 的文件,该文件用于定义哪些包会被包含在 workspace 工作空间中,默认情况下,所有子目录下的所有包都会被包含在 works ace 里面。
示例:
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 + workspace 的 Monorepo 工程,完整步骤:
-
创建一个根目录,并初始化 pnpm workspace,例如:
mkdir monorepo cd monorepo pnpm init // 初始化 pnpm workspace,如果本地没有安装pnpm包的话,需要先安装或者使用 npx pnpm init -
创建 pnpm-workspace.yaml 文件,并定义工作空间,例如:
packages: - "components/\*" - "utils/\*" - "projects/\*" 创建目录:
monorepo/components //公共组件存放的目录
monorepo/utils //公共函数存放的目录
monorepo/projects //存放多个项目的目录
4.在utils下创建一个公共的工具库,tools目录,并切换到该目录下,使用pnpm init初始化项目,并安装依赖,例如:
pnpm init
现在tools目录下已经创建了package.json文件,可以安装依赖,tools目录就是一个包了
接下来我们将会使用typescript来开发我们的工具库,那么我们要把typescript安装在那个目录呢?
我们的typescript多个包都需要用到,所以我们需要安装到工作空间里面,而不是安装到某个包里面,这样可以保证多个项目使用的typescript版本保持一致
pnpm add typescript -D --workspace-root
执行完上面的代码之后就可以在项目的根目录package.json文件里面看到typescript的依赖了
接下来我们就可以愉快的开发我们的工具库了,在tools下面创建一个src目录,并创建一个index.ts文件,一个sum.ts文件,并编写我们的工具库,例如:
//sum.ts
export function sum(a: number, b: number): number {
return a + b;
}
//index.ts
export * from './sum';
至此,我们的工具库就算开发完成了,接下来我们需要对我们的工具库进行测试,在tools目录下创建一个tests目录存放我们的测试文件,这里我们选择vitest作为我们的测试框架,同样,除了tools需要测试,我们也需要对其他的模块进行测试,所以我们还是把vitest安装到工作空间里面:
pnpm add vitest -D --workspace-root
安装好之后,我们就可以开始编写我们的测试文件了,在tests目录下创建一个sum.test.ts文件,并编写我们的测试代码,例如:
import { sum } from "../src/sum";
test("测试sum方法", () => {
const result = sum(10, 2);
expect(result).toBe(12);
});
测试文件写完之后,我们需要运行测试文件,检查测试结果,我们在tools/package.json文件里面添加一下命令:
"test": "vitest"
然后在终端中运行该命令:
pnpm test
这个时候,终端中汇报一个错:
ReferenceError: test is not defined
这个时候我们需要在项目根目录下创建一个vitest.config.ts文件,并编写我们的配置文件,例如:
import {defineConfig} from 'vitest/config'
export default defineConfig({
test: {
globals: true,//表示vitest启用全局模式
environment: 'node',
},
})
配置完成之后,再次运行测试文件,就会发现我们的测试结果是正确的,我们的工具库开发完成了。
然后我们就需要对我们的工具库函数进行打包,以便其他开发人员可以使用
对于打包我们主要有两件事情要做,一个是调整typescript的配置选项,另一个是使用rollup打包
首先我们使用npx tsc --init在tools目录下创建一个tsconfig.json文件,并编写我们的配置选项,例如:
{
"compilerOptions": {
"target": "ES2015",//表示编译成es6的代码
"module": "ES2015",//指定使用的模块系统
"declaration": true,//生成类型声明文件
"declarationDir": "./dist/types",//生成声明文件的目录
"esModuleInterop": true,//允许导入非模块的包(启用es模块和common模块操作的互相支持)
"forceConsistentCasingInFileNames": true//强制文件名大小写匹配
},
"include": ["src/**/*"]//告诉编译器要编译的文件
}
配置好tsconfig.json文件之后,我们就可以开始打包我们的工具库了,首先安装打包相关的依赖:
pnpm add rollup rollup-plugin-typescript2 @babel/preset-env @rollup/plugin-babel @rollup/plugin-commonjs @rollup/plugin-json @rollup/plugin-node-resolve -D --workspace-root
在tools目录下创建一个rollup.config.js文件,并编写我们的配置文件:
// tools/rollup.config.js
// 导入各种插件
import typescript from "rollup-plugin-typescript2";
import commonjs from "@rollup/plugin-commonjs";
import resolve from "@rollup/plugin-node-resolve";
import json from "@rollup/plugin-json";
import babel from "@rollup/plugin-babel";
const extensions = [".js", ".ts"];
// 导出一个数组,数组里面每一个对象对应一种格式的配置
export default [
// CommonJS
{
input: "src/index.ts",
output: {
file: "dist/index.cjs",
format: "cjs",
},
plugins: [
typescript({
useTsconfigDeclarationDir: true,
}),
resolve({ extensions }),
commonjs(),
json(),
],
},
// ESM
{
input: "src/index.ts",
output: {
file: "dist/index.js",
format: "es",
},
plugins: [
typescript({
useTsconfigDeclarationDir: true,
}),
resolve({ extensions }),
commonjs(),
json(),
],
},
// Browser-compatible
{
input: "src/index.ts",
output: {
file: "dist/index.browser.js",
format: "iife",
name: "jsTools",
},
plugins: [
typescript({
useTsconfigDeclarationDir: true,
}),
resolve({ extensions }),
commonjs(),
json(),
babel({
exclude: "node_modules/**",
extensions,
babelHelpers: "bundled",
presets: [
[
"@babel/preset-env",
{
targets: "> 0.25%, not dead",
},
],
],
}),
],
},
];
安装好依赖之后,我们需要添加一个打包的命令,在tools目录下创建一个package.json文件,并添加以下代码:
"scripts": {
"build": "rollup -c"
}
接下来,我们就可以运行打包命令了,在终端中运行以下命令:
pnpm build
打包完成之后,我们就可以得到一个dist目录,里面有index.cjs,index.js,index.browser.js三个文件,分别对应三种格式的代码,我们可以根据需要选择使用哪种格式的代码。
接下来我们需要在别的项目中引入我们的工具库,还有一件非常重要的事情,我们需要在package.json中指定一下我们的入口文件,
以便其他包含我们的工具库时,可以正确地解析我们的代码。
在package.json中添加以下代码:
"main": "dist/index.cjs",
"module": "dist/index.js",
"types": "dist/types/index.d.ts",
"exports": {
".": {
"import": "./dist/index.js",//esm
"require": "./dist/index.cjs"//commonjs
}
},
做完所有的这些操作之后,我们huidao项目根目录下,就可以使用我们的工具库了。
我们回到项目根目录下的projects目录下,来测试下是否可以正常使用我们的工具库。
新建一个test-tools的目录
mkdir test-tools //projects/test-tools
然后使用pnpm init 初始化一个package.json文件
然后通过mkdir src &&cd ./src &&touch index.ts一个src目录,并创建一个index.ts文件
接下来我们要在index.ts里面使用我们的工具库,tools是存放在工作空间里面的,那么我们如何安装一个工作空间的包呢?我们可以通过一下的命令来实现:
pnpm add <包名B> --workspace --filter <包名A>
所以我们只需要运行:
pnpm add tools --workspace --filter test-tools
就可以了,运行完这个命令之后,就可以在test-tools的package.json文件中看到我们的工具库依赖了。

这表示test-tools的依赖tools来自于工作空间,而不是单独的包。这样当我们的tools发生变化时,我们在使用的时候也会自动更新。
现在我们就可以愉快的使用工具库了。
在index.ts里面添加以下代码:
import { sum } from "tools";
console.log(sum(1, 2));
console.log(sum(10, 2));
然后在package.json中添加以下代码:
"type": "module",
"scripts": {
"dev": "tsc && node dist/index.js"
}
然后运行pnpm dev就可以看到在控制台打印出了我们的结果了。