利用pnpm的workspace搭建一个简单的monorepo项目

大家好,我是刘布斯。

之前,我们通过《还在npm或者yarn?试试pnpm》和 Monorepo - 理论篇这两篇文章,介绍过 pnpm 和 Monorepo,今天继续分享一篇实践的文章,看看怎么使用 pnpm 搭建一个简单的 monorepo 项目。

什么是monorepo?

Monorepo 其实不是一个新的概念,在软件工程领域,它已经有着十多年的历史了。概念上很好理解,就是把多个项目放在一个仓库里面,相对立的是传统的 MultiRepo 模式,即每个项目对应一个单独的仓库来分散管理。

而且越来越多的项目已经采用了Monorepo的方式来进行开发,可以看一下pnpm官网上贴出来的使用pnpm的workspace功能搭建的monorepo开源项目。

一般monorepo项目的目录都如下图所示,在packages中存放每个子项目,并且每个子项目都会有自己的package.json和node_modules

├── packages
|   ├── pkg1
|   |   ├── package.json
|   ├── pkg2
|   |   ├── package.json
├── package.json

为什么使用monorepo?

如果使用原来的MultiRepo模式,每个包独立一个仓库,那就可能会有十多个仓库,这些仓库都需要单独去搭建环境,配置lint,发包等工程化配置,重复十多次。

但是最大的问题还是以下几点:

1.代码复用

在维护多个项目的时候,有一些逻辑很有可能会被多次用到,比如一些基础的组件、工具函数,或者一些配置,你可能会想: 要不把代码直接 copy 过来,多省事儿!但有个问题是,如果这些代码出现 bug、或者需要做一些调整的时候,就得修改多份,维护成本越来越高。

那如何来解决这个问题呢?比较好的方式是将公共的逻辑代码抽取出来,作为一个 npm 包进行发布,一旦需要改动,只需要改动一份代码,然后 publish 就行了。

但这真的就完美解决了么?举个例子,比如你引入了 1.1.0 版本的 A 包,某个工具函数出现问题了,你需要做这些事情:

    1. 去修改一个工具函数的代码
    2. 发布1.1.1版本的新包
    3. 项目中安装新版本的 A。

可能只是改了一行代码,需要走这么多流程。然而开发阶段是很难保证不出 bug 的,如果有个按钮需要改个样式,又需要把上面的流程重新走一遍......停下来想想,这些重复的步骤真的是必须的吗?我们只是想复用一下代码,为什么每次修改代码都这么复杂?

即使使用npm link不用将包发布到npm上直接再本地使用,但是也要再需要依赖该包的项目中执行npm link package命令,如果有十几个包都需要依赖该包,还需要频繁的切换目录再执行命令,也非常麻烦。

2.频繁执行命令

需要在每个包里执行命令,现在也是要分别进入到不同的目录下来执行十多次。最关键的是有一些包需要根据依赖关系来确定执行命令的先后顺序。

3. 版本管理

版本更新的时候,要手动更新所有包的版本,如果这个包更新了,那么依赖它的包也要发个新版本才行。

接下来我们看看使用monorepo是如何解决这些问题的。

如何使用pnpm的workspace搭建monorepo项目

1. 初始化package.json

pnpm init
├── package.json

2. 根目录下创建pnpm-workspace.yaml文件

pnpm-workspace.yaml文件是在使用pnpm包管理器时用于定义工作空间(Workspace)配置的文件,该文件的作用体现在以下几个方面:

  1. 包含/排除目录:pnpm-workspace.yaml文件可以指定哪些目录或项目属于当前的工作空间。通过配置,开发者可以明确地将某些目录或项目包含在工作空间中,同时也可以排除一些不需要管理的目录。
  2. 自定子项目的目录位置:使用通配符等配置方式,可以灵活地定义工作空间的目录结构,满足不同的项目管理需求。
├── package.json
├── pnpm-workspace.yaml
    packages: // 定义子项目的目录
      # all packages in direct subdirs of packages/
      - 'packages/*'
      # all packages in subdirs of components/
      - 'components/**'
      # exclude packages that are inside test directories
      - '!**/test/**'

3. 创建packages文件夹并在该文件夹内创建每个子项目

主要是使用vite创建了vue和react项目以及一个自定义的shared包,每个子项目中也都会有package.json

├── packages
|   ├── my-react-app
|   ├── my-vue-app
|   ├── shared
├── package.json
├── pnpm-workspace.yaml

4. 安装依赖

1.直接在根目录下执行pnpm i,可以同时安装根目录和所有子项目的依赖

pnpm i

2.向根目录中添加依赖

而且根目录中添加的依赖可以直接在子项目中使用,比如我在根目录中添加lodash依赖,在子项目my-vue-app中就可以直接使用,这主要得益于在查找依赖时是通过查找每一层的node_modules目录,直到查找到顶层的node_moudles

pnpm add <package-name> --workspace-root(-w)
image.png

3.向子项目中添加依赖

pnpm add <package-name> --filter(-F) <project-name>

