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

2,691 阅读6分钟

先看下项目的整体结构:

image.png

使用 yarn 初始化项目

  1. 先执行 yarn set version stable 使用稳定版本, 当前是 v3.4.1。
  2. 执行 yarn init 初始化项目
  3. 执行 mkdir packages 新建 packages 文件夹
  4. 修改 package.json, 声明 workspace
      "workspaces": [
        "packages/*"
      ]
    

yarn v2+ 默认开启 PnP 模式, 因此无需修改 .yarnrc.yml 文件

初始化子项目

1. 执行以下命令

yarn create react-app packages/app1 --template typescript
yarn create react-app packages/app2 --template typescript

按照 monorepo 的惯例,子项目的名称最好命名为 @<主项目名称>/<子项目名称>。分别进入 app1 和 app2 的 package.json 中修改 name :

@yarn3-demo/app1
@yarn3-demo/app2

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

create-react-app 在初始化项目的过程中就会安装依赖到 .yarn/cache 中。

接着,手动创建 common 子项目备用

  1. 进入 packages 文件夹,执行 mkdir common
  2. 进入 common 文件夹,按照下面目录新建文件
   └── common
       ├── README.md
       ├── components
       │   └── test.ts
       ├── index.d.ts
       ├── index.ts
       └── package.json
  1. 修改 common 子项目名称为 @yarn3-demo/common

在 test.ts 中编写一个测试函数

export function add(x: number, y: number) {
  return x + y;
}

在 index.ts 中导出

export { add } from './components/test';

修改 package.json 中的 main 字段:

"main": "index.ts"

在 index.d.ts 中全局声明一下 common

declare module '@yarn3-demo/common' {}

2. tsconfig.json 共享和 ts 支持

tsconfig.json 共享

把子项目中的 tsconfig.json copy 一份到顶层并删除其中的 include 属性

{
  "compilerOptions": {
    "target": "es5",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noFallthroughCasesInSwitch": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx"
  }
}

修改子项目的 tsconfig.json

{
  "extends": "../../tsconfig.json",
  "include": ["src"]
}

ts 支持

这个时候 ts 还找不到依赖包的定义。这是因为 .yarn/cache 下都是 .zip 文件,ts 解析器无法直接读取 .d.ts 文件。

typescript 官方目前也不支持读取 .zip 下的 .d.ts,尽管 yarn 团队已经为 PnP 提了 PR。 yarnpkg.com/getting-sta…

image.png

下面按照官方给出的步骤一步步操作:yarnpkg.com/getting-sta…

  1. 在 vscode 中安装 ZipFS
  2. 在根目录下执行
    yarn dlx @yarnpkg/sdks vscode
    
  3. 这个时候 vscode 会弹窗提示选择一个 typescirpt 版本,我们选择 Use Workspace Version 即可。

这个时候就会发现 ts 已经可以找到依赖的定义了😀。

image.png

共享配置文件

配置文件包括 .prettierrc .eslintrc.js .editorconfig 等, 这里我们只着重说项目构建相关配置

在根目录执行

yarn workspace @yarn3-demo/app2 run eject

这个是 create-react-app 给我们提供的一个用来自定义构建的命令。执行后 CRA 项目中的 config 文件夹和 scripts 文件夹会弹出来, 这两个文件夹下的文件的依赖会放在项目的 package.json 中。我们需要做的就是:

  1. config 文件夹和 scripts 文件夹移动到根目录下
  2. 在 config/paths.js 中新增 packages 路径
    packages: path.resolve(__dirname, '../packages/')
    
  3. 在 config/webpack.config.js 中修改 babel-loader 配置
    include:paths.appSrc  -> include: [paths.appSrc, paths.packages]
    
  4. 修改 package.json 中的 scripts:
    "start": "yarn node ../../scripts/start.js",
    "build": "yarn node ../../scripts/build.js",
    
    注意这里要使用 yarn node

    yarnpkg.com/getting-sta…

依赖管理

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

拿 lodash 举例, 执行命令

yarn workspace @yarn3-demo/app2 add lodash

删除依赖

yarn workspace @yarn3-demo/app2 remove lodash

另外,我们一般安装第三方依赖的时候会把其对应的 @types/xxx 也安装,比如

yarn add lodash @types/lodash

