Monorepo 101

前段时间我尝试把几个小项目合并起来,但是效果很不理想。最近看了一篇文章讲 monorepo,终于意识到了自己的问题所在。本文借机讲讲 monorepo 的简单实践,分享一下基于大前端代码管理的一些常规操作。

Monorepo

Mono- 词根的意思是单体,所以 monorepo 指的就是单体仓库管理,通俗来说就是一个 git 仓库包含项目所有应用的源代码。

Monorepo 项目通常由多个 app 组成,比如,把网页端、移动端、小程序等等 app 放在一起;或是把复杂产品线里拆分成微服务,但依旧共享同一个代码仓库。

Mono-repo vs. Multi-repo

除了 monorepo,在大型项目管理中还有一个名词,叫 multirepo。顾名思义,前者是单仓库;后者就是多仓库——把应用按模块分散到不同的代码仓库里。通俗来说,它们是单个 git repo 和多个 git repo 的区别。


multirepo v.s. monorepo

我们看看 monorepo 在与 multirepo 对比下的优劣点:

  • pros of Monorepo

    all in one 所用代码共用一套配置,更易于统一管理
    代码复用 代码复用度高,提取公共模块也简单易行
    透明度 源码均可见,方便在 IDE 里查看
    最小更改 依赖项代码更改后,所有被依赖项立即生效
  • cons of Monorepo

    访问约束 代码集中一处,有越权修改的风险
    Build 时间 Mono 的代码量往往较大,构建时间会很长
    Git 性能 Git 设计初衷是小代码仓库,代码量太大会拖垮 git

Monorepo 工具

主流的 mono 管理工具很多,如:

  • lerna:老牌的 JS 多包管理工具
  • Baze:google 出品的多语言 mono 构建工具
  • RushJs:巨硬出品的 mono 全生命周期工具
  • NX:可扩展的 mono 开发工具

这些工具非常强大,有的集成了 CI 特性,有的是内置了脚手架功能。不过学习新工具有很容易无端增加认知成本。其实想入手 monorepo,只要有 yarn 就行了,其他的高级功可以慢慢学,也可以配合其他传统工具使用。后文就围绕 yarn 简单介绍如何配置 monorepo。

Yarn workspaces

yarn 从 1.0 版本起就有多包管理的功能——yarn workspaces。主要关注如下三个需求:

  • 对于每个项目,Yarn 将使用一个单独的 yarn.lock 文件而不是为每个工程使用一个不同的锁文件,这意味着更少的冲突和更容易的审查
  • 所有的项目依赖关系都将被安装在一起,为 Yarn 提供更多的自由度来更好地优化它们
  • 依赖关系可以链接在一起,这意味着工作区可以相互依赖,并始终使用最新的可用代码

启用 workspace

我们试着开启 yarn workspace,首先在控制台敲下如下命令:

yarn config set workspaces-experimental true

这样你系统目录下的 .yarnrc 文件就写入了 workspaces-experimental true

接着我们初始化项目根目录的 package.json 文件如下:

{
  "private": true,
  "workspaces": {
    "packages": ["packages/*"]
  }
}

这里 "private": true 是必需的,主要是增加安全措施;而 "workspaces.packages" 用于指定子项目所在的文件夹。

初始化项目

简单起见,我们先创建两个子应用——foo 和 bar,结构如下:

.
├── packages
│   ├── foo
│   │   └── package.json
│   └── bar
│       └── package.json
└── package.json

指定 foo 和 bar 的包名,

// packages/foo/package.json
{
  "name": "@onion/foo",
  "version": "1.0.0",
  "devDependencies": {
    "chalk": "^4.1.0"
  }
}

其中 bar 依赖于 foo

// packages/bar/package.json
{
  "name": "@onion/bar",
  "version": "1.0.0",
  "dependencies": {
    "@onion/foo": "1.0.0"
  },
  "devDependencies": {
    "chalk": "^4.1.0"
  }
}

安装

我们在根目录跑一下yarn install;node_modules 下出现了 @onion/bar@onion/foo,以及两个包共同的依赖 chalk

.
├─node_modules
│  ├─@onion
│  │  ├─bar
│  │  └─foo
│  └─chalk@4.1.0
│
└─packages
    ├─bar
    └─foo

复盘一下 yarn install 的效果:

  1. 将 workspaces 里的包提升到 node_modules 下,使项目底下的 js 都可以通过 import '@onion/foo' 的形式调用私有项目包

  2. 将 workspaces 下共同依赖的三方库——chalk——也提升到了 node_modules 下,供两者使用

