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


    本文首发于 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
    
    • 1

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

    工程初始化

    为了便于后续的演示,先在工程根目录下新建 packages 目录,并且在 packages 目录下创建 pkg1pkg2 两个工程,分别进到 pkg1pkg2 两个目录下,执行 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;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    分别在 pkg1 和 pkg2 下新增 .fatherrc.tstsconfig.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
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    全局安装 father-build:

    $ pnpm i -Dw father-build
    
    • 1

    最后在 pkg1 和 pkg2 下的 package.json 文件中增加一条 script:

    {
      "scripts": {
        "build": "father-build"
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    这样在 pkg1 或者 pkg2 下执行 build 命令就会将各子包的ts代码打包成js代码输出至 lib 目录下。

    要想启动 pnpmworkspace 功能,需要工程根目录下存在 pnpm-workspace.yaml 配置文件,并且在 pnpm-workspace.yaml 中指定工作空间的目录。比如这里我们所有的子包都是放在 packages 目录下,因此修改 pnpm-workspace.yaml 内容如下:

    packages:
      - 'packages/*'
    
    • 1
    • 2

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

    .
    ├── 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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    安装依赖包

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

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

    pnpm 提供了 -w, --workspace-root 参数,可以将依赖包安装到工程的根目录下,作为所有 package 的公共依赖。

    比如:

    $ pnpm install react -w
    
    • 1

    如果是一个开发依赖的话,可以加上 -D 参数,表示这是一个开发依赖,会装到 pacakage.json 中的 devDependencies 中,比如:

    $ pnpm install rollup -wD
    
    • 1
    • 给某个package单独安装指定依赖

    pnpm 提供了 –filter 参数,可以用来对特定的package进行某些操作。

    因此,如果想给 pkg1 安装一个依赖包,比如 axios,可以进行如下操作:

    $ pnpm add axios --filter @qftjs/monorepo1
    
    • 1

    需要注意的是,--filter 参数跟着的是package下的 package.jsonname 字段,并不是目录名。

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

    $ pnpm build --filter @qftjs/monorepo1
    
    • 1

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

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

    此命令会执行所有 package 下的 build 命令。具体的用法可以参考filter文档。

    • 模块之间的相互依赖

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

    基于 pnpm 提供的 workspace:协议,可以方便的在 packages 内部进行互相引用。比如在 pkg1 中引用 pkg2:

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

    此时我们查看 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"
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    在设置依赖版本的时候推荐用 workspace:*,这样就可以保持依赖的版本是工作空间里最新版本,不需要每次手动更新依赖版本。

    pnpm publish 的时候,会自动将 package.json 中的 workspace 修正为对应的版本号。

    只允许pnpm

    当在项目中使用 pnpm 时,如果不希望用户使用 yarn 或者 npm 安装依赖,可以将下面的这个 preinstall 脚本添加到工程根目录下的 package.json中:

    {
      "scripts": {
        "preinstall": "npx only-allow pnpm"
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    preinstall 脚本会在 install 之前执行,现在,只要有人运行 npm installyarn install,就会调用 only-allow 去限制只允许使用 pnpm 安装依赖。

    Release工作流

    workspace 中对包版本管理是一个非常复杂的工作,遗憾的是 pnpm 没有提供内置的解决方案,一部分开源项目在自己的项目中自己实现了一套包版本的管理机制,比如 Vue3Vite 等。

    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
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    说明如下:

    • 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

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

    为了便于统一管理所有包的发布过程,在工程根目录下的 pacakge.jsonscripts 中增加如下几条脚本:

    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”
      }

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

    这里还有个细节,如果我不想直接发 release 版本,而是想先发一个带 tagprerelease版本呢(比如beta或者rc版本)?

    这里提供了两种方式:

    • 手工调整

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

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

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

    然后运行:

    $ pnpm changeset publish --tag beta
    
    • 1

    注意发包的时候不要忘记加上 --tag 参数。

    • 通过 changeset 提供的 Prereleases 模式

      利用官方提供的 Prereleases 模式,通过 pre enter 命令进入先进入 pre 模式。

    常见的tag如下所示:

    名称

    功能

    alpha

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

    beta

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

    rc

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

    $ pnpm changeset pre enter beta
    
    • 1

    之后在此模式下的 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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    完成版本发布之后,退出 Prereleases 模式:

    $ pnpm changeset pre exit
    
    • 1
    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 可以选填。

    (): 
    // 空一行
    
    // 空一行
    
    • 1
    • 2
    • 3
    • 4
    • 5

    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必需,否则可以省略。

    使用方法:

    commitizencz-conventional-changelog

    如果需要在项目中使用 commitizen 生成符合 AngularJS 规范的提交说明,还需要安装 cz-conventional-changelog 适配器。

    $ pnpm install -wD commitizen cz-conventional-changelog
    
    • 1

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

    "scripts": {
      "commit": "cz"
    }
    
    • 1
    • 2
    • 3

    接下来就可以使用 $ pnpm commit 来代替 $ git commit 进行代码提交了,看到下面的效果就表示已经安装成功了。

    请添加图片描述

    commitlint && husky

    前面我们提到,通过 commitizen && cz-conventional-changelog 可以规范我们的 commit message,但是同时也存在一个问题,如果用户不通过 pnpm commit 来提交代码,而是直接通过 git commit 命令来提交代码,就能绕开 commit message 检查,这是我们不希望看到的。

    因此接下来我们使用 commitlint 结合 husky 来对我们的提交行为进行约束。在 git commit 提交之前使用 git 钩子来验证信息,阻止不符合规范的commit 提交。

    安装 commitlinthusky

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

    在工程根目录下增加 commitlint.config.js 配置文件,指定 commitlint 的校验配置文件:

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

    husky 配置(husky的每个版本配置不一样,具体可以参考官方文档,当前的husky是v8.0.1)。

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

    "scripts": {
      "postinstall": "husky install"
    }
    
    • 1
    • 2
    • 3

    该脚本会在执行完 $ pnpm install 之后自动执行,进行 husky 的初始化,执行完毕后就会在根目录下创建一个 .husky 目录。

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

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

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

    请添加图片描述

    代码规范检查

    良好的代码编写规范对团队的可持续发展起着至关重要的作用,因此接下来我会配置 eslint 对代码进行统一的规范校验,配合 lint-staged 可以对已经提交的代码进行校验。

    首选需要安装 eslintlint-stage

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

    在根成根目录下添加 .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
        }
      }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    lint-staged 是 Git 里的概念,表示暂存区,lint-staged 表示只检查暂存区中的文件。

    package.json 中增加如下配置:

    "lint-staged": {
        "*.ts": [
          "eslint --fix",
          "git add"
        ]
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    husky 中增加 pre-commit 校验:

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

    示例代码

    参考链接

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

  • 相关阅读:
    zookeeper 进阶 —— (动态上下线监听;;分布式锁;;企业面试)(师承尚硅谷)
    React 消息文本循环展示
    SpringCloud的新闻资讯项目07 --- app端文章搜索
    企业电子招标采购系统源码Spring Boot + Mybatis + Redis + Layui + 前后端分离 构建企业电子招采平台之立项流程图
    信号隔离、电源隔离介绍
    基于K8s构建Jenkins持续集成平台(部署流程)(转)
    利用学信网免费激活PyCharm企业版(也适用所有其它JetBrains的IDE)
    python提取word文本和word图片
    1015;计算并联电阻的阻值(信奥一本通 )
    Linux-Docker的基础命令和部署code-server
  • 原文地址:https://blog.csdn.net/m0_67401382/article/details/126113862