Mono Repo第二弹

Monorepo基本概念回顾

Monorepo:一种在单个代码仓库管理下维护多个子项目的代码管理策略。优势在于避免了多个单包单仓库之间的跨工程联调,也可以避免所有代码都在一个工程下的巨大资源消耗。算一个可以灵活控制粒度的中间解决方案。

Workspace:工作区,是monorepo的策略实现,一般是一个顶层项目下包含多个子项目的模式,提供多包的依赖管理、包的管理(新增、发布)等功能。现在市面上有许多workspace的实现,比如 npm / pnpm / yarn,以及接下来会展开的lerna。

依赖管理:

  • npm:两个阶段,第一个阶段是每个项目将依赖层层安装到自己的node_modules中,会形成一棵依赖树,当依赖层级过深时,依赖树会变得很庞大且路径也会超出部分操作系统支持的最大路径长度;所以第二阶段开始将依赖打平,同一个依赖会统一安装到顶层的node_modules中,如果某个依赖存在多个版本,则会提升一个版本到顶层,其它版本的维持在未被提升依赖的子项目自己的node_modules中。
  • yarn:它先推出的npm第二阶段的策略,npm后面v3以后也出的类似的版本,就不赘述了。
  • pnpm:npm和yarn这种安装依赖到顶层的方式,由于node查找依赖的特殊路径,会存在子包能够访问到不在自己依赖声明范围内的代码,也就是幽灵依赖。pnpm的依赖安装完之后,会在顶层node_modules中生成一个.pnpm的虚拟store,这个目录下的链接指向实际安装的依赖的代码,然后每个子项目的node_modules又维护着自己的依赖树,里面都是指向.pnpm的软连接,通过这种方式规避了幽灵依赖。

幽灵依赖

某些依赖没有被显式地声明在package.json中,但是又可以被访问和引用,这种依赖就叫做幽灵依赖。参考链接👉rush官网&掘金译文

上次的提问:所以mono repo就是建立在幽灵依赖上的吗?

是的。顶层要想维护公共依赖,是需要借助幽灵依赖的,所以顶层的公共依赖的添加应该慎重。

PNPM怎么解决幽灵依赖的

上次实验中用到的那个包,因为没有任何一个子目录声明了那个依赖,也没有安装那个包的types文件,所以不管哪种依赖是否提升编译都会报错,因为缺少类型文件。pnpm可以做依赖提升但默认是全部依赖都不提升的。可以通过更改.npmrc里的配置来实现依赖提升,还可以限制提升的依赖的范围。

实验准备:
首先补齐项目中的依赖,为其中一个子项目添加antd-mobile的依赖。然后准备一个子项目react-antd-pkg,这个包在除了react、ts这些开发依赖之外就只依赖antd,现在在react-antd-pkg的代码中引入antd-mobile的Input组件并渲染。

npm利用幽灵依赖

npm将依赖全部安装到顶层的node_modules,因为在查找依赖的时候默认会一层一层向上查找所有的node_modules目录,所以在目标项目中可以正常引用未声明的依赖antd-mobile并且编译通过。
在这里插入图片描述

yarn利用幽灵依赖

yarn的表现与npm没什么不同,原因也是一样的,就不赘述了。
在这里插入图片描述

pnpm杜绝幽灵依赖

pnpm安装完依赖以后,.pnpm里面放了所有子项目的依赖实体,然后各个子项目自己声明的依赖会在自己的node_modules目录下生成一系列指向.pnpm中实体文件的硬连接。所以pnpm很快发现当前子项目中访问了未声明的依赖,并给出了红色下波浪线进行提示。
在这里插入图片描述
然后编译当前子项目,报错找不到引用的包或者其类型声明。
在这里插入图片描述

pnpm也是能做到像npm那样的依赖提升的效果的,需要配置一下.npmrc文件中的shamefully-hoist属性为true。npmrc的全称是npm running configuration,也就是npm运行时配置文件。这个文件会在npm、yarn、pnpm等包管理工具运行的时候去读取,可以配置包的下载来源。一般来说,.npmrc文件有三个级别,权限由高到低分别是:项目内.npmrc、用户配置的.npmrc和全部配置的.npmrc。然后不同的包管理工具可能还有自己专属的配置文件,比如pnpm有pnpm-workspace.yaml,npm有npm的内置配置文件,然后这些配置文件会和系统本身的那三个级别的.npmrc一起工作,具体细节大家可以下来看看npm官方文档

其它链接,请自助食用:pnpm如何于npmrc合作yarn 1.x的配置yarn 3.x的配置

加上shamefully-hoist之后,所有的依赖都提升到顶层的node_modules中了。
在这里插入图片描述然后此时来编译目标的子项目,可以编译成功了:
在这里插入图片描述
但是反过来,将shamefully-hoist置为true(默认值为false),并不会改变npm和yarn的默认安装依赖到顶层的行为。

