作者:全栈开发小助手
🎉《用 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/config@2.0.0/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/"
}
        