基于 Yarn WorkSpace + Lerna + OrangeCI 搭建 Typescript Monorepo 项目实践

栏目: IT技术 · 发布时间: 4年前

内容简介:Lerna 已然成为搭建 monorepo 工程的首选,然而官方文档[1]并没有给出构建 monorepo 项目最后一公里的解决方案。而在这次在迁移搭建全民 K 歌基础库的实践中,在诸如 Orange CI 自动发布 npm 包等问题上就遇到了不少阻碍,我们把经验总结记录如下。Orange CI:腾讯内部开源的持续集成服务,类似于 Travis CI,一旦代码有变更,就自动运行构建和发布,并输出结果,是实现自动更新版本号及发布npm包的基础。

Lerna 已然成为搭建 monorepo 工程的首选,然而官方文档[1]并没有给出构建 monorepo 项目最后一公里的解决方案。而在这次在迁移搭建全民 K 歌基础库的实践中,在诸如 Orange CI 自动发布 npm 包等问题上就遇到了不少阻碍,我们把经验总结记录如下。

名词解释 :

Orange CI:腾讯内部开源的持续集成服务,类似于 Travis CI,一旦代码有变更,就自动运行构建和发布,并输出结果,是实现自动更新版本号及发布npm包的基础。

Monorepo:一种管理组织代码的方式,其主要特点是多个项目的代码存储在同一个 git repo 中

Multirepo:一种管理组织代码的方式,其主要特点是多个项目的代码存储在不同 git repo 中

一. 背景

早期全民 K 歌 web 项目基础库是夹杂在业务项目中,存在着许多问题

  • 基础库潜藏在业务代码

  • 基础库没有按照 package 分类

  • 不适合快速迭代开发

  • 难以对代码追踪溯源

  • 无版本号管理,无代码变更文档

  • 无代码使用文档

所以要更好管理基础库代码,从业务项目迁移基础库代码、独立发布 npm 包是解决问题的关键。

二. 代码管理方案对比

1. Git Submodule 、Git Subtree

优点:方便项目回馈更改

缺点:协同开发分支多、子模块数量多,管理成本高

2. Multirepo 划分为多个模块,一个模块一个 Git Repo

优点:模块划分清晰,每个模块都是独立的 repo,利于团队协作

缺点:由于依赖关系,所以版本号需要手动控制、调试麻烦、issue 难以管理

3. Monorepo 划分多个模块,所有模块均在一个 Git Repo

优点:代码统一管理、方便统一处理 issue 和生成 ChangeLog、调试代码 npm/yarn link 一把梭

缺点:统一构建、CI、测试和发布流程带来的技术挑战、项目体积变得更大

基于 Yarn WorkSpace + Lerna + OrangeCI 搭建 Typescript Monorepo 项目实践 )

一图胜千言,很显然 Monorepo 是解决这次问题的最优解。所以接下来要在项目内采用 lerna + yarn workspace 架构,使用 Typescript 语言编写代码,利用 Orange CI 去完成版本号、ChangeLog 迭代。

三. 改造的实现

1. 依赖管理

由于 Monorepo 的特性,各个 package 之间可能会形成相互依赖,手动进行 npm link 对于多 package 的 Monorepo 来说,无疑是个巨大的负担,因此我们需要一个自动化的 npm link 操作脚本。

其实了解 Lerna 用法的同学都知道,这里只用 Lerna 的命令 lerna bootstrap 可以完美的解决这个问题,但在这里,我使用 Yarn workSpace 代替 npm,除了保证 package 相互依赖, Yarn 还带来显著的优点。

  1. Yarn 只使用唯一的 yarn.lock 文件,而不是每个项目都有一个 package-lock.json ,这能降低很多潜在性的冲突。

  2. lerna bootstap 会重复安装相同的依赖项。

  3. yarn why <query> 命令,能提示为什么安装一个 package,还有什么 package 是依赖该 package,这就方便我们方便理清 monorepo 的依赖关系。

  4. Yarn workspace 是 Lerna 利用的底层机制,而且 Lerna 支持与 Yarn 协同工作。

使用 Yarn workspace,需要在根目录 package.json 添加以下内容

// package.json
{
  "name": "root",
  "private": true,
  "workspaces": ["packages/*"]
}

2. 项目初始化

lerna 初始化项目(采用 independent 管理模式)

lerna init --independent

新增 packages

lerna create @tencent/pkg1
lerna create @tencent/pkg2
// pkg1/package.json 配置
// pkg2/package.json 同理
{
 "name": "pkg1",
 "version": "0.0.1",
 "main": "lib/index.js", // 输出目录为lib
 "types": "./lib/index.d.ts" // 声明文件
}

