用 pnpm + workspace + changesets 打造monorepo工程,【前端er必看!】

作者:全栈开发小助手

🎉《用 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.tstsconfig.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目录下啦。

哦对了,要想启动pnpmworkspace功能,需要工程根目录下存在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. 全局的公共依赖包 : 比如打包涉及到的rolluptypescript等,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.jsonname字段,可不是目录名哈。关于--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 installyarn 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 :不要让changesetpublish的时候帮我们做git add
  • linked:配置哪些包要共享版本。
  • access:公私有安全设定,内网建议restricted,开源使用public。
  • baseBranch:项目主分支。
  • updateInternalDependencies:确保某包依赖的包发生upgrade,该包也要发生version upgrade的衡量单位(量级)。
  • ignore:不需要变动version的包。
  • ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH:在每次version变动时一定无理由patch抬升依赖他的那些包的版本,防止陷入major优先的未更新问题。

(二)如何使用changesets

一个包一般分如下几个步骤哦:

1. 编译阶段,生成构建产物 : 在工程根目录下的pacakge.jsonscripts中增加如下脚本:

perl 复制代码
{
  "build": "pnpm --filter=@qftjs/* run build"
}

2. 清理构建产物和node_modules

css 复制代码
{
  "clear": "rimraf 'packages/*/{lib,node_modules}' && rimraf node_modules"
}

3. 执行changeset,开始交互式填写变更集 : 这个命令会将你的包全部列出来,然后选择你要更改发布的包哦,在pacakge.jsonscripts中增加如下脚本:

json 复制代码
{
  "changeset": "changeset"
}

4. 执行changeset version,修改发布包的版本 : 在pacakge.jsonscripts中增加如下脚本:

json 复制代码
{
  "version-packages": "changeset version"
}

这里要注意哦,版本的选择一共有三种类型,分别是patchminormajor,严格遵循semver规范呢。

要是不想直接发release版本,而是想先发一个带tagprerelease版本(比如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/"
}
相关推荐
qq_25249639962 分钟前
react 子组件暴露,父组件接收
前端·javascript·react.js
fakaifa5 分钟前
【最新版】西陆健身系统源码全开源+uniapp前端
前端·小程序·uni-app·开源·php·约课小程序·健身小程序
南囝coding11 分钟前
关于我的第一个产品!
前端·后端·产品
iOS阿玮17 分钟前
别等了,今天是Xcode15时代的最后一天。
前端·app·apple
沙尘暴炒饭24 分钟前
vuex持久化vuex-persistedstate,存储的数据刷新页面后导致数据丢失
开发语言·前端·javascript
2401_8370885026 分钟前
CSS清楚默认样式
前端·javascript·css
zwjapple37 分钟前
React 的 useEffect 清理函数详解
前端·react.js·前端框架
Jewel1051 小时前
如何配置Telegram Mini-App?
前端·vue.js·app
s11show_1632 小时前
hz修改后台新增keyword功能
android·java·前端
二个半engineer2 小时前
Web常见攻击方式及防御措施
前端