这篇文章旨在记录有关 Monorepo 的全部技术点,以及基于 typescript + yarn workspaces + lerna 进行多个 node package 开发管理的最佳实践。node
NOTE: 本文针对 library 开发的场景进行描述,其中的实践并不必定知足全部的 monorepo 的场景,文章也不会全面的介绍各类 monorepo 的应用场景下的方案,可是会在部分章节必要的时候作一些简单的说明。react
正如咱们所熟知的,npm 和 yarn 都是原生的 node packge 的管理工具,他们在包管理方面具备相互兼容的功能,好比,依赖管理(dependency management),包发布(publish),安装(install)等。git
npm 是随着 node 的发布一块儿发布的,属于 node 官方维护的包管理工具。github
yarn 是 Facebook 为了改善前期 npm 在大型项目中依赖安装性能而独立开发维护的包管理工具,在依赖管理方面,yarn 除了大幅提高了安装性能在,在分布发布系统中依赖一致性方面也进行了保证(yarn.lock), 从而解决了分布发布中因为依赖版本不一致而致使应用表现不一致的问题。web
npm5 以后,npm 慢慢补齐了在性能与依赖一致性方面的差距,因此若是仅仅是单独的做为项目的依赖管理来讲,使用 yarn 或者 npm 都是能够的。typescript
yarn 与 npm 的最大区别是,yarn 原生引入了 workspaces
概念,让使用 yarn 进行依赖管理的项目具有了原生的 mono-repo 的能力,而 npm 原生至今也没有对等的功能,而要在不适用 yarn 的状况下实现 workspaces 的能力,咱们不得不借助 lerna
来实现。接下来的章节,咱们会具体介绍 mono-repo
& yarn workspaces
& lerna
。express
与 mono-repo
相对应的一个概念的 multi-repo
. 这两个概念实际上是同一个问题的不一样解决方案。npm
在咱们进行多个项目开发的过程当中,总会存在一些能够复用的逻辑,这时候,咱们会经过可复用逻辑的相关性,把若干可复用逻辑封装到同一个 package 中,而后咱们可能还会指望根据 package 的相关性,将若干 package 聚合在一块儿进行管理(版本控制,git / svn),在进行 package 聚合管理的时候,就出现了两种不一样的管理方案:json
multi-repo: 最开始的时候,不少开发者会为每一个独立的 package 建立一个 git 仓库,多个 package 就会存在多个相互独立的 git 仓库,这就是所谓的 multi-repo
.bootstrap
mono-repo(多包仓库): multi-repo 最大的问题在于同一个开发者同时维护多个 package, 且这些 package 之间还存在相互依赖的时候,管理起来会至关的麻烦,不只须要检出多个仓库,并且须要在某个 package 更新以后,手动的去更新他的依赖方,并且不一样 package 的相同依赖须要安装多份,因此为了解决上述问题,提升开发效率,mono-repo
诞生了,mono-repo 会在同一个 git 仓库中管理一组具备相关性的 package, 不一样 package 之间的共同依赖会被提高,并且 package 之间的相互依赖也会在其中一个 package 更新后自动更新其依赖(也能够不自动升级,下面的章节进行介绍),并发布新的版本。
因此,从上一章节的介绍中,咱们能够了解到使用 mono-repo 的硬核指标就是:就有相关性的一组 package 的管理
,咱们能够尝试问本身如下几个问题来肯定咱们是否须要使用 mono-repo:
正在维护的这一组 packages 具备相关性么?好比:都是一些工具类的库,或者是是一个系列工具的不一样部件的封装(react),或者是一个体系系统下的不一样插件(bable)
这些 packages 之间是否相互依赖?好比有一个 core package, 被多个其余 package 所依赖,而且须要在 core 版本发生变化后,升级依赖方的版本
这些 packages 之间是否有比较多得共同依赖?(具备局限性,适当参考)
从这一章节开始,咱们就须要动手来实践,到底怎么建立 mono-repo, 以及集成一些最佳实践,让咱们的项目更加标准与高效。
建立 mono-repo 业内其实有多种方案,鉴于笔者精力,本文只介绍其中比较流行且比较活跃的两种方案 yarn workspaces
和 lerna
.
咱们先简单的对比下这两种方案:
相同点
均可以独立的建立 mono-repo
packages 之间的相互依赖(如下以 local dependency 来讲明)均可以使用 syslink 来连接到本地,也能够是直接安装已经发布到指定 registry 中的版本
packages 的共同依赖均可以提高到根目录,避免重复安装,lerna 默认状况下不会提高,须要在 bootstrap 的时候显示指定 --hoist
参数
不一样点
yarn workspaces 不具具有 local dependency 之间语义版本的自动管理,包的统一发布等,须要进入到各个包内进行手动版本更新或者发布
lerna 默认使用 npm 做为包管理工具,可是经过 npmClient
来改成 yarn, 可是若是仅仅是使用 yarn 做为依赖管理工具,yarn 和 npm 区别不大
yarn2 对 workspaces 有了不少提高,也提供了对 local dependency 版本更新时的管理,workspaces 发布等新功能,感兴趣的能够深刻研究,后续笔者实践后,也会整理一些使用心得出来,官方文档
从以上的对比中,咱们能够发现,yarn workspaces 进行 mono-repo 的管理时,其实方案是不完备的,好比进行相似 react 或者 babel 这种有明确的版本管理的 library 类型的 mono-repo 管理只使用 yarn workspaces 是不够的,可是对于一个 node 开发的小型 web 项目,该项目由 client 端,server 端以及一个本身开发的组件库组成,对于这种类型的项目,没有很强的版本管理负担(仅组件库须要),且各个子项目又具有必定的独立性,为了提升开发场景下的构建效率,仅使用 yarn workspaces 来建立 mono-repo 是很是轻量的选择。
因此进行 library 的多包仓库的管理时,推荐的方案是 yarn workspaces + lerna 组合,没错,这两个方案是能够完美结合的,自己 yarn 的 workspaces 就是一个偏底层的能力,而 lerna 自己也仅是利用 npm 或 yarn 提供的能力来工做的,因此咱们能够切换 larna 底层的多包能力为 yarn workspaces 而同时使用 leran 额外的命令来进行包的版本管理和发布。
完整的项目能够参考 github.com/dancon/mono… 。
笔者假设各位看官已经安装 node 以及全局安装 yarn 1.x 或者 2.x ,若是没有,能够参考如下文档自行安装:
在 github 建立一个远程项目,远程地址为
检出远程仓库, 并进行项目初始化,这里使用 monorepo
进行演示
# 检出代码库 git clone <remote-repo url> # 进入项目根目录 cd monorepo # 初始化为 npm 项目 yarn init --yes 复制代码
<remote-repo url>
替换为你建立的 github 仓库地址
至此项目只有一个 package.json
文件
package.json
, 具体能够参考 classic.yarnpkg.com/en/docs/wor…{
"private": true, // 避免根项目被发布出去
"workspaces": [
"packages/*"
] // 暂时填写为 packages/* 指定 workspace 位置为 packages
}
复制代码
# 确保在 monorepo 目录下,建立 packages 目录,做为 yarn workspace 或者子项目 mkdir packages 复制代码
至此,项目已是在 yarn workspaces 模式下工做。
# 安装 lerna, 注意,使用 yarn add -W 将 lerna 安装到根目录 yarn add -W -D lerna # lerna 初始化, 使用 independent 模式 yarn lerna init --independent 复制代码
yarn add
的时候,若是没有指定-W
参数会报错自行建立 .gitignore 文件
生成 lerna.json 配置文件,内容以下:
{
"packages": [
"packages/*"
],
"version": "independent"
}
复制代码
添加如下配置,让 lerna 切换使用 yarn 进行依赖管理,而且使用 yarn workspaces
{
// 此项配置为可选,在部分 IDE(VS Code 支持)中会读这个 json schema 进行配置项的智能提示
"$schema": "http://json.schemastore.org/lerna",
"packages": [
"packages/*"
],
"version": "independent",
"npmClient": "yarn", // 告知 lerna 使用 yarn 做为包管理工具
"useWorkspaces": true // 使用 yarn workspaces
}
复制代码
至此,已经支持使用 yarn workspaces + lerna 进行管理了。
# 安装依赖 yarn add -W -D typescript # 在更目录下初始化 yarn tsc --init 复制代码
修改根目录下的 tsconfig.json 为以下内容
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"allowJs": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"strict": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true
},
// 排除一些不须要处理的文件,在后面的章节中会详细介绍其中的一些文件以及文件夹
"exclude": [
"node_modules",
"packages/**/node_modules",
"packages/**/lib",
"packages/**/test",
"packages/**/*.test.ts",
"packages/**/jest.config.js"
]
}
复制代码
# 接下来建立两个 package 分别为 core 和 pkg1 cd packages mkdir core mkdir pkg1 复制代码
每一个 package 的目录结构以下:
. ├── src │ └── index.ts # 源码目录 ├── jest.config.js # jest 配置 yarn jest --init ├── package.json # yarn init --yes └── tsconfig.json # package 的 ts 配置 复制代码
jest.config.js
的具体配置在下面的章节中重点说明
package.json
着重说明如下配置:
{
"name": "@scope/core",
"main": "lib/index.js",
"types": "lib/index.d.ts",
"publishConfig": { // 若是咱们的包名包含在一个特殊的 @scope 下,为了能让包正常发布,必须添加该项配置
"access": "public"
},
"devDependencies": {
"@types/node": "^13.13.5"
},
"scripts": {
"test": "jest",
"build": "rm -rf lib && tsc" // 添加构建脚本
}
}
复制代码
tsconfig.json
配置内容以下:
{
"extends": "../../tsconfig.json", // 继承根目录下的 tsconfig.json 配置
"compilerOptions": {
"rootDir": "src",
"outDir": "lib"
}
}
复制代码
接下来重点说明 mono-repo 中项目依赖管理,涉及如下几个主题:
包的安装
公共包安装
为全部 package 安装包
安装本地依赖
项目结合 lerna + workspaces, 因此包的安装方式有两种:
# 经过 yarn workspace 安装 yarn add -W <package-name> yarn workspace <workspace-name> add <package-name> # <workspace-name> 为对应包 package.json 中的 name # 经过 lerna 安装 lerna add <package-name> # 为全部 packages/* 安装依赖 lerna add <package-name> --scope <workspace-name> # 效果同 yarn workspace 复制代码
NOTE:
yarn add -W 效果和 lerna add 不指定
--scope
是不等价的yarn add -W 是安装通用依赖,更新根目录下的 package.json
lerna add 不指定 --scope 是为全部的 package 安装依赖,更新全部 packages/**/package.json
若是咱们须要为本身的包添加一个本地依赖,好比为 @scope/pkg1 添加 @scope/core 做为依赖,咱们既可使用 yarn workspace 或者 lerna add --scope 来安装
yarn workspace @scope/pkg1 add @scope/core@1.0.0 # 等价于 lerna add @scope/core@1.0.0 --scope @scope/pkg1 复制代码
以上方式指定了与本地 @scope/core 相同或者兼容的版本,因此 @scope/core 实际上是经过 syslinks 的方式指向本地仓库
若是咱们不指定版本,则 yarn 或者 lerna 会从 npm registry 中拉取在线包,这时候,咱们项目中引用的就是 node_modules 中的包
如下图为 local dependency,指定相同或者兼容的版本号
不指定版本
版本管理与发布
yarn workspaces 并无提供统一的版本管理与发布,若是不是 lerna, 咱们也能够单独使用 yarn version,yarn publish 来进行管理,可是相互之间的依赖都须要人工管理,低效而易错。
使用 lerna 利用 commit convention 经过 git message 来自动的进行版本管理,而且在包版本变动后,自动更新依赖方的版本,而且能够各个包自动生成 CHANGELOG.md, 为了使用 lerna 的这些能力,咱们只需进行如下配置:
lerna.json
{
"packages": [
"packages/*"
],
"version": "independent",
"npmClient": "yarn",
"useWorkspaces": true,
"command": {
"publish": {
"conventionalCommits": true, // 启用 commit convention 自动进行版本管理
"registry": "https://registry.npmjs.org/", // 指定 registry
"message": "chore: publish" // lerna 自动提交时的 commit message 前缀
},
"version": {
"message": "chore: publish"
}
}
}
复制代码
第一次发布的时候,无需经过 commit message 来自动升级版本,这时候,咱们可使用 from-package
lerna publish from-package
复制代码
根目录下的 package.json
添加以下 scripts
命令
{
"scripts": {
"release": "lerna publish", // 添加 release script
"build": "lerna exec --stream yarn build",
"test": "lerna exec -- yarn test --passWithNoTests",
"lint": "eslint --ext js,jsx,ts,tsx packages --fix",
"gen": "plop"
}
}
复制代码
而后,后续的发布中经过 yarn 来执行
# 第一次发布 yarn release from-package # 以后的发布 yarn release 复制代码
关于 commit convention 能够参考: Conventional Commits
mono-repo 中,关于 commit message 须要单独说明的是,在每次提交的时候,最好为每一个包的修改指定对应的 scope, 好比:
git commit -m 'feat(core): add a new feature'
咱们也能够利用 commitlint 来规范咱们的提交信息,后面的章节会详细介绍其使用。
接下来咱们着重介绍一些经常使用的命令:
yarn workspaces info
展现当前项目中全部 workspace 的依赖关系执行结果以下:
yarn workspaces v1.21.1 { "@pandolajs-test/core": { "location": "packages/core", "workspaceDependencies": [], "mismatchedWorkspaceDependencies": [] }, "@pandolajs-test/pkg1": { "location": "packages/pkg1", "workspaceDependencies": [ "@pandolajs-test/core" ], "mismatchedWorkspaceDependencies": [] } } ✨ Done in 0.04s. 复制代码
yarn workspaces run <command>
在全部 workspace 中执行 <command>
lerna 中的等价命令:
lerna run <command>
yarn workspace <workspace> <command>
在指定的 <workspace>
中执行 yarn 的命令官方文档:GitHub - lerna/lerna: A tool for managing JavaScript projects with multiple packages.
经常使用的全局参数
经常使用的命令
lerna bootstrap
lerna add
lerna publish
lerna run
lerna exec
每一个 workspace 中的 tsconfig.json 继承根目录下的 tsconfig.json 提高通用配置。
yarn add -W -D typescript
复制代码
yarn tsc --init
复制代码
每一个 workspace 中使用本身的 jest 配置
yarn add -W -D jest @types/jest ts-jest
复制代码
yarn jest --init
复制代码
{
"preset": "ts-jest",
}
复制代码
更多查看文档:www.robertcooper.me/using-eslin…
yarn add -W -D eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-config-prettier eslint-config-standard eslint-config-standard-with-typescript eslint-plugin-import eslint-plugin-jest eslint-plugin-node eslint-plugin-prettier eslint-plugin-promise eslint-plugin-standard
复制代码
.eslintrc.json
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 2020,
sourceType: 'module',
tsconfigRootDir: __dirname,
project: [
'./packages/**/tsconfig.json'
]
},
env: {
node: true
},
plugins: [
'@typescript-eslint',
'jest',
'prettier'
],
extends: [
'eslint:recommended',
'standard-with-typescript',
'prettier/@typescript-eslint',
'plugin:jest/recommended',
'plugin:prettier/recommended'
],
rules: {
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/triple-slash-reference': 'off',
'@typescript-eslint/strict-boolean-expressions': 'off'
}
}
复制代码
.vscode/setting.json
{
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
}
}
复制代码
.prettierrc.json
{
"semi": false,
"tabWidth": 2,
"singleQuote": true,
"trailingComma": "none"
}
复制代码
可根据本身口味进行修改
yarn add -W -D husky lint-staged
复制代码
.lintstagedrc
{
"packages/**/*.{js,ts,jsx,tsx}": [
"eslint --fix"
]
}
复制代码
.huskrc.json
{
"hooks": {
// commitlint 配置的 git hook
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS",
// 注意这里 --since HEAD 的做用 参考:https://github.com/lerna/lerna/tree/master/core/filter-options#--since-ref
"pre-commit": "lerna exec --concurrency 1 --stream lint-staged --since HEAD"
}
}
复制代码
官方文档:commitlint - Lint commit messages
yarn add -W -D @commitlint/cli @commitlint/config-conventional @commitlint/config-lerna-scopes
复制代码
.commitlintrc.json
{
"extends": [
"@commitlint/config-conventional",
"@commitlint/config-lerna-scopes"
]
}
复制代码
官方文档:GitHub - plopjs/plop: Consistency Made Simple
具体参考代码库实现:GitHub - dancon/monorepo
yarn add -W -D plop
复制代码
plopfile.js
module.exports = function(plop) { // ... } 复制代码
经常使用方法
setGenerator
setHelper
项目根目录配置如下 script
{
"scripts": {
"gen": "plop"
}
}
复制代码
而后执行 yarn gen [template-name]
就能够建立你配置的模板,或者不指定 [template-name]
会列出全部你配置的模板。更多使用方式能够参考官方文档。