Mono Repo多包管理

workspaces:mono repo本质上是一个在单repo下管理多个pkg的项目组织方式,workspace是支持这种模式的一种实现,通常会包含创建工作区、管理工作区内所有包的管理(比如新增、依赖管理、命令执行、版本管理和包的发布以及包与包之间的互相连接),这些功能的支持程度要看具体的workspace的实现。

node_modules/.bin:当前工作区的依赖中下载下来的可执行的二进制文件,这些文件基本可以在命令行直接执行,也可以写到script脚本中。

Yarn + Lerna

yarn是monorepo策略的一种实现,它可以做到在顶层项目中安装整个工作区的依赖,可以通过yarn run去执行一些指令,还可以在run之前加上workspaces / workspace来限定当前脚本执行的范围是整个工作区的所有子项目还是仅某一个子项目,也可以做包与包之间的link,类似于npm link,以及发布某一个子项目等。具体能执行的操作参见官方文档:https://classic.yarnpkg.com/en/docs/cli/。

lerna是一个管理多包的工具,也算是一种workspace的解决方案,它具有workspace常见的一些功能,比如安装依赖、为当前workspaces添加新的子项目、面向各个子项目执行各种命令(通过-- scope参数来限定指令作用的范围)、版本管理等。

yarn已经能完成npm包的创建、联调以及包的发布,但是它不能提供较好的版本管理,尤其是在版本之间有依赖的时候,是需要开发者手动去判断哪些包需要联动升级版本以及将当前包的新版本手动更新到下游的包的package.json中去的。但是lerna就很好地提供了这个功能并且填补了这个空缺。

lerna的使用需要有git,在执行lerna init创建一个lerna的工作区的时候会learn会判断当前项目有没有关联的git仓库,如果已有就不会创建,没有的话就会初始化一个git仓库以及创建一个gitignore文件。主要 lerna的文件差异对比以及版本的更新都需要和git有关联。当开发者提交了开发的代码之后,就可以通过lerna diff来查看具体的文件改动,以及可以用lerna changed来查看改动的包及依赖它的包(msg: found ${n} packages ready to publish)。确认了改动的文件后就可以通过lerna version来执行版本的升级和下游依赖的包的package.json的更新。

版本管理

lerna的版本管理有两种模式,一种是统一版本,一种是各个包之间独立维护自己的版本。【lerna.json】

  • 统一版本:顾名思义就是变动的包都用一个版本号。但是lerna如果设置了统一的版本升级,没有修改的文件不会提升版本,所以版本还是会有差异,所以如果需要始终统一,每次lerna version的时候可以加上--force-publish参数来忽略文件变动并生成新的版本号。
  • 独立版本:其实就是每个子项目之间的版本是互不干扰,各升各的,每次一个包更新的时候只更新它以及依赖它的包的版本。

执行lerna version的时候,lerna会问开发者当前的包要选择发布什么版本。当当前的改动会引起多个包的升级时,这样依次确认就会变得很麻烦。lerna支持在执行version命令的时候配置参数 --conventional-commit来根据提交信息自动为需要升级的包生成版本号。Conventional Commit就是我们项目中也在使用的git commit message规范,比如特性功能的提交就是feat,修复缺陷提交就是fix,这两者分别会导致minor和patch级别的版本提升,对它感兴趣的话可以看看👉:Conventional Commit的官网

发包

包的发布(lerna publish [from-git/package]):当我们在工作区里面执行lerna publish命令的时候,会发布每一个工作区中非私有的包(这个主要是因为lerna publish底层还是用的npm publish,不发布私有包到registry是npm的默认行为)。然后也可以配置registry指定包要发布的源地址【.npmrc】。

坑:如果你是先执行了lerna version(此时lerna会生成一条包含版本变更和changelog文件的提交记录,并为这次变更打上Tag),然后再执行lerna publish的话,因为lerna publish会默认在后台执行lerna version,所以当它识别到距离上次的git tag以来并没有新的提交后,就不能生成新的版本,同时也不能走后面的发包流程了。所以此时要换命令lerna publish from-git(从当前提交的git tag中找需要发布的包)或者from-package(比对包的package.json中的版本号和npm源中的版本号,如果当前版本号不存在于npm源中,则发布)来发包。

两次lerna version,生成的两个tag,多次提交记录:
两次lerna version,生成的两个tag,多次提交

lerna publish from-git:
lerna publish from-git

lerna publish from-package:
lerna publish from-package

还有一些额外的功能,比如lerna create / bootstrap / add / link就不讲了,后面三个在最新的版本中基本都废弃了,lerna现在把依赖管理直接托管出去了,支持npm(默认) / yarn / pnpm,自己只关注一些工作流的优化(还可以和nx一起结合,进一步优化构建的速度、提升并行效率,因为默认是串行操作),具体的可以直接看👀:lerna官方文档

Pnpm + Lerna

pnpm和lerna的结合,跟yarn和lerna的组合是差不多的,只是把包管理工具换成了pnpm而已。

Pnpm + Changeset

pnpm的workspace和yarn没有太大区别,基本的功能也都一致,不足的地方也很类似,没有内置的版本管理的解决方案,然后changeset是pnpm官方推荐的版本控制工具之一。

要使用changeset需要先安装@changesets/cli,它会在顶层目录生成一个.changeset目录,里面包含了changeset的配置文件、一个readme,还有就是生成的changeset。

changeset的生成:
  1. 当用户修改完代码后,在提交之前执行pnpm changeset (add),changeset就会提示当前用户选择要添加到当前changeset中的包,在提交之前执行此命令方便的点在于,changeset可以基于git diff为用户摘出来当前已经修改过的包,不然的话用户要从所有未修改的包中选择自己要准备升级和发布的包。
  2. 选完包之后,changeset会提示开发者选择哪些包要做major级别的版本的提升、哪些要做minor级别的提升,然后前面两步都没有选择的包,会默认执行patch级别的版本提升。
  3. 版本确定完之后,changeset会提示用户输入此次changeset的信息,这部分信息会在后续具体升级版本的过程中放到具体要升级的包的changelog中。确认都信息填充完成后就会在.changeset目录中生成一个新的文件。文件的格式基本如下:
// [pkg-name]: [version bump level]
---
"@demo-project/wakawaka": patch
"@demo-project/react-antd-pkg": patch
---
// summary info for this changeset
version bump 1.0
changeset的消费:

changeset version是强依赖项目中生成的changeset文件的,没有changeset文件,就没有办法升级版本。当拥有changeset文件后,开发者可以执行pnpm changeset version命令来将changeset文件中的version bump level具化为具体的版本号。如果一个包在多个changeset中被包含,且有不同版本级别的version bump,会选择最高版本进行升级。默认情况下,changeset为工作区中的每个包的升级是独立的,不会保持版本同步,如果有这样的需求,可以在config.json中配置fixed或者link属性来达到目的。

  • link:在消费changeset生成包的版本时,会将在changeset中被登记且在link范围内的包都升级成同一个版本,以这一批包中最高的版本为准。
  • fixed:跟link是类似的,可以指定一组始终一起变更版本的包,但是fixed中的包不管有没有修改,有没有在changeset中被包含,都会一起升级版本。

坑:changeset version只会变更存在于changeset中的包的package.json,即:如果当前工作区有包是依赖当前升级的npm包的话,这些包不仅自身版本不会更新,它们的dependence中当前变更的包的版本也不会被更新,除非在生成changeset文件的时候把依赖当前包的那些包都加进去,这样changeset就会同步更新当前包的版本。但是这样的话需要用户自己很清楚,当改动一个包的时候它还会/应该联动更新哪些包,这个在大型的项目里面做起来就很麻烦,所以changeset也提供了一个实验性的属性updateInternalDependents来达到patch更新下游包版本的目的👍。

{
  "$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json",
  "changelog": "@changesets/cli/changelog",
  "commit": false,
  "fixed": [["@demo-project/react-hook-*"]],
  "linked": [],
  "access": "restricted",
  "baseBranch": "main",
  "updateInternalDependencies": "patch",
  "ignore": [],
}

发包

执行pnpm changset publish就可以发包,也可以用pnpm自己的publish,执行pnpm -r publish就行。

WorkSpace协议

第一弹的遗留问题:workspace的内部依赖怎么关联

同一个工作区内的包互相依赖时,应该使用最新的代码,这样可以保证功能的一致性,避免同一个工程不同的子项目引入同一个包的多个版本,造成不必要的混乱。pnpm和yarn 3.x以上是支持workspace协议的,可以通过"@aaa/bbb": "worspace:*"的方式来引用同一个工作区下的包,确保一个工作区内的包与包之间都是用的最新的代码以及防止多人协作时修改版本的遗漏和冲突。感兴趣的同学可以参照👉:pnpm的workspace协议yarn 3.x工作区内的交叉引用

Yarn和Npm提升依赖的规则

没有一种方法可以决定如何去转换树,不同的包管理器会做出不同的判断(有些针对包流行度,大小,最高版本等等进行了优化)。出于这个原因,无法保证最终的提升结果。如果想要确认自己安装的每一个依赖到底是被谁依赖、因为什么缘故安装的,可以check项目中的lock文件。

  • 9
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值