pnpm + workspace + changesets 构建你的 monorepo 工程

  • Post author:
  • Post category:其他


本文首发于 https://mp.weixin.qq.com/s/nuSmPllrXQQC30YjTduk2g

更多资讯欢迎关注公众号:

前端架构师笔记



pnpm + workspace + changesets 构建你的 monorepo 工程



什么是monorepo?

什么是 monorepo?以及和 multirepo 的区别是什么

关于这些问题,在之前的一篇

介绍 lerna

的文章中已经详细介绍过,感兴趣的同学可以再回顾下。

简而言之,

monorepo

就是把多个工程放到一个

git

仓库中进行管理,因此他们可以共享同一套构建流程、代码规范也可以做到统一,特别是如果存在模块间的相互引用的情况,查看代码、修改bug、调试等会更加方便。



什么是 pnpm?


pnpm

是新一代的包管理工具,号称是

最先进的包管理器

。按照官网说法,可以实现

节约磁盘空间并提升安装速度



创建非扁平化的 node_modules 文件夹

两大目标,具体原理可以参考

pnpm 官网


pnpm

提出了

workspace

的概念,内置了对

monorepo

的支持,那么为什么要用

pnpm

取代之前的

lerna

呢?

这里我总结了以下几点原因:

  • lerna 已经不再维护,后续有任何问题社区无法及时响应
  • pnpm装包效率更高,并且可以节约更多磁盘空间
  • pnpm本身就预置了对monorepo的支持,不需要再额外第三方包的支持

  • one more thing

    ,就是好奇心了??



如何使用 pnpm 来搭建 menorepo 工程



安装 pnpm

$ npm install -g pnpm

v7版本的pnpm安装使用需要node版本至少大于v14.19.0,所以在安装之前首先需要检查下node版本。



工程初始化

为了便于后续的演示,先在工程根目录下新建

packages

目录,并且在

packages

目录下创建

pkg1



pkg2

两个工程,分别进到

pkg1



pkg2

两个目录下,执行

npm init

命令,初始化两个工程,

package.json

中的

name

字段分别叫做

@qftjs/menorepo1



@qftjs/monorepo2

(

PS:@qftjs是提前在npm上创建好的组织,没有的话需要提前创建

)。

为了防止根目录被发布出去,需要设置工程工程个目录下

package.json

配置文件的

private

字段为

true

为了实现一个完整的例子,这里我使用了

father-build

对模块进行打包,

father-build

是基于

rollup

进行的一层封装,使用起来更加便捷。

在 pkg1 和 pkg2 的 src 目录下个创建一个

index.ts

文件:

// 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
function fun2() {
  console.log('I am package 2');
}

export default fun2;

分别在 pkg1 和 pkg2 下新增

.fatherrc.ts



tsconfig.ts

配置文件。

// .fatherrc.ts
export default {
  target: 'node',
  cjs: { type: 'babel', lazy: true },
  disableTypeCheck: false,
};