根目录安装 Typescript 依赖

yarn add typescript -W -D

Typescript 完成初始化

// 根目录新建tsconfig.json
{
  "compilerOptions": {
    "module": "es2015",
    "target": "es5",
    "lib": ["esnext", "dom"],
    "baseUrl": "./packages",
    "paths": {
      "@tencent/*": ["*/src"]
    },
  },
  "include": ["packages/*"],
  "exclude": [
    "node_modules",
    "lib"
  ]
}

这个配置对于每个包都是相同的,并且是完全可选的。如果想为每个包分别定制设置,那么可以创建一个该 package 的 tsconfig.json ,否则根目录的 tsconfig.json 就会起作用。

这里根目录 tsconfig.json 的 paths 是这里的神奇之处:它告诉 TypeScript 编译器,每当一个模块尝试从 monorepo 导入另一个模块时,它都应该从 packages 文件夹中解析它。具体来说,它应指向该包的 src 文件夹,因为这是构建时将编译的文件夹。除此之外,在 IDE 点击依赖包的方法,就会跳转对应的源代码。

然而 compilerOptions.outDir compilerOptions.include 不能提升至根目录的 tsconfig,因为它们是相对于它们所在的配置进行解析的。(详见issue[2])

// 各package的tsconfig.json
{
  "extends": "../../tsconfig.json",

  "compilerOptions": {
    "outDir": "./lib"
  },

  "include": [
    "src/**/*"
  ]
}

到目前为止,最基本的 Monorepo + Yarn + Typescript 项目目录结构如下。

├── lerna.json
├── yarn.lock
├── package.json
├── packages
│   ├── pkg1
│   │   ├── package.json
│   │   ├── src
│   │   │   └── index.ts
│   │   └── tsconfig.json
│   └── pkg2
│       ├── package.json
│       ├── src
│       │   └── index.ts
│       └── tsconfig.json
└── tsconfig.json

3. 项目构建

Monorepo 的构建区别普通项目在于,各个 package 之间会存在相互依赖,比如 packageA 依赖 packageB,必须 packageA 构建完毕后 packageB 才能进行构建,否则就会报错。

这里就涉及到项目构建的执行顺序问题,实际上是要求项目以一种拓扑 排序 的规则进行构建,这里我们有两种解决方案:

  1. 使用 lerna run 构建所有 package,并依靠 lerna 通过查看每个 package 的依赖关系以正确的顺序构建软件包。

  2. 使用 Typescript 3.0 的新特性 Project References[3]

lerna run

@lerna/run [4] 按照拓扑顺序运行每个 package 的 <script> 里的命令,这意味着如果 pkg1 依赖于 pkg2,那么 pkg2 <script> 里的命令将在 pkg1 之前运行。这个执行顺序是通过每个 package 的 package.json 中的 dependenciesdevDependencies 来确立的。

通常情况,在发布 npm run publish 之前,通常是需要触发 <script> 里的 prepublishOnly 来运行 npm run build 完成项目的构建。但在 monorepo 项目发布则需要注意一些注意事项。

当发布单个 package 时,lerna 不会为其依赖包运行 prepublishOnly 脚本。所以当 package 的依赖包没发布到 npm 前,npm install 该 package 时,npm 就会报错。

解决问题的方法是不要依赖每个 package 的 prepublishOnly 脚本,而是在发布任何一个 package 之前构建所有的 package。我们可以通过在 lerna 发布之前调用 lerna run build 来实现这一点,这将运行每个 package 的 build 脚本。

或者我们可以使用 lerna 发布命令 lerna publishlerna publish 也支持了拓扑顺序的发布,确保发布某个 package 前,其依赖项已经发布出去,

lerna publish --graph-type all
# all 包括dependencies、devDependencies 和 peerDependencies

如果经常遇到发布单独几个 package 的情况,或者只是希望能够轻松调试构建,那么 Project References 的解决方案可能更适合。

Project References

使用 Project References 可以达到 lerna 以正确的顺序运行构建项目的效果,而且还允许我们一次构建一个包。

// pkg1/tsconfig.json
{
  "extends": "../../tsconfig.json",

  "compilerOptions": {
    "composite": true,
    "outDir": "./lib",
    "rootDir": "./src"
  },

  "references": [
    {
      "path": "../pkg2/tsconfig.json"
    }
  ],

  "include": ["src/**/*"]
}

