TypeScript 如何做 monorepo
写代码时,你有没有过这样的经历:一个项目拆成了用户系统、订单系统、支付系统,却发现改了用户系统里的一个公共函数,订单和支付系统还要手动复制粘贴,或是复制后改出了两个不一样的版本?或是找一个公共工具函数,要在几十个文件里翻来覆去?其实,有一种方法能解决这些问题,就是monorepo,而用TypeScript来实现它,还能让你的代码更清晰、更少出错。
先搞懂:monorepo是什么?和普通项目有啥不一样?
很多人会把代码按模块拆成好几个独立的包,比如一个用户包、一个工具包,再把这些包放在不同的仓库里——这叫polyrepo,就像把不同的东西放进不同的抽屉,虽然整洁,但找起来麻烦,还容易重复写代码。而monorepo是把所有相关的包都放进同一个仓库,就像把常用的工具都放在同一个工具箱里,需要的时候直接拿,改的时候也能统一改。

举个生活化的例子:你做家庭早餐,把面包、鸡蛋、牛奶分别放进三个抽屉(polyrepo),每次做早餐都要分别拉开三个抽屉拿,要是发现面包坏了,还要去三个抽屉确认有没有同款面包;而monorepo就是把面包、鸡蛋、牛奶都放进一个橱柜,拿的时候直接开一个门,坏了也只需要检查这一个柜子。
TypeScript 做 monorepo,先搭好基础工具
用TypeScript做monorepo,不用自己写一堆复杂的配置,现在有现成的工具能帮你省事儿,最常用的就是pnpm和TypeScript自己的工程引用(Project References)。
首先,pnpm是个包管理器,它比npm、yarn更省空间,而且专门优化了monorepo的依赖管理——你可以在根目录建一个pnpm-workspace.yaml,告诉pnpm哪些是你的包,比如写:
packages:
- 'packages/*'
这样pnpm就知道,所有放在packages文件夹里的文件夹,都是你要管理的子包,不用再每个包都单独装一遍依赖。
然后是TypeScript的工程引用,这是核心。每个子包都要有自己的tsconfig.json,比如你建了两个子包:packages/user和packages/utils,那utils的tsconfig.json里,要把user设成它的依赖,写:
{
"references": [{ "path": "../user" }]
}
这样当你在user包里改了代码,utils能自动知道用的是最新的user代码,不用手动发布包再安装,就像你把面包和鸡蛋放在同一个橱柜,拿鸡蛋的时候直接用最新的面包,不用再去商店买新面包。
举个实际的小例子,跟着做一遍就会了
我们来搭一个最简单的monorepo,全是TypeScript,一共三步:
第一步:建根文件夹,比如叫my-monorepo,里面建三个东西:pnpm-workspace.yaml、tsconfig.json、packages文件夹。根的tsconfig.json是给所有子包用的公共配置,写个基础的就行,不用太复杂;
第二步:在packages里建第一个包utils,它放公共工具函数,比如写一个格式化日期的函数,文件是packages/utils/src/date.ts;
第三步:在packages里建第二个包user,它要用到utils里的格式化函数,那在user的index.ts里,直接import { formatDate } from '@my-monorepo/utils',这里的@my-monorepo/utils是你给utils起的别名,在tsconfig.json里配一下路径就行,不用写长串的相对路径。
这时候你运行pnpm install,pnpm会自动把两个包的依赖连起来,不用在user里单独装utils,就像你把厨房的工具连在同一个电源上,不用每个工具都插一个插座。
最后说几个新手常踩的小坑
做monorepo刚开始容易遇到问题,比如改了utils的代码,user没用到最新的,这时候只要运行pnpm build,TypeScript会自动检查各个包的引用,把最新的代码编译出来;还有别把所有依赖都装在根目录,要按每个包的需要装,不然会冲突,就像你别把所有调料都倒在一个碗里,分开装才好用。
其实TypeScript做monorepo,本质就是把分散的代码串成一个整体,让你不用在不同的包之间来回折腾,不管是改公共函数还是找代码,都能省很多时间,项目大了之后,这种好处会特别明显。