// tsconfig.ts
{
  "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

:

$ pnpm i -Dw father-build

最后在 pkg1 和 pkg2 下的

package.json

文件中增加一条

script

:

{
  "scripts": {
    "build": "father-build"
  }
}

这样在 pkg1 或者 pkg2 下执行

build

命令就会将各子包的ts代码打包成js代码输出至

lib

目录下。

要想启动

pnpm



workspace

功能,需要工程根目录下存在

pnpm-workspace.yaml

配置文件,并且在

pnpm-workspace.yaml

中指定工作空间的目录。比如这里我们所有的子包都是放在

packages

目录下,因此修改

pnpm-workspace.yaml

内容如下:

packages:
  - 'packages/*'

初始化完毕后的工程目录结构如下:

.
├── 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

安装依赖包一般分以下几种情况:


  • 全局的公共依赖包,比如打包涉及到的

    rollup



    typescript



pnpm

提供了

-w, –workspace-root

参数,可以将依赖包安装到工程的根目录下,作为所有 package 的公共依赖。

比如:

$ pnpm install react -w

如果是一个开发依赖的话,可以加上

-D

参数,表示这是一个开发依赖,会装到

pacakage.json

中的

devDependencies

中,比如:

$ pnpm install rollup -wD

  • 给某个package单独安装指定依赖


pnpm

提供了

–filter

参数,可以用来对特定的package进行某些操作。

因此,如果想给 pkg1 安装一个依赖包,比如

axios

,可以进行如下操作:

$ pnpm add axios --filter @qftjs/monorepo1

需要注意的是,

--filter

参数跟着的是package下的

package.json



name

字段,并不是目录名。

关于

--filter

操作其实还是很丰富的,比如执行 pkg1 下的 scripts 脚本:

$ pnpm build --filter @qftjs/monorepo1


filter

后面除了可以指定具体的包名,还可以跟着匹配规则来指定对匹配上规则的包进行操作,比如:

$ pnpm build --filter "./packages/**"

此命令会执行所有 package 下的

build

命令。具体的用法可以参考

filter

文档。


  • 模块之间的相互依赖

最后一种就是我们在开发时经常遇到的场景,比如 pkg1 中将 pkg2 作为依赖进行安装。

基于 pnpm 提供的

workspace:协议

,可以方便的在 packages 内部进行互相引用。比如在 pkg1 中引用 pkg2:

$ pnpm install @qftjs/monorepo2 -r --filter @qftjs/monorepo1

此时我们查看 pkg1 的

package.json

,可以看到

dependencies

字段中多了对

@qftjs/monorepo2

的引用,以

workspace:

开头,后面跟着具体的版本号。

{
  "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

中:

{
  "scripts": {
    "preinstall": "npx only-allow pnpm"
  }
}


preinstall

脚本会在

install

之前执行,现在,只要有人运行

npm install



yarn install

,就会调用

only-allow

去限制只允许使用

pnpm

安装依赖。



Release工作流



workspace

中对包版本管理是一个非常复杂的工作,遗憾的是

pnpm

没有提供内置的解决方案,一部分开源项目在自己的项目中自己实现了一套包版本的管理机制,比如

Vue3



Vite

等。

pnpm 推荐了两个开源的版本控制工具:

这里我采用了

changesets

来做依赖包的管理。选用

changesets

的主要原因还是文档更加清晰一些,个人感觉上手比较容易。

按照

changesets 文档

介绍的,changesets主要是做了两件事:

Changesets hold two key bits of information: a version type (following semver), and change information to be added to a changelog.

简而言之就是

管理包的version



生成changelog



配置changesets

  • 安装

    $ pnpm add -DW @changesets/cli

  • 初始化

    $ pnpm changeset init

执行完初始化命令后,会在工程的根目录下生成

.changeset

目录,其中的

config.json

作为默认的

changeset

的配置文件。

修改配置文件如下:

{
  "$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

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

为了便于统一管理所有包的发布过程,在工程根目录下的

pacakge.json



scripts

中增加如下几条脚本:

  1. 编译阶段,生成构建产物

    {


    “build”: “pnpm –filter=@qftjs/* run build”

    }

  2. 清理构建产物和

    node_modules

    {


    “clear”: “rimraf ‘packages/*/{lib,node_modules}’ && rimraf node_modules”

    }

  3. 执行

    changeset

    ,开始交互式填写变更集,这个命令会将你的包全部列出来,然后选择你要更改发布的包

    {


    “changeset”: “changeset”

    }

  4. 执行

    changeset version

    ,修改发布包的版本

    {


    “version-packages”: “changeset version”

    }

这里需要注意的是,版本的选择一共有三种类型,分别是

patch



minor



major

,严格遵循

semver

规范。


这里还有个细节,如果我不想直接发

release

版本,而是想先发一个带

tag



prerelease

版本呢(比如beta或者rc版本)?

这里提供了两种方式:

  • 手工调整

这种方法最简单粗暴,但是比较容易犯错。

首先需要修改包的版本号:

{
  "name": "@qftjs/monorepo1",
  "version": "1.0.2-beta.1"
}

然后运行:

$ pnpm changeset publish --tag beta

注意发包的时候不要忘记加上

--tag

参数。

  • 通过

    changeset

    提供的

    Prereleases

    模式

    利用官方提供的

    Prereleases 模式

    ,通过

    pre enter <tag>

    命令进入先进入 pre 模式。

常见的tag如下所示:

名称

功能

alpha

是内部测试版,一般不向外部发布,会有很多Bug,一般只有测试人员使用

beta

也是测试版,这个阶段的版本会一直加入新的功能。在Alpha版之后推出

rc

Release Candidate) 系统平台上就是发行候选版本。RC版不会再加入新的功能了,主要着重于除错

$ pnpm changeset pre enter beta

之后在此模式下的

changeset publish

均将默认走

beta

环境,下面在此模式下任意的进行你的开发,举一个例子如下:

# 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

模式:

$ pnpm changeset pre exit
  1. 构建产物后发版本

    {


    “release”: “pnpm build && pnpm release:only”,

    “release:only”: “changeset publish –registry=https://registry.npmjs.com/”

    }



规范代码提交

代码提交规范对于团队或者公司来说是非常重要的,养成良好的代码提交规范可以方便回溯,有助于对本次提交进行review,如果单纯的只是要求团队成员遵循某些代码提交规范,是很难形成强制约束的,现在我们就尝试通过工具来约束代码提交规范。



使用commitizen规范commit提交格式


commitizen

的作用主要是为了生成标准化的

commit message

,符合

Angular

规范。

一个标准化的

commit message

应该包含三个部分:Header、Body 和 Footer,其中的 Header 是必须的,Body 和 Footer 可以选填。

<type>(<scope>): <subject>
// 空一行
<body>
// 空一行
<footer>

Header 部分由三个字段组成:type(必需)、scope(可选)、subject(必需)

  • Type


    type

    必须是下面的其中之一:

    • feat: 增加新功能
    • fix: 修复 bug
    • docs: 只改动了文档相关的内容
    • style: 不影响代码含义的改动,例如去掉空格、改变缩进、增删分号
    • refactor: 代码重构时使用,既不是新增功能也不是代码的bud修复
    • perf: 提高性能的修改
    • test: 添加或修改测试代码
    • build: 构建工具或者外部依赖包的修改,比如更新依赖包的版本
    • ci: 持续集成的配置文件或者脚本的修改
    • chore: 杂项,其他不需要修改源代码或不需要修改测试代码的修改
    • revert: 撤销某次提交
  • scope

用于说明本次提交的影响范围。

scope

依据项目而定,例如在业务项目中可以依据菜单或者功能模块划分,如果是组件库开发,则可以依据组件划分。

  • subject

主题包含对更改的简洁描述:

注意三点:

  1. 使用祈使语气,现在时,比如使用 “change” 而不是 “changed” 或者 ”changes“
  2. 第一个字母不要大写
  3. 末尾不要以.结尾
  • Body

主要包含对主题的进一步描述,同样的,应该使用祈使语气,包含本次修改的动机并将其与之前的行为进行对比。

  • Footer

包含此次提交有关重大更改的信息,引用此次提交关闭的issue地址,如果代码的提交是不兼容变更或关闭缺陷,则Footer必需,否则可以省略。

使用方法:




commitizen



cz-conventional-changelog

如果需要在项目中使用

commitizen

生成符合

AngularJS

规范的提交说明,还需要安装

cz-conventional-changelog

适配器。

$ pnpm install -wD commitizen cz-conventional-changelog

工程根目录下的

package.json

中增加一条脚本:

"scripts": {
  "commit": "cz"
}

接下来就可以使用

$ pnpm commit

来代替

$ git commit

进行代码提交了,看到下面的效果就表示已经安装成功了。

请添加图片描述



commitlint && husky

前面我们提到,通过

commitizen

&&

cz-conventional-changelog

可以规范我们的

commit message

,但是同时也存在一个问题,如果用户不通过

pnpm commit

来提交代码,而是直接通过

git commit

命令来提交代码,就能绕开

commit message

检查,这是我们不希望看到的。

因此接下来我们使用

commitlint

结合

husky

来对我们的提交行为进行约束。在

git commit

提交之前使用

git

钩子来验证信息,阻止不符合规范的

commit

提交。

安装

commitlint



husky

$ pnpm install -wD @commitlint/cli @commitlint/config-conventional husky

在工程根目录下增加

commitlint.config.js

配置文件,指定

commitlint

的校验配置文件:

module.exports = { extends: ['@commitlint/config-conventional'] };

husky 配置(husky的每个版本配置不一样,具体可以参考

官方文档

,当前的husky是v8.0.1)。

工程根目录下的 package.json 中增加一条 script:

"scripts": {
  "postinstall": "husky install"
}

该脚本会在执行完

$ pnpm install

之后自动执行,进行 husky 的初始化,执行完毕后就会在根目录下创建一个

.husky

目录。

执行如下命令新增一个husky的hook:

$ npx husky add .husky/commit-msg 'npx --no-install commitlint --edit "$1"'

当我们通过 git commit 提交不符合规范的代码,就会出现如下报错,并且自动退出提交流程。

请添加图片描述



代码规范检查

良好的代码编写规范对团队的可持续发展起着至关重要的作用,因此接下来我会配置

eslint

对代码进行统一的规范校验,配合

lint-staged

可以对已经提交的代码进行校验。

首选需要安装

eslint



lint-stage

$ pnpm install -wD eslint lint-staged @typescript-eslint/parser @typescript-eslint/eslint-plugin

在根成根目录下添加

.eslintrc

配置文件:

module.exports = {
  'parser': '@typescript-eslint/parser',
  'plugins': ['@typescript-eslint'],
  'rules': {
    'no-var': 'error',// 不能使用var声明变量
    'no-extra-semi': 'error',
    '@typescript-eslint/indent': ['error', 2],
    'import/extensions': 'off',
    'linebreak-style': [0, 'error', 'windows'],
    'indent': ['error', 2, { SwitchCase: 1 }], // error类型,缩进2个空格
    'space-before-function-paren': 0, // 在函数左括号的前面是否有空格
    'eol-last': 0, // 不检测新文件末尾是否有空行
    'semi': ['error', 'always'], // 在语句后面加分号
    'quotes': ['error', 'single'],// 字符串使用单双引号,double,single
    'no-console': ['error', { allow: ['log', 'warn'] }],// 允许使用console.log()
    'arrow-parens': 0,
    'no-new': 0,//允许使用 new 关键字
    'comma-dangle': [2, 'never'], // 数组和对象键值对最后一个逗号, never参数:不能带末尾的逗号, always参数:必须带末尾的逗号,always-multiline多行模式必须带逗号,单行模式不能带逗号
    'no-undef': 0
  },
  'parserOptions': {
    'ecmaVersion': 6,
    'sourceType': 'module',
    'ecmaFeatures': {
      'modules': true
    }
  }
};


lint-staged

是 Git 里的概念,表示暂存区,

lint-staged

表示只检查暂存区中的文件。

package.json 中增加如下配置:

"lint-staged": {
    "*.ts": [
      "eslint --fix",
      "git add"
    ]
}


husky

中增加

pre-commit

校验:

$ npx husky add .husky/pre-commit "npx --no-install lint-staged"


示例代码



参考链接

先自我介绍一下,小编13年上师交大毕业,曾经在小公司待过,去过华为OPPO等大厂,18年进入阿里,直到现在。深知大多数初中级java工程师,想要升技能,往往是需要自己摸索成长或是报班学习,但对于培训机构动则近万元的学费,着实压力不小。自己不成体系的自学效率很低又漫长,而且容易碰到天花板技术停止不前。因此我收集了一份《java开发全套学习资料》送给大家,初衷也很简单,就是希望帮助到想自学又不知道该从何学起的朋友,同时减轻大家的负担。添加下方名片,即可获取全套学习资料哦



版权声明:本文为m0_67401382原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。