使用 pnpm + workspace + typescript 从零开始搭建一个 monorepo

2,563 阅读5分钟

强烈建议先阅读 《使用 yarn3 PnP + workspace + typescript 从零开始搭建一个 monorepo》, 因为本文与其部分内容有重叠, 重叠部分就忽略了。

还是先看下项目的整体结构:

image.png

使用 pnpm 初始化项目

本文使用 pnpm 版本为: v7.5.2

  1. 执行 pnpm init 初始化项目
  2. 执行 mkdir packages 新建 packages 文件夹
  3. 新建 .pnpm-workspace.yaml, 声明 workspace
      packages:
      # all packages in direct subdirs of packages/
      - 'packages/*'
    

初始化子项目

1. 执行以下命令

pnpm dlx create-react-app packages/app1 --template typescript
pnpm dlx create-react-app packages/app2 --template typescript

关于 pnpm dlx 可以查看 pnpm.io/cli/dlx

yarn 也提供了一个 yarn dlx, 二者都是提供一个临时的环境来运行包提供的脚本而不需要在 package.json 中添加依赖

修改子项目名称:

@pnpm-monorepo-demo/app1
@pnpm-monorepo-demo/app2

然后删除这两个子项目中的 node_modules 文件夹。

common 子项目的创建可参考上篇文章。

tsconfig.json 共享

参考上篇文章。

与 yarn 不同的是不需要做额外的工作去支持 ts。

共享配置文件

这里只说和上篇不同的地方。在根 package.json 中新增命令

 "eject": "pnpm run -C packages/app1 eject && pnpm run -C packages/app2 start"

-C 是指在指定的路径下执行命令而不是在当前目录

依赖管理

1. 给子项目安装第三方依赖

pnpm add @types/lodash --filter @pnpm-monorepo-demo/app1

删除依赖:

pnpm remove @types/lodash --filter @pnpm-monorepo-demo/app1

2. 子项目安装其他作为本地依赖的子项目

现在我们要在子项目 app1 中引用 common,在根目录下执行

pnpm add @pnpm-monorepo-demo/common --filter @pnpm-monorepo-demo/app1

查看 app2 的 package.json 文件会发现 dependencies 中多了

"@pnpm-monorepo-demo/common": "workspace:^1.0.0",

测试一下,在 app1/src/index.tsx 中

import { add } from '@pnpm-monorepo-demo/common';
console.log(add(1, 2));

几个需要注意的点:

  1. 当子项目有本地依赖时(即使这个依赖已经被安装了),比如上面 app1 依赖了 common, 这时候在 app1 目录下执行 pnpm add xxx 安装其他依赖会报错, 比如:
$ pnpm add antd
 ERR_PNPM_NO_MATCHING_VERSION_INSIDE_WORKSPACE  In : No matching version found for @pnpm-monorepo-demo/common@^1.0.0 inside the workspace

看了下 pnpm add 命令,官网是这么说的:

Installs a package and any packages that it depends on.

这就难怪了,安装 antd 的同时会顺便安装其他包,而在 app1 目录下没有 @pnpm-monorepo-demo/common@^1.0.0, 因为 common 是和 app1 同级的, 所以报错了。

这样的话就只能通过在根目录安装

pnpm add antd --filter @pnpm-monorepo-demo/app1`
  1. 关于 pnpm install 官网说法:
Inside a workspace, pnpm install installs all dependencies in all the projects. If you want to disable this behavior, set the recursive-install setting to false.

但实际上执行后并不能安装子项目的本地依赖, 这个和官网说的有点区别,不知道是不是自己理解或者操作有误。把根和子项目的 node_modules pnpm-lock.yaml 都删掉重装也不行,有知道的同学可以告知下~

启动子项目

启动单个子项目

注意在根目录执行以下命令是行不通的

pnpm run start --filter @pnpm-monorepo-demo/app1

因为 pnpm start 或者 pnpm run start 只会在根 package.json 中查找 start命令

Runs an arbitrary command specified in the package's start property of its scripts object. If no start property is specified on the scripts object, it will attempt to run node server.js as a default, failing if neither are present.

这时候 -C 又派上用场了, 在根 package.json 中新增命令:

"start:app1": "pnpm run -C packages/app1 start",

然后执行

pnpm run start:app1

同时启动所有子项目

pnpm run -r start

-r 就是递归去执行所有包下的 start 命令

注意不能写成 pnpm run start -r

这一点可以参考 exec 命令:

Any options for the exec command should be listed before the exec keyword. Options listed after the exec keyword are passed to the executed command.

Good. pnpm will run recursively:

pnpm -r exec jest

Bad, pnpm will not run recursively but jest will be executed with the -r option:

pnpm exec jest -r

版本控制

pnpm 推荐 Rushchangesets, 这里就不多说了~

总结

相比于 yarn(v2+), pnpm 保留了 node_modules , 只是用了另一种更加合理的组织形式来解决 node_modules 的问题:

  1. 更易被我们理解和接受
  2. ts 支持更友好
  3. 调试支持更友好

另外几点对比

以下仅代表个人观点,如有错误请指正~

关于安装速度

通过 benchmarks可以看到很多情况下 pnpm 的安装速度要更胜一筹,这是因为 pnpm 和其他包管理器的安装方式不一样:

pnpm:每一个依赖都是单独地经历 resolve -> fetch -> write, 并且所有依赖并行。

其他:所有依赖共同经历 resolve -> fetch -> write

对于 pnpm, 最终的安装时间只取决于路径最长的那个依赖;对于其他包管理器来说每个阶段都要等待耗时最长的那个依赖的完成。举个例子,现在有 A 和 B 两个依赖, A 对应三个阶段的耗时分别是 1 2 3, B 对应三个阶段的耗时分别是 3 2 1 , 那么 pnpm 最终安装完成时间是:1 + 2 + 3 = 6 < 其他包管理器完成时间: 3 + 2 + 3 = 8。

关于节省空间

因为 pnpm 默认所有依赖都放在磁盘的同一个地方,这些依赖可以被跨工程共享,所以说比较节省空间。这个其实 yarn 也支持, 使用 enableGlobalCache:true 就可以使用全局缓存,但是这样的话零安装就不能使用了, 一般不建议这么使用。

关于运行时依赖查找速度

在运行时,pnpm 应该比不过 yarn, 因为 yarn 是通过 .pnp.cjs 直接告知 Node 依赖的位置(内部是重写了 require.resolve 方法); 而 pnpm 中还是依靠 Node 层层查找,理论上会更慢。

PnP 模式诞生的初衷

Yarn already knows everything there is to know about your dependency tree - it even installs it on the disk for you. So, why is it up to Node to find where your packages are? Instead, it should be the package manager's job to inform the interpreter about the location of the packages on the disk and manage any dependencies between packages and even versions of packages. This is why Plug'n'Play was created.

yarn 重写 require.resolve 方法:

Given that PnP is a resolver standard different from Node, tools that reimplement the require.resolve API need to add some logic to account for the PnP resolution.

参考

[1] pnpm.io/

[2] yarnpkg.com/