p.s. 这里插播一个知识点,NPM 模块的加载顺序是:先查看当前目录下的 node_modules 文件夹;如果未找到 import 模块,再寻找上一级目录的 node_modules 文件夹;直到系统 home 下的 node_modules 为止。所以提升依赖不会影响使用。

nohoist

当然,假如子项目各自的三方依赖项版本不同,yarn 也会选择性地只提升其中一个版本,另一个版本会保留在特定子项目下的 node_modules 文件夹下方:

.
├─node_modules
│  ├─@onion
│  │  ├─bar
│  │  └─foo
│  └─chalk@4.0.0
│
└─packages
    ├─bar
    │  └─node_modules
    │      └─chalk@4.1.0
    └─foo

你也可以更狠一点,干脆不让 yarn 提升某些依赖:

//package.json
"workspaces": {
    "packages": ["packages/*"],
    "nohoist": ["**/chalk"]
  }

这样各自项目的依赖会保留在自己所在目录下的 node_modules 里:

.
├─node_modules
│  └─@onion
│     ├─bar
│     └─foo
│
└─packages
    ├─bar
    │  └─node_modules
    │      └─chalk@4.1.0
    └─foo
       └─node_modules
           └─chalk@4.0.0

运行 workspaces commands

运行子项目的 npm script 和常规操作一直,先cd到特定目录,然后yarn run即可;当然你也可以在根目录操作,指定空间名字即可:

yarn workspace @onion/foo run test

Symlink

最后,再说了个 monorepo 在开发环境中与常规工具的集成。上面提到过: @onion/bar 项目依赖 @onion/foo 项目。

// packages/bar/package.json
{
  "name": "@onion/bar",
  "version": "1.0.0",
  "dependencies": {
    "@onion/foo": "1.0.0"
  }
}

我们在 bar 项目内引用 foo 项目,通常不会使用相对路径的形式 ;而是用 Symlink(符号链接)的形式导入(如@onion/foo)。

// packages/bar/index.js
import foo from "../foo/src/index"; // Bad

import foo from "@onion/foo"; // Good

Symlink 指向的依赖一般就是 node_modules 里的文件。正如上文提到过,在本地开发中使用最新的依赖代码,我们每次都需要事先安装一下,有时候还有版本配置等等问题,使用起来略显麻烦。更大的问题是:通常依赖项里的代码会做压缩或是转义,不利于调试。所以想使用最新代码,我们还要再加点配置。

ts-node

比如使用 typescript,会以起别名的形式将 symlink 指向源文件。如下,在 tsconfig 里将 @onion/foo 指向源代码,VSCode 编译器就会支持源码跳转:

// tsconfig.json
{
  "compilerOptions": {
    "baseUrl": "./packages",
    "paths": {
      "@onion/foo": ["foo/src"]
    }
  }
}

调试的时候在 npm script 里加上tsconfig-paths即可:

// package.json
{
  "scripts": {
    "start": "ts-node -r tsconfig-paths/register src/index.ts"
  }
}

Webpack

使用 webpack 热加载时,也可以通过tsconfig-paths-webpack-plugin来读取 tsconfig 配置:

//packages/foo/webpack.config.js
const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin");

module.exports = {
  resolve: {
    plugins: [new TsconfigPathsPlugin()],
  },
};

Babel

babel 的话,我暂时没有发现特别好用的 plugin,但是自己写个简单的正则也能实现别名功能:

// packages/bar/babel.config.js
module.exports = {
  plugins: [
    [
      "module-resolver",
      {
        alias: {
          "^@onion/(.+)": "../\\1/src",
        },
      },
    ],
  ],
};

还有一些工具诸如 rollup、jest、cra 脚手架等等,配置起来也大同小异,改改别名就行了。大家有兴趣的话可以自己试试。

小结

本期简单介绍了一下前端项目配合 yarn workspaces 管理 monorepo 的一些常用手段。上面提到的 monorepo 和 multirepo 优缺点对比,不知道大家是否有“感同身受”的体会?

我自己参与过一个大型项目,从几人到几十人,后来又萎缩到几个人。我们最初用的是 multirepo,当时玩得不亦乐乎;后来,人力流失,由几个人管理 multirepo,就变得异常艰辛了。现在回忆起来,各中体会历历在目。

虽然 monorepo 和 multirepo 各有千秋吧,但是我个人还是推荐使用 monorepo,因为你无法预计项目的未来;在交接时,monorepo 至少是完整的,不像 multirepo 你根本猜不透其中会遗漏掉多少信息。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容