为什么需要 Monorepo

在开发中,功能模块间的耦合会导致项目难以维护,因此需要分包来进行解耦合。

但是单纯地进行分包,就需要单独发布,单独安装,会增加一些繁琐的步骤。并且,快速迭代开发时,一个包需要依赖其他的包的最新修改,此时,因为需要操作相关包发布更新,开发效率会有一定降低。

而 Monorepo 将所有包都放置于同一个项目内,在分包的前提下通过直接链接本地依赖,解决了等待更新的问题。

如何创建 Monorepo

以下操作使用 pnpm 作为包管理器,如果还未安装,可以参见 通过 corepack 使用 pnpm 与 yarn 作为包管理器 进行 pnpm 的启用。

首先在项目根目录下创建pnpm-workspace.yaml文件,添加需要作为 workspace 的目录项,例如 pnpm 的官方示例1

packages:
  # all packages in subdirs of packages/ and components/
  - 'packages/**'
  - 'components/**'
  # exclude packages that are inside test directories
  - '!**/test/**'

同时新建 workspace 的文件夹,

/packages
    /someWorkspace
    /web
    /...

之后就可以进行依赖的添加。如果需要在项目全局进行安装,需要添加-w参数,而如果在 workspace 内进行安装,需要添加--filter <workspace>参数,同时可以使用-r参数为所有包添加依赖,例如:

// install to monorepo
pnpm add react -w

// install to workspace
pnpm add react --filter @project/someWorkspace

// install recursively
pnpm add react -r

如果需要依赖其他 workspace,可以执行

pnpm add @project/someWorkspace --filter @project/web

pnpm 会自动处理为 workspace 依赖,例如:

{
    "dependencies": {
        "@project/someWorkspace": "workspace:^1.0.0"
    }
}

并且在发布时会替换为正确的依赖项。

同理,package.json 中的 scripts 也是类似的配置:

// monorepo
{
    "scripts": {
        "dev:web": "pnpm dev --filter @project/web"
    }
}

// workspace (@project/web)
{
    "scripts": {
        "dev": "start dev server"
    }
}

这样就建立了基本的 Monorepo 项目。

如果使用的是 yarn,可以参见官方文档

选择 pnpm 主要是因为 pnpm 的实现方式更直接,且其在个人开发环境下对硬盘更友好,安装速度会有一定的提升。如果是组织协作等场景,还需要视具体情况选择包管理器。

遇到的问题

使用 TypeScript

在 Monorepo 中使用 TypeScript 时,会在 import 时提示

Cannot find module '@project/someWorkspace' or its corresponding type declarations.ts(2307)

因此需要进行tsconfig.json的配置,添加相应 workspace 的 paths,例如:

{
    "compilerOptions": {
        ...
        "paths": {
            "@project/web": ["./packages/web/src"],
            "@project/someWorkspace": ["./packages/someWorkspace/src"]
        }
    }
}

使用 ESLint

在 Monorepo 中使用 ESLint 与 eslint-plugin-import / eslint-import-resolver-typescript 时,会在 import 时提示

Unable to resolve path to module '@project/someWorkspace'. eslint(import/no-unresolved)

因此需要进行.eslintrc.json的配置,禁用相应的规则

{
    "rules": {
        "import/no-unresolved": 0
    }
}

这个问题也有一些讨论2345,但尝试后发现,最直接的解决方法就是禁用import/no-unresolved规则。因为 TypeScript 也会检查import语句,所以目前看还未造成太大问题。

工程管理粒度

Monorepo 方式会将所有包置于一个项目中,对其中一个包的修改也需要满足整体项目的管理要求。

一方面,这强化了工程管理,对各个包的管理更为紧密,使得整个项目可维护性更高;另一方面,这也增加了管理成本,丧失了一些灵活性。

总结

使用 Monorepo 方式建立项目,能够更好地进行分包管理,降低项目中多个模块之间的耦合度,同时避免单独分包的繁琐操作,提高迭代效率。但同时也会增加管理成本,降低一些灵活性。

因此,需要根据实际情况选择是否使用 Monorepo。而在合适的情况下,Monorepo 是很值得尝试的,尽管目前可能遇到一些小问题,但相信随着技术完善,更多的 corner cases 会被覆盖。