比较麻烦也有可能会忘记安装 types 文件,yarn 提供了一个插件可以自动为我们安装 types 文件,执行

yarn plugin import typescript

然后重新安装 lodash

yarn add lodash

这样 @types/lodash 就会自动被安装在 devDependencies

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

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

yarn workspace @yarn3-demo/app2 add @yarn3-demo/common

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

"@yarn3-demo/common": "workspace:^"

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

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

几个需要注意的点:

  1. 子项目不能使用父级依赖。
  2. 无论是父级还是子项目都只能使用自己的 package.json 中显示声明的依赖,这是因为 PnP 默认为严格模式,不建议更改。
  3. 如果是手动修改了 package.json 一定要记得执行 yarn isntall 重新生成 .pnp.cjs,目的是重新构建依赖树让 node 可以定位到我们的依赖。

启动子项目

以上所有准备已经完成,启动子项目试试吧~

执行命令

 yarn workspace @yarn3-demo/app2 start

success😀

版本控制

现在我们要发布 app2 了,如果只是修复了一个小 bug , 那我们可以

执行命令, 安装 version 插件

yarn plugin import version

修改版本

yarn workspace @yarn3-demo/app2 version patch

查看 package.json 会发现 app2 的版本从 0.1.0 变成了 0.1.1

更多可以查看 yarn version 命令

打包

执行命令

yarn workspace @yarn3-demo/app2 run build   

总结

PnP

yarn v2 版本起默认开启了 PnP 的功能,这个功能开启后项目将不再存在 node_modules 文件夹,所有的依赖都会被压缩成一个 .zip 文件存放在 .yarn/cache 中。

零安装

由于压缩后的包体积很小,而且包的数量不会很多,我们可以直接把 .yarn/cache 上传到 git 仓库, 这样一来的好处:

  1. 更好的开发体验。你每次使用 git clone git pull 等命令更新完你的代码后无需使用 yarn install 进行依赖的安装,这样可以避免一些问题的出现,例如别人更新了某个依赖的版本后,如果你没有进行对应的更新的话,你的代码可能会报错。
  2. 代码 review 的时候可以更清楚哪些依赖发生了改变。
  3. 更快更简单更稳定的 CI 部署。由于每次部署代码的时候,yarn install 占用的时间都是一个大头,去掉这个步骤后部署速度将会大大提升。
  4. 不会存在本地运行没问题,发布线上环境的时候挂掉了的问题
  5. 不用你在 CI 文件里面进行一些安装依赖的配置。

零安装好处毋庸置疑,但显得比较激进,.yarn/cache 对于开发者更像一个黑盒:

  1. monorepo 下无论是子项目依赖还是父级依赖都安装在 .yarn/cache,没有一个清晰的结构

  2. 因为 .zip 格式,ts 支持不友好,需要额外工作量(上面已经提到)

  3. 同样因为 .zip 格式,想要调试某个依赖的话需要额外工作量(需要 yarn unplug

  4. 实际测试中 vscode 的 ts 服务有时候会挂掉(可能是因为根目录没有安装 typescript 或者版本过低)

image.png

优劣对比之下,通过 yarn3 PnP + workspace + typescript 来搭建 monorepo 还是非常值得一试的~

后续还会尝试一下 pnpm + workspace + typescript 来搭建 monorepo,敬请期待~

本文产出 git 仓库地址:github.com/ohguaiguai/… 如想尝试可直接 git clone, 无需 yarn install 体验下零安装👏👏👏

git clone 之后,启动项目会报错:

Error: Required unplugged package missing from disk. This may happen when switching branches without running installs (unplugged packages must be fully materialized on disk to work).

Missing package: open@npm:8.4.0

Expected package location: /Users/zhangxing/Desktop/yarn3-demo/.yarn/unplugged/open-npm-8.4.0-df63cfe537/node_modules/open/

这是因为部分包是默认 unplugged 的, 比如上面的 open@npm:8.4.0, 可能还需要 yarn install 一次。感谢大佬@洛冰河指正~

按照官网说的配置 enableScripts: false 之后,还是无法在 git clone 之后直接运行。看起来如果使用了默认为 unplugged 的包必须要重新 yarn install 一次。

参考

[1] zhuanlan.zhihu.com/p/107343333

[2] yarnpkg.com/

[3] juejin.cn/post/691378…