从上面的 tsconfig.json 可见,我们通过设置 composite:true ,并指定该 package 所依赖 monorepo 的其他 package,设置解释如下。

  • references 是路径的数组,在这里需要指定依赖包的 tsconfig.json 的路径。

  • 每个 package 都需要设置 composite: true ,即使它们只是引用树中的一个叶节点,也应为 true,否则 tsc 会报错。

  • rootDir 是输出正确的输出文件夹路径所必需的,否则 TypeScript 可能会推断出根文件夹目录输出不必要的嵌套文件夹。

针对构建某个 package 的情况,我们可以修改该 package 的 package.json

compile 脚本是运行  tsc --build ,而 build 脚本除了运行 compile 脚本外,还前置清除了所有 package 的输出目录,以及 tsconfig.build.tsbuildinfo tsc 的构建缓存,不然 tsc -b 将不会重新构建它。

// pkg1/package.json
{
  "scripts": {
    "dev": "npm run clean && tsc --build --watch",
    "build": "npm run clean && npm run compile",
    "clean": "rm -rf ./lib && rm -rf tsconfig.build.tsbuildinfo",
    "compile": "tsc --build",
    "prepublishOnly": "npm run build"
  }
}

而这个方案下, lerna run 将像以前一样工作,所以这个解决方案的主要优点是它允许我们调试包的构建而不用担心其他包。

回到本次基础库构建,我们并不需要针对某几个 package 发布,所以我们也可以在根目录的 tsconfig.json 设置 references ,引用所有的需要构建的 package,这样我们在根目录的 package.json 就能使用单个命令就能完成所有 packge 的构建,而不需要在每个 package 重复新增一个构建的脚本。

//tsconfig.json
{
    "compilerOptions": {
     // 忽略
    },
    "references": [{
        "path": "packages/pkg1"
    }, {
        "path": "packages/pkg2"
    }]
}

// package.json
{
  "dev": "yarn clean && tsc --build --watch",
  "clean": "rm -rf ./lib && rm -rf tsconfig.build.tsbuildinfo",
  "build": "yarn clean && tsc --build",
}

4 . 版本升级及发包

到本次文章的最后了,也是最重要的关键点,发布 npm 包。当然,结合 lerna 的文档,搞出一个能用的发布脚本是很简单的,但结合团队的实际情况,当前发布 npm 包有以下几点痛点是需要解决的:

  1. 基础库发布前,需要 Code Review

  2. 限制特定的分支发布 npm 包

  3. 通过 CI 完成项目构建,并标记修改的 package,修改其版本号以及 changelog

  4. 在个人的开发分支,需要发布临时测试用的 npm 包

Code Review

首先针对 Code Review,git repo 可以限制开发分支合并 master 前需要提 Merge Request ,Review 者通过 Merge Request 即代表该基础库通过了 Code Review,问题 1 解决。

限定 Master 分支发布 npm 包

问题 2 的解决是在问题 1 解决的基础上延伸的,当开发分支合并至 master 后,理论上在 master 分支发布 npm 包是最好的选择,所以要在限定 master 分支上发布 npm 包

//lerna.json
{
  "packages": ["packages/*"],
  "version": "independent",
  "command": {
    "version": {
      "allowBranch": "master"
    }
  },
  "useWorkspaces": true,
  "npmClient": "yarn"
}

设置 "allowBranch": "master" ,那运行  lerna version 或者 lerna publish 都只能在 master 分支上运行。

自动化流水线完成构建,生成版本号、changlog,发布

问题 3,我们使用的是 Orange CI,在 master 分支触发 git push 事件时,通过注册 orange ci 的 master push 钩子实现构建以及发布。

构建这块实现相对简单,在 package.json 包装好构建的脚本,package.json 如下:

{
  "scripts": {
    "clean": "rm -rf ./lib && rm -rf tsconfig.build.tsbuildinfo",
    "build": "yarn clean && tsc --build",
    "prepublishOnly": "npm run build"
  }
}

这里使用 prepublishOnly ,在 lerna 执行 npm publish 命令前运行,保证 lerna publish 执行前完成项目的构建。

发版的时候需要更新版本号,这时候如何更新版本号就是个问题,一般来说,版本号都是遵循 semver [5] 语义。这里需要 Orange CI 自动完成版本号更新,更好的办法是根据 git 的提交记录自动更新版本号,实际上只要我们的 git commit message 符合 Conventional commit[6] 规范,即可通过 lerna version 根据 git 提交记录,更新版本号,简单的规则如下

  1. 存在 feat 提交:需要更新 minor 版本

  2. 存在 fix 提交:需要更新 patch 版本

  3. 存在 BREAKING CHANGE 提交:需要更新大版本

为了方便查看每个 package 每个版本解决了哪些功能,我们需要给每个 package 都生成一份 changelog 方便用户查看各个版本的功能变化。同理只要我们的 commit 记录符合 conventional commit 规范,即可通过 工具 为每个 package 生成 changelog 文件

