Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

浅谈前端 monorepo 模式 #27

Open
hanrenguang opened this issue May 9, 2021 · 0 comments
Open

浅谈前端 monorepo 模式 #27

hanrenguang opened this issue May 9, 2021 · 0 comments

Comments

@hanrenguang
Copy link
Owner

hanrenguang commented May 9, 2021

在版本控制系统中,monorepomono 意为单个的,repo 即代码仓库)是一种软件开发策略:将多个项目的代码整合到单个仓库中进行管理。

PS:目前笔者正准备开发一个开源工具,预计会拆分为多个包,于是准备使用 monorepo 模式来进行项目管理,本文的内容是笔者实践所得,如有不足之处欢迎指出!

monorepo 是什么

与每个项目/包各自存储在一个仓库的 multi-repo 模式相对,monorepo 是把所有包都存放到一个仓库集中管理。一个 monorepo 的项目目录大概如下所示:

monorepo/
  package.json
  packages/
    package-1/
      package.json
    package-2/
      package.json

所有的包都存放在 packages (也可以是任意自定义的)目录下,每个包都是一个独立的项目,但他们之间又存在紧密的联系,或者说依赖关系,在笔者看来这是使用 monorepo前提

为什么选择 monorepo

其实起因是笔者观察到很多开源项目,比如 VueReact 等都是使用的 monorepo 方式进行项目管理。遂进行了一番了解,在实践中也更好地感受到这种模式的优势:

  • 依赖提升(hoist

    多个互相关联的包大多都存在很多相同的依赖,在 monorepo 模式下,这些相同的依赖会被提升到根目录下下载并存储。node.js 解析依赖的方式是逐级向上查找 node_modules 目录,所以通过这种方式,能够极大减少依赖重复安装,并保证了依赖版本的一致性。安装依赖后的目录如下:

    monorepo/
      node_modules/       # 公共依赖项
      package.json
      packages/
        package-1/
          node_modules/   # package-1 独有的依赖
          package.json
        package-2/
          node_modules/   # package-2 独有的依赖
          package.json
  • 代码复用更方便

    相互关联的包之间通常都会有一些通用的方法,在 multi-repo 的模式下,这些通用函数往往存在于多个仓库中,当某个函数更改时,还需要手动同步所有仓库,不仅繁琐而且容错率低。monorepo 项目通过把通用函数抽离到类似于 root/packages/shared/ 这样的目录下(可以通过 yarn/npm link 该目录或者 webpack alias 甚至直接通过相对路径引用的形式进行代码复用)。现在,所有通用函数都被抽离到独立的包中管理,只需要维护这一份代码即可。

    monorepo/
      node_modules/       # 公共依赖项
      package.json
      packages/
        package-1/
          node_modules/   # package-1 独有的依赖
          package.json
        package-2/
          node_modules/   # package-2 独有的依赖
          package.json
        shared/           # 公共方法包
  • 原子提交特性

    对于原子提交,笔者的理解是:在 monorepo 中,多个项目之间相互关联,每次代码提交覆盖到所有的项目更改,较大限度地保证了各项目之间版本的兼容性。

  • ......

任何事物都有相对性,monorepo 也有这样一些缺点:

  • 基本放弃了项目权限控制:所有的包都存储在单个仓库,意味着所有能访问该仓库的人员都能访问所有的包。
  • 更大的存储空间:当某些开发人员只更改某个包时,monorepo 对这些开发人员来说造成了很大的冗余(多余的包,多余的依赖)。
  • 代码检索更困难:随着仓库代码量的增大,进行全量检索时,会花费更多的时间。
  • ......

monorepo 实践

让我们先从最简单的方式入手,创建一个 monorepo 项目,随后逐步添加新的工具。

PS:本文不会深入到各种细节,如 lerna 配置详解、 TS 配置如何复用之类的

第一版

你只需要 yarn/npm 就能实现。首先让我们新建一个文件夹作为我们的 monorepo 项目根目录,根目录下的 package.jsonprivate 字段记得设置为 true,因为我们并不想发布我们的 monorepo 整个项目:

$ mkdir test-monorepo
$ cd test-monorepo
$ yarn init

接着我们创建 packages 目录,并添加 package-1package-2 两个包,并用 yarn 初始化它们,现在我们的项目文件结构是这样的:

test-monorepo/
  package.json
  packages/
    package-1/
      package.json
    package-2/
      package.json

我们l两个项目都使用了 lodashpackage-1package-2package.json 依赖如下:

{
  "dependencies": {
    "lodash": "^4.17.21"
  }
}

为了让依赖提升,现在让我们回到项目根目录(test-monorepo),添加依赖:

$ yarn add lodash -D

现在的目录结构:

test-monorepo/
  node_modules/
    lodash/
  package.json
  packages/
    package-1/
      package.json
    package-2/
      package.json

假设 package-1 依赖 package-2,我们使用 yarn linkpackage-2link 到根目录下:

# package-2/
$ yarn link
# return to test-monorepo/
$ yarn link package-2

现在我们的目录结构如下:

test-monorepo/
  node_modules/
    lodash/
    package-2/   # link
  package.json
  packages/
    package-1/
      package.json
    package-2/
      package.json

好了,我们可以在 package-1 中通过 require('package-2') 获得 package-2 模块了。开发完成,提交代码,然后我们分别进入 package-1package-2 目录下发布包:

# package-1/
$ yarn publish
# package-2/
$ yarn publish

第一版小结:仅仅通过 yarn 的基本命令及我们勤劳的双手(手动狗头),我们就创建了一个 monorepo 项目,体会到了 monorepo 带来的优势。不过嘛,这也太繁琐了,我们奔走于各个目录安装依赖,体现不出我们的X格啊,于是该 yarn workspaces 登场了。

第二版

yarn workspaces 是一种新的包管理架构,从 Yarn 1.0 版本开始支持。它提供了运行一次 yarn install 命令即可设置好多个 package 依赖的能力。(PS:英语渣翻译可能不太好,可以参照以下原文)

Workspaces are a new way to set up your package architecture that’s available by default starting from Yarn 1.0. It allows you to setup multiple packages in such a way that you only need to run yarn install once to install all of them in a single pass.

那么 yarn workspaces 能给我们带来什么好处呢?

  • 所有的依赖都会被下载到根目录下的 node_modules,同时,工作区的所有包都会被 link 到根目录下的 node_modules。而你只需要在根目录 yarn or yarn install 即可;
  • 只有一个 yarn.lock 文件,在根目录下,避免了依赖的版本冲突。
  • ......

回到第一版结尾的目录结构:

test-monorepo/
  node_modules/
    lodash/
    package-2/   # link
  package.json
  packages/
    package-1/
      package.json
    package-2/
      package.json

现在,删除根目录下的 node_modules,修改根目录下的 package.json,添加 workspaces 字段,并清除所有依赖:

{
  "workspaces": [
    "packages/*"
  ]
}

然后,只需要在根目录执行:

$ yarn install

现在,你将观察到项目的目录结构如下:

test-monorepo/
  node_modules/
    lodash/
    package-1/   # link
    package-2/   # link
  package.json
  packages/
    package-1/
      package.json
    package-2/
      package.json

同时,通过以下命令可以轻松做到给各个包添加依赖并自动下载到根目录下的 node_modules,而无需手动修改包的 package.jsonyarn install

# 给 package-1 添加 cross-env 依赖
$ yarn workspace package-1 add cross-env
# 如果要添加 monorepo 里的包作为依赖,则必须指定正确的版本号
$ yarn workspace package-1 add package-2@1.0.0

瞬间清爽了!

第二版小结:我们通过 yarn workspaces 进行依赖包管理,摆脱了原始的依赖管理方式,通过几个命令即可在根目录下管理所有的依赖。我们已经前进了一大步,但这还不够,还记得我们发包的过程吗,进入每个包目录,运行 yarn publish。我们的发包过程还是相对繁琐。接下来,我们将引入 lerna 帮助我们进一步提高效率。

第三版

lerna 是一个用于优化基于 gitnpmmonorepo 项目管理流程的工具。

lerna 提供了创建和管理 monorepo 项目的便捷工作流程,其中包括了 yarn workspaces 能做的依赖管理,但相比之下,用 yarn workspaces 管理依赖更好,毕竟 yarn 是专业的包管理器(更详细的原因先占个坑,有空再补)。所以有关依赖包的管理还是交给 yarn workspaces 来做,而我们将使用 lerna 来进行发包管理,当然 lerna 还有很多其他很有用的功能,但在本文不做讨论。

还是前面的项目,现在我们需要使用 lerna 进行管理,首先我们需要初始化 lerna 相关配置,只需要在项目根目录下运行:

$ npx lerna init

lerna 的初始化包括:

  • 将当前项目初始化为 git 仓库;
  • 创建 packages 目录,用于存放所有的包;
  • 创建 lerna.json 配置文件;
  • 创建 package.json 文件。

我们是在已有的项目中进行初始化,所以仅会创建我们项目没有的部分。接下来我们需要重新运行 yarn install 下载 lerna 依赖包,然后就可以愉快的玩耍了。

首先我们先修改一下 lerna.json

# lerna.json
{
  "packages": [
    "packages/*"
  ],
  "version": "independent",  # 各个包的版本号相互独立
  "npmClient": "yarn",       # 使用 yarn 作为包管理器
}

接着我们需要做的是将我们的 test-monorepo 项目托管到 github 上,将本地文件提交到远程仓库后,在根目录运行:

$ lerna publish

这个命令会对比上次发包的 commit,并识别出有变更的包,然后在命令行工具里询问每个更新包的版本,这些变更后的版本号也会同步到 packages 下所有包的 package.json 依赖中,确保依赖了正确版本的包。之后这些版本变更会被提交到远程仓库,同时变更的包会被打上 tag 并发布到 npm 服务器上。

第三版小结:通过 lerna 我们很轻松的完成了 monorepo 的包之间版本依赖自动更新,近乎一键发包的功能。建议大家都动手尝试一下,会更容易理解。

总结

本文简单讨论了 monorepo 模式在前端的应用,提供了一个简易的渐进式的实践教学(yarn ——> yarn workspaces ——> yarn workspaces + lerna),希望能对大家有所帮助。

BTW,笔者更推荐在开源项目中使用 monorepo 模式,因为通常开源项目都会有自己的生态,通过 monorepo 将生态整合到一个仓库中管理,能更好地发挥 monorepo 的优势。

References

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant