作者:全栈开发小助手
🎉《用 pnpm + workspace + changesets 打造monorepo工程,前端er必看!》
❝
前端leader必须了解的monorepo工程!:pnpm + workspace + changesets
🎉
一、啥是monorepo呀?
Monorepo呢,简单来说就是把多个工程都放到一个git仓库里去管理哦。这样做的好处可多啦,比如说它们能共享同一套构建流程,代码规范也能统一起来呢。特别是当存在模块间相互引用的情况时,查看代码、修改bug、调试啥的都会变得更加方便哟。
二、pnpm又是何方神圣?
Pnpm那可是新一代的包管理工具呀,号称是最先进的包管理器呢!按照官网的说法,它能实现节约磁盘空间并且提升安装速度,还能创建非扁平化的node_modules文件夹哦,具体原理大家可以去pnpm官网瞧瞧。而且呀,pnpm提出了workspace的概念,还内置了对monorepo的支持呢。
那为啥要用pnpm取代之前的lerna呢?这里我给大家总结几点哈:
- lerna现状:lerna已经不再维护啦,要是后续遇到啥问题,社区可没办法及时响应咯。
- 装包效率:pnpm装包效率更高,并且能节约更多的磁盘空间呢。
- 对monorepo的支持:pnpm本身就预置了对monorepo的支持,不需要再额外搞啥第三方包来支持啦。
三、咋用pnpm来搭建monorepo工程呢?
(一)安装pnpm
首先得安装pnpm呀,代码如下:
ruby
$ npm install -g pnpm
注意哦,v7版本的pnpm安装使用需要node版本至少大于v14.19.0,所以安装之前可别忘了先检查下node版本呢。
(二)工程初始化
为了方便后面演示,咱先在工程根目录下新建个packages目录,然后在这个目录下创建pkg1和pkg2两个工程哦。分别进到这两个目录下,执行npm init
命令,来初始化这两个工程。这里package.json
中的name
字段分别叫做@qftjs/menorepo1
和@qftjs/monorepo2
(PS:@qftjs是提前在npm上创建好的组织,没有的话需要提前创建呀)。
为了防止根目录被发布出去,还要设置工程各个目录下package.json
配置文件的private
字段为true
哦。
这里我用father-build
对模块进行打包哈,它是基于rollup
进行的一层封装,用起来更方便呢。在pkg1和pkg2的src目录下都创建一个index.ts
文件哦,给大家看看代码示例:
pkg1/src/index.ts:
javascript
// pkg1/src/index.ts
import pkg2 from '@qftjs/monorepo2';
function fun2() {
pkg2();
console.log('I am package 1');
}
export default fun2;
pkg2/src/index.ts:
javascript
// pkg2/src/index.ts
function fun2() {
console.log('I am package 2');
}
export default fun2;
然后分别在pkg1和pkg2下新增.fatherrc.ts
和tsconfig.ts
配置文件哦,代码解读如下:
.fatherrc.ts
:
arduino
export default {
target: 'node',
cjs: { type: 'babel', lazy: true },
disableTypeCheck: false,
};
tsconfig.ts
:
json
{
"include": ["src", "types", "test"],
"compilerOptions": {
"target": "es5",
"module": "esnext",
"lib": ["dom", "esnext"],
"importHelpers": true,
"declaration": true,
"sourceMap": true,
"rootDir": "./",
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictPropertyInitialization": true,
"noImplicitThis": true,
"alwaysStrict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"moduleResolution": "node",
"baseUrl": "./",
"paths": {
"*": ["src/*", "node_modules/*"]
},
"jsx": "react",
"esModuleInterop": true
}
}
接着要全局安装father-build
哦:
css
$ pnpm i -Dw father-build
最后在pkg1和pkg2下的package.json
文件中增加一条script
:
json
{
"scripts": {
"build": "father-build"
}
}
这样在pkg1或者pkg2下执行build
命令就会将各子包的ts代码打包成js代码输出至lib
目录下啦。
哦对了,要想启动pnpm
的workspace
功能,需要工程根目录下存在pnpm-workspace.yaml
配置文件,并且在里面指定工作空间的目录哦。比如这里我们所有的子包都是放在packages
目录下,那pnpm-workspace.yaml
内容就可以这样写:
vbnet
packages:
- 'packages/*'
初始化完毕后的工程目录结构大概是这样的:
go
.
├── README.md
├── package.json
├── packages
│ ├── pkg1
│ │ ├── package.json
│ │ ├── src
│ │ │ └── index.ts
│ │ └── tsconfig.json
│ └── pkg2
│ ├── package.json
│ ├── src
│ │ └── index.ts
│ └── tsconfig.json
├── pnpm-workspace.yaml
└── tsconfig.root.json
(三)安装依赖包
用pnpm安装依赖包一般有下面几种情况哦:
1. 全局的公共依赖包 : 比如打包涉及到的rollup
、typescript
等,pnpm
提供了-w, --workspace-root
参数,可以将依赖包安装到工程的根目录下,作为所有package的公共依赖呢。比如:
ruby
$ pnpm install react -w
要是一个开发依赖的话,可以加上-D
参数,表示这是一个开发依赖,会装到pacakage.json
中的devDependencies
中哦,比如:
ruby
$ pnpm install rollup -wD
2. 给某个package单独安装指定依赖 :pnpm
提供了--filter
参数,可以用来对特定的package进行某些操作哦。比如说想给pkg1安装一个依赖包,比如axios
,就可以这样操作:
css
$ pnpm add axios --filter @qftjs/monorepo1
要注意哦,--filter
参数跟着的是package下的package.json
的name
字段,可不是目录名哈。关于--filter
操作其实挺丰富的呢,比如执行pkg1下的scripts脚本:
css
$ pnpm build --filter @qftjs/monorepo1
filter
后面除了可以指定具体的包名,还可以跟着匹配规则来指定对匹配上规则的包进行操作哦,比如:
python
$ pnpm build --filter "./packages/**"
此命令会执行所有package下的build
命令呢,具体的用法可以参考filter文档哦。
(四)模块之间的相互依赖
在开发时经常会遇到这种情况啦,比如pkg1中将pkg2作为依赖进行安装。基于pnpm提供的workspace:协议
,可以方便地在packages内部进行互相引用哦。比如在pkg1中引用pkg2:
ruby
$ pnpm install @qftjs/monorepo2 -r --filter @qftjs/monorepo1
这时我们查看pkg1的package.json
,就会看到dependencies
字段中多了对@qftjs/monorepo2
的引用,是以workspace:
开头,后面跟着具体的版本号哦,就像这样:
perl
{
"name": "@qftjs/monorepo1",
"version": "1.0.0",
"dependencies": {
"@qftjs/monorepo2": "workspace:^1.0.0",
"axios": "^0.27.2"
}
}
在设置依赖版本的时候推荐用workspace:*
,这样就可以保持依赖的版本是工作空间里最新版本,不需要每次手动更新依赖版本啦。当pnpm publish
的时候,会自动将package.json
中的workspace
修正为对应的版本号哦。
(五)只允许pnpm
当在项目中使用pnpm
时,如果不希望用户使用yarn
或者npm
安装依赖,可以将下面的这个preinstall
脚本添加到工程根目录下的package.json
中哦:
json
{
"scripts": {
"preinstall": "npx only-allow pnpm"
}
}
preinstall脚本会在install
之前执行,现在,只要有人运行npm install
或yarn install
,就会调用only-allow去限制只允许使用pnpm
安装依赖咯。
四、Release工作流
在workspace
中对包版本管理可是个挺复杂的事儿呢,遗憾的是pnpm
没有提供内置的解决方案哦。一部分开源项目在自己的项目中自己实现了一套包版本的管理机制,比如Vue3、Vite等。pnpm推荐了两个开源的版本控制工具:changesets和rush。这里我采用了changesets来做依赖包的管理哦,主要是因为它的文档更加清晰一些,个人感觉上手比较容易呢。
(一)配置changesets
首先要安装changesets哦:
ruby
$ pnpm add -Dw @changesets/cli
然后初始化:
csharp
$ pnpm changeset init
执行完初始化命令后,会在工程的根目录下生成.changeset
目录,其中的config.json
作为默认的changeset
的配置文件哦。我们可以修改配置文件如下:
lua
{
"$schema": "https://unpkg.com/@changesets/[email protected]/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"linked": [["@qftjs/*"]],
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": [],
"___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": {
"onlyUpdatePeerDependentsWhenOutOfRange": true
}
}
这里给大家说说各项的含义哈:
- changelog:changelog生成方式。
- commit :不要让
changeset
在publish
的时候帮我们做git add
。 - linked:配置哪些包要共享版本。
- access:公私有安全设定,内网建议restricted,开源使用public。
- baseBranch:项目主分支。
- updateInternalDependencies:确保某包依赖的包发生upgrade,该包也要发生version upgrade的衡量单位(量级)。
- ignore:不需要变动version的包。
- ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH:在每次version变动时一定无理由patch抬升依赖他的那些包的版本,防止陷入major优先的未更新问题。
(二)如何使用changesets
一个包一般分如下几个步骤哦:
1. 编译阶段,生成构建产物 : 在工程根目录下的pacakge.json
的scripts
中增加如下脚本:
perl
{
"build": "pnpm --filter=@qftjs/* run build"
}
2. 清理构建产物和node_modules
:
css
{
"clear": "rimraf 'packages/*/{lib,node_modules}' && rimraf node_modules"
}
3. 执行changeset
,开始交互式填写变更集 : 这个命令会将你的包全部列出来,然后选择你要更改发布的包哦,在pacakge.json
的scripts
中增加如下脚本:
json
{
"changeset": "changeset"
}
4. 执行changeset version
,修改发布包的版本 : 在pacakge.json
的scripts
中增加如下脚本:
json
{
"version-packages": "changeset version"
}
这里要注意哦,版本的选择一共有三种类型,分别是patch
、minor
和major
,严格遵循semver规范呢。
要是不想直接发release
版本,而是想先发一个带tag
的prerelease
版本(比如beta或者rc版本)呢?这里给大家提供两种方式哦:
方式一:手工调整这种方法最简单粗暴,但是比较容易犯错哦。首先需要修改包的版本号,比如:
perl
{
"name": "@qftjs/monorepo1",
"version": "1.0.2-beta.1"
}
然后运行:
css
$ pnpm changeset publish --tag beta
注意发包的时候不要忘记加上--tag
参数哦。
方式二:通过changeset
提供的Prereleases
模式 利用官方提供的Prereleases模式,通过pre enter <tag>
命令进入先进入pre模式哦。常见的tag如下所示:
名称 | 功能 |
---|---|
alpha | 是内部测试版,一般不向外部发布,会有很多Bug,一般只有测试人员使用 |
beta | 也是测试版,这个阶段的版本会一直加入新的功能。在Alpha版之后推出 |
rc | Release Candidate) 系统平台上就是发行候选版本。RC版不会再加入新的功能了,主要着重于除错 |
先运行:
ruby
$ pnpm changeset pre enter beta
之后在此模式下的changeset publish
均将默认走beta
环境,下面在此模式下任意的进行你的开发哦,给大家举个例子:
bash
# 1-1 进行了一些开发...
# 1-2 提交变更集
pnpm changeset
# 1-3 提升版本
pnpm version-packages # changeset version
# 1-4 发包
pnpm release # pnpm build && pnpm changeset publish --registry=...
# 1-5 得到 1.0.0-beta.1
# 2-1 进行了一些开发...
# 2-2 提交变更集
pnpm changeset
# 2-3 提升版本
pnpm version-packages
# 2-4 发包
pnpm release
# 2-5 得到 1.0.0-beta.2
完成版本发布之后,退出Prereleases
模式:
shell
$ pnpm changeset pre exit
最后还有构建产物后发版本的脚本哦:
json
{
"release": "pnpm build && pnpm release:only",
"release:only": "changeset publish --registry=https://registry.npmjs.com/"
}