由于开发者数量较多,发布 npm 包统一使用公共账号,至于 npm 包关联开发者信息,则可以根据 git merge request 来回溯

总结下来,在 Orange CI 输出以下命令

npx lerna version --conventional-graduate --yes

npx lerna publish from-package --legacy-auth \$TNPM_USERPASSWORD_BASE64 --yes

--conventional-graduate 该标志会按照conventional commit 规范 生成版本号以及输出 changlog

from-package :将发布的 package 列表 和 npm registry 做比较。npm registry 中没有的 package 都将被发布。当一个发布失败时,这成为一个失败发布重试机制。

--legacy-auth : 输入发布 npm 包的公共账号密码,形式为  username:password ,将该字符串进行 base64 转化。这里也可以用环境变量来注入提升安全性。

--yes :运行 lerna version、lerna publish 将跳过所有确认提示

临时发布 npm 包

当开发者开发基础库时,需要在业务测试该 package,但不能以 release 的版本号发布,需要在每个 commit 能够发布一个 beta 预览版,参考 lerna 文档,建议如下:

lerna publish --canary --preid beta
# 1.0.0 => 1.0.1-beta.0+${SHA}

然而这个命令并不好使,存在几个问题,首先这个 npm 包发布至 npm dist-tag 为 latest,直接 npm install 就会安装 beta 预览版;其次, 1.0.1-beta.0+${SHA} 并不符合semver 语义,发布到 npm 后,版本号变为``1.0.1-beta.0`,beta 后的数字,很多时候并不会随着发布次数增加而增加,这里就造成了冲突

所以这时把发布命令修改如下:

lerna publish -y --canary --preid \"beta.$(git rev-parse --short HEAD)\" --pre-dist-tag=beta --legacy-auth xxx
# `0.5.7` => `0.5.7-beta.${SHA}.1

可以看出,版本号通过 preid 配置,添加了 git sha 值,保证了每个版本号是相对于 git commit 唯一的。

四. 效果 & 总结

整个流程下来,得益于企业微信的消息推送,我们能很直观的看到整个构建发布流程。

基于 Yarn WorkSpace + Lerna + OrangeCI 搭建 Typescript Monorepo 项目实践

以及发布的变更也通过上述过程自动化生成 changelog.md 并周知出来。

基于 Yarn WorkSpace + Lerna + OrangeCI 搭建 Typescript Monorepo 项目实践

整个开发构建发布 npm 包的流程图总结如下所示:

基于 Yarn WorkSpace + Lerna + OrangeCI 搭建 Typescript Monorepo 项目实践

目前方案已在团队内多个项目上线,整体提升了团队迭代维护的秩序和效率。

基于 Yarn WorkSpace + Lerna + OrangeCI 搭建 Typescript Monorepo 项目实践

基于 Yarn WorkSpace + Lerna + OrangeCI 搭建 Typescript Monorepo 项目实践

注: 文中 使用的 CI 是腾讯内部开源的   Orange CI ,但万变不离其宗, 利用  CI  发布  npm 包的核心 要义是,把  CI 模拟为本地环境,编写脚 本完成 构造 、更新版本标签、发布  npm 这一流水线。 所以即便用别的  CI 服务, 如  G ItHub 的  G itHub  A ctio n Git La 的  CI,只要 围绕这核心要义,巧妙使用 lerna,打造一个  CI 发布  n pm  包的流 水线也 是不难 的。

文中相关链接:

  1. https://lerna.js.org/

  2. http://github.com/microsoft/TypeScript/issues/29172

  3. https://www.typescriptlang.org/docs/handbook/project-references.html

  4. https://github.com/lerna/lerna/tree/master/commands/run#lernarun

  5. https://semver.org/lang/zh-CN/

  6. https://www.conventionalcommits.org/zh/v1.0.0-beta.2/

腾讯音乐全民k歌招聘客户端、web前端、后台开发,点击查看原文投递简历!或邮箱联系: godjliu@tencent.com


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

游戏人工智能编程案例精粹

游戏人工智能编程案例精粹

巴克兰德 (Mat Buckland) / 罗岱 / 人民邮电出版社 / 2008年06月 / 55.00元

《游戏人工智能编程案例精粹》适合对游戏AI开发感兴趣的爱好者和游戏AI开发人员阅读和参考。一起来看看 《游戏人工智能编程案例精粹》 这本书的介绍吧!

随机密码生成器
随机密码生成器

多种字符组合密码

HTML 编码/解码
HTML 编码/解码

HTML 编码/解码

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具