-w: 该命令主要作用是运行命令时就像是在workspace的根目录中运行的,而不是在当前工作目录中,主要可以使用在添加依赖时指定-w就是把依赖添加到根目录中,或者在其它目录中例如子项目的目录中可以通过添加-w执行一些根目录的script命令。

-F:该命令根据名称就可以知道大概是什么意思,filter-过滤,其实就是根据你的project-name精确选择该包,你可以利用该命令向特定的包中添加依赖或者执行该包中的script命令

pnpm add moment --filter(-F) my-react-app 向my-react-app中添加moment依赖

pnpm dev --filter my-react-app 执行my-react-app中的dev命令

-r(--recursive): 该命令允许用户在 Monorepo(多包仓库)结构的项目中,对工作区(workspaces)内的每个包执行指定的命令。--recursive 参数的含义是递归地执行某个命令,即在 Monorepo 工作区的每个子包(package)中运行该命令。这非常有用,因为它允许开发者一次性地对所有子包执行相同的操作,如安装依赖、更新依赖、运行测试等,而无需逐个进入每个子包目录执行命令。

pnpm test -r // 在每个子包中执行测试命令
pnpm add <package> -r // 在每个子包中添加依赖,除了添加npm仓库中的包,也可以添加本地workspace工作目录中的子包,但是需要设置`link-workspace-packages`为`true`,下边也会讲到
pnpm i -r // 在工作区中安装每个子包中的依赖

5. 子项目相互引用

有两种方式可以实现

1.利用命令: pnpm add package-A -F package-B

首先需要每个子项目中的package.json中都指定了name属性,然后再进行额外的配置,需要在根目录中创建.npmrc文件,并将link-workspace-packages设置为true,只有开启了这个选项才会先去查找本地工作空间,如果找到了就会通过符号链接将package-A包链接到package-Bnode\_modules目录中,同时B包的package.json中的dependencies选项也会将A包添加进来形式如下"package-A": "workspace:\*", 如果找不到会尝试去npm仓库进行下载.如果不设置该配置,则每次添加都会去npm仓库进行下载,而不会在本地工作空间进行查找。

link-workspace-packages=true
pnpm add <package-A-F <package-B>

2.利用workspace: *协议,

可以直接手动在package-B包中的package.json中的dependencies选项中手动添加package-A的依赖,形式也是"package-A":"workspace:*",之后再执行pnpm i命令也会将A包自动符号链接到B包中。

不管这两种方式的哪一种,在发包的时候都会将workspace:*替换为该包的具体版本号。

changeset的使用

changeset主要是解决版本更新时,要手动更新所有包的版本,比如A、B、C包都依赖了D包,那么当D包更新发包之后,需要手动更新A、B、C包中依赖D包的版本再重新发包。而changeset就可以通过命令来完成这些操作。

  1. 安装changeset
pnpm add @changesets/cli -w

2. 执行init

npx changeset init

执行完该命令后会创建一个.changeset文件夹

image.png

什么叫 changeset 呢?

就是一次改动的集合,可能一次改动会涉及到多个 package,多个包的版本更新,这合起来叫做一个 changeset。

  1. 然后我们执行add命令添加一个changeset
npx changeset add

执行完会让你进行选择哪些项目会有更新:

image.png

之后会让你选择哪个是 major 版本更新,哪个是 minor 版本更新,剩下的就是 pacth 版本更新。

然后就是填写本次版本更新的总结。

image.png

执行完命令后会在.changeset文件夹下生成一个文件记录着这次变更的信息:

image.png
  1. 然后可以执行 version 命令来生成最终的 CHANGELOG.md 还有更新版本信息:
npx changeset version

该命令执行完后,你会发现.changeset中刚才生成的文件已经被消费掉了,而在share子项目中生成了一个CHANGELOG.md文件记录了我们这次更改的信息,可以自己手动进行编辑信息,同时也会修改包中package.json中的包的版本。

image.png
image.png

但是发现my-vue-app包中也生成了一个CHANGESETLOG.md文件,看下里边的记录原来是因为我们的my-vue-app包依赖了dyh-share包,所以也会更新一个patch版本。

这就是这就是 changeset 的作用。

如果没有这个工具呢?

你要自己一个个去更新版本号,而且你还得分析依赖关系,知道这个包被哪些包用到了,再去更改那些依赖这个包的包的版本。

就很麻烦。

  1. 接下来就可以执行publish命令进行发包了
npx changeset publish

总结

使用pnpm workspace + changeset就可以解决文章开头所说的三个问题。

  1. pnpm workspace可以解决代码复用的问题。
  2. pnpm -r可以解决频繁更换目录执行命令的问题。
  3. changeset可以解决版本更新、发布的问题。

原文链接:https://juejin.cn/post/7425420976487415846

最后

还没有使用过我们刷题网站(https://fe.ecool.fun/)或者刷题小程序的同学,如果近期准备或者正在找工作,千万不要错过,题库主打题全和更新快哦~。

有会员购买、辅导咨询的小伙伴,可以通过下面的二维码,联系我们的小助手。