tsc性能优化Project References使用详解
作者:草帽Plasticine
什么是 Project References
在了解一个东西是什么的时候,直接看其官方定义是最直观的
TypeScript: Documentation中对于Project References
的介绍如下:
Project references are a new feature in TypeScript 3.0 that allow you to structure your TypeScript programs into smaller pieces.
这是TypeScript 3.0
新增的特性,这个特性有啥用呢?
将我们的项目分成多个小的片段,也就是允许我们将项目进行分包模块化
这样一来我们在执行tsc
对项目进行构建的时候,无论是出于代码转译成js
的目的还是说出于单纯的类型检查(将compilerOptions.noEmit
置为true
)的目的
都可以严格按照自己的需要对想要被tsc
处理的那部分代码进行处理,而不是每次都对整个项目进行处理,这是我认为Project References
的最大好处
就以一个前后端代码在同一个仓库里维护的项目为例,如果没有Project References
,那么我执行tsc
时,会对前后端模块都进行类型检查和转译
但实际上如果我只修改了前端部分的代码,理所应当让tsc
只处理前端模块,后端模块的构建产物不需要进行重新构建,有了Project References
,我们就能实现到这个需求,从而优化项目的构建或类型检查性能
相信大家对Project References
有一个大概的认识了,接下来我们就开始实际动手体验一下Project References
加深我们对它的理解吧!
示例项目结构
. ├── package.json ├── pnpm-lock.yaml ├── src │ ├── __test__ // 单元测试 │ │ ├── client.test.ts // 前端代码测试 │ │ ├── index.ts // 简陋的单元测试 API │ │ └── server.test.ts // 后端代码测试 │ ├── client // 前端模块 │ │ └── index.ts │ ├── server // 后端模块 │ │ └── index.ts │ └── shared // 共享模块 -- 包含通用的工具函数 │ └── index.ts └── tsconfig.json // TypeScript 配置
这是一个很常见的项目目录结构,有前端代码,有后端代码,也有通用工具函数代码以及前后端的单元测试代码
它们的依赖关系如下:
- client 依赖 shared
- server 依赖 shared
__test__
依赖 client 和 server- shared 无依赖
不使用 Project References 带来的问题
现在整个项目只有一个tsconfig.json
位于项目根目录下,其内容如下:
{ "compilerOptions": { "target": "ES5", "module": "CommonJS", "strict": true, "outDir": "./dist" } }
如果我们执行tsc
,它会将各个模块的代码都打包到项目根目录的dist
目录下
dist ├── __test__ │ ├── client.test.js │ ├── index.js │ └── server.test.js ├── client │ └── index.js ├── server │ └── index.js └── shared └── index.js
这有一个很明显的问题,正如前面所说,当我们只修改一个模块,比如只修改了前端模块的代码,那么理应只需要再构建前端模块的产物即可,但是无论改动范围如何,都是会将整个项目都构建一次,这在项目规模变得越来越大的时候会带来极大的性能问题,构建时长会变得特别长
或许你会想着在每个模块里创建一个tsconfig.json
,然后通过tsc -p
指定每个模块的目录去单独对它们进行构建,没错,这是一种解决方案
但是这会带来下面两个问题:
- 如果需要全量构建项目,你得需要运行三次
tsc
,对每个模块分别构建,而tsc
的启动时间开销是比较大的,在这个小规模项目里甚至启动开销的时间比实际构建的时间更长,现在还只是运行三次tsc
,如果项目模块很多,有几十上百个呢?那光是启动tsc
几十上百次都已经会花一些时间了 tsc -w
不能一次监控多个tsconfig.json
,只能是对各个模块都启动一次tsc -w
Project References
的出现,就是为了解决上述问题的
tsconfig.json 的 references 配置项
Project References
就是tsconfig.json
里的references
配置项,其结构是一个包含若干对象的数组,对象的结构如下:
{ "references": [{ "path": "path/to/referenced-project" }] }
核心就是一个path
属性,该属性指向被引用的项目模块路径,该路径下需要包含tsconfig.json
,如果该模块不是用tsconfig.json
命名的话,你也可以指定具体的文件名,比如:
{ "references": [{ "path": "path/to/referenced-project/tsconfig.web.json" }] }
当指定了references
选项后,会发生如下改变:
- 在主模块中导入被引用的模块时,会加载它的类型声明文件,也就是
.d.ts
后缀的文件 - 使用
tsc --build
或tsc -b
构建主模块时,会自动构建被引用的模块
这样一来能够带来三个好处:
- 提升类型检查和构建的速度
- 减少
IDE
的运行内存占用 - 更容易对项目结构进行划分
tsconfig.json 的 composite 配置项
光是在主模块中指定references
配置项还不够,还需要在被引用的项目对应的tsconfig.json
中开启composite
配置项
composite
配置项又是干嘛的呢? -- 它可以帮助tsc
快速确定如何寻找被引用项目的输出产物
当被引用的项目开启composite
配置项后,会有如下改变和要求:
当未指定rootDir
时,默认值不再是The longest common path of all non-declaration input files
,而是包含了tsconfig.json
的目录
Tips: 关于The longest common path of all non-declaration input files
的意思可以到tsconfig.json 文章中关于 rootDir 的介绍中查阅
必须开启include
或者files
配置项将要参与构建的文件声明进来
必须开启declaration
配置项(因为前面介绍references
的时候说了,会加载被引入模块的类型声明文件,因此被引用模块自然得开启declaration
配置项生成自己的类型声明文件供主模块加载)
使用 Project References 改造示例项目
根据目前我们对Project References
的认识,现在可以开始改造一下我们的项目了,首先是根目录下的tsconfig.json
配置,它起到一个类似于项目入口的作用,因此这里面只负责添加references
声明项目中需要被构建的模块,以及通过exclude
将不需要参与构建的模块排除(比如src/__test__
中的测试代码)
/tsconfig.json
{ "references": [ { "path": "src/client" }, { "path": "src/server" }, { "path": "src/shared" } ], "exclude": ["**/__test__"] }
然后是各个子模块的tsconfig.json
配置,这里我们假设构建目标为es5
的代码,所以对于client
、server
以及shared
来说是存在公共配置的,所以我们可以抽离出一个公共配置,然后在子模块中通过extends
配置项公用一个配置
/tsconfig.base.json
{ "compilerOptions": { "target": "ES5", "module": "CommonJS", "strict": true } }
src/client/tsconfig.json
{ "extends": "../../tsconfig.base.json", "compilerOptions": { "outDir": "../../dist/client", "composite": true, "declaration": true }, // 依赖哪个模块则引用哪个模块 "references": [{ "path": "../shared" }] }
src/server/tsconfig.json
{ "extends": "../../tsconfig.base.json", "compilerOptions": { "outDir": "../../dist/server", "composite": true, "declaration": true }, // 依赖哪个模块则引用哪个模块 "references": [{ "path": "../shared" }] }
src/shared/tsconfig.json
{ "extends": "../../tsconfig.base.json", "compilerOptions": { "outDir": "../../dist/shared", "composite": true, "declaration": true } }
全量构建
现在我们在项目根目录下运行tsc --build --verbose
,就会根据references
配置去寻找各个子模块,并对它们进行构建,可以理解为对项目的全量构建
--build
参数表示让tsc
以build
模式进行构建和类型检查,也就是会使用references
配置项,如果不开启的话是不会使用references
配置项的,这点可以从官方文档中得证:
--verbose
参数则是会将构建过程中的输出显示在控制台中,不开启该参数的话则不会显示输出(除非构建过程中报错)
运行后/dist
目录结构如下
dist ├── client │ ├── index.d.ts │ ├── index.js │ └── tsconfig.tsbuildinfo ├── server │ ├── index.d.ts │ ├── index.js │ └── tsconfig.tsbuildinfo └── shared ├── index.d.ts ├── index.js └── tsconfig.tsbuildinfo
可以看到,所有子模块都被构建进来了,各模块的产物中有一个tsconfig.tsbuildinfo
文件,这个文件起到一个类似缓存的作用,对于后续进行增量构建有着重要作用
前面看到的官方文档中对tsc --build
的作用的介绍中的第二点Detect if they are up-to-date
主要就是依靠这个缓存文件去识别的
开启--verbose
参数后,可以看到控制台输出如下:
[4:50:26 PM] Projects in this build:
* src/shared/tsconfig.json
* src/client/tsconfig.json
* src/server/tsconfig.json
* tsconfig.json
[4:50:26 PM] Project 'src/shared/tsconfig.json' is out of date because output file 'dist/shared/tsconfig.tsbuildinfo' does not exist
[4:50:26 PM] Building project '/home/plasticine/demo/ts-reference-demo/src/shared/tsconfig.json'...
[4:50:28 PM] Project 'src/client/tsconfig.json' is out of date because output file 'dist/client/tsconfig.tsbuildinfo' does not exist
[4:50:28 PM] Building project '/home/plasticine/demo/ts-reference-demo/src/client/tsconfig.json'...
[4:50:28 PM] Project 'src/server/tsconfig.json' is out of date because output file 'dist/server/tsconfig.tsbuildinfo' does not exist
[4:50:28 PM] Building project '/home/plasticine/demo/ts-reference-demo/src/server/tsconfig.json'...
[4:50:29 PM] Project 'tsconfig.json' is out of date because output 'src/shared/index.js' is older than input 'src/client'
[4:50:29 PM] Building project '/home/plasticine/demo/ts-reference-demo/tsconfig.json'...
[4:50:29 PM] Updating unchanged output timestamps of project '/home/plasticine/demo/ts-reference-demo/tsconfig.json'...
xxx out of date xxx
意思就是这个模块没被构建过,因此会开始对其进行构建
由于我们是首次构建,所以三个模块都是没被构建过的,所以三个模块都被检测为out of date
当我们再次运行tsc --build --verbose
时,输出如下:
4:54:35 PM - Projects in this build:
* src/shared/tsconfig.json
* src/client/tsconfig.json
* src/server/tsconfig.json
* tsconfig.json
4:54:35 PM - Project 'src/shared/tsconfig.json' is up to date because newest input 'src/shared/index.ts' is older than output 'dist/shared/tsconfig.tsbuildinfo'
4:54:35 PM - Project 'src/client/tsconfig.json' is up to date because newest input 'src/client/index.ts' is older than output 'dist/client/tsconfig.tsbuildinfo'
4:54:35 PM - Project 'src/server/tsconfig.json' is up to date because newest input 'src/server/index.ts' is older than output 'dist/server/tsconfig.tsbuildinfo'
4:54:35 PM - Project 'tsconfig.json' is up to date because newest input 'dist/server/index.d.ts' is older than output 'src/client/index.js'
可以看到,所有模块都被检测为up to date
,从而避免了重复构建
增量构建
如果现在我们修改了client
模块的代码,再运行tsc --build --verbose
会怎样呢?估计你也能猜到了,只有client
模块会被构建,而其他模块则会跳过
4:56:44 PM - Projects in this build:
* src/shared/tsconfig.json
* src/client/tsconfig.json
* src/server/tsconfig.json
* tsconfig.json
4:56:44 PM - Project 'src/shared/tsconfig.json' is up to date because newest input 'src/shared/index.ts' is older than output 'dist/shared/tsconfig.tsbuildinfo'
4:56:44 PM - Project 'src/client/tsconfig.json' is out of date because output 'dist/client/tsconfig.tsbuildinfo' is older than input 'src/client/index.ts'
4:56:44 PM - Building project '/home/plasticine/demo/ts-reference-demo/src/client/tsconfig.json'...
4:56:45 PM - Project 'src/server/tsconfig.json' is up to date because newest input 'src/server/index.ts' is older than output 'dist/server/tsconfig.tsbuildinfo'
4:56:45 PM - Project 'tsconfig.json' is out of date because output file 'src/client/index.js' does not exist
4:56:45 PM - Building project '/home/plasticine/demo/ts-reference-demo/tsconfig.json'...
4:56:45 PM - Updating unchanged output timestamps of project '/home/plasticine/demo/ts-reference-demo/tsconfig.json'...
相信现在你能体会到Project References
的好处了吧,能够很大程度上优化我们的构建速度!
不过实际开发中,tsc
更多的是用来进行类型检查,至于compile
的工作,则更多地是交给如Babel
、swc
、esbuild
等工具去完成,这也是官方文档中有提到过的
这也是为什么你在vite
创建的项目中能够看到默认的build
命令配置为tsc && vite build
,正是将类型检查的工作交给tsc
,而构建工作则交给vite
底层依赖的rollup
去完成
对__test__测试代码的处理
我们的改造貌似已经完成了,但其实还忽略了一个src/__test__
,它也可以被视为一个模块,它作为主模块,依赖了client
和server
,因此也可以给它加上tsconfig.json
配置,并且对于测试代码,我们一般不希望将它们构建成js
,只希望tsc
负责类型检查的工作,因此我们需要进行如下配置:
src/__test__/tsconfig.json
{ "compilerOptions": { "noEmit": true }, "references": [{ "path": "../client" }, { "path": "../server" }] }
noEmit
的作用刚刚在官方文档中也看到了,不会把产物文件输出,如果我们只需要类型检查能力的话很适合开启该配置项
现在我们如果需要对__test__
中的代码进行类型检查的话,只需要执行:
# 忽略 references 配置项 tsc --project src/__test__ # 启用 references 配置项 tsc --build src/__test__
如果是使用--project
参数的话,tsconfig.json
中可以忽略references
配置项,因为即便配置了也不会被使用,这在依赖产物未构建出来时能起作用
而如果使用--build
参数,并且client
和server
未构建出来时,会先构建它们,再对测试代码进行类型检查,可以根据个人需求场景来决定使用--project
还是--build
总结
本篇文章介绍了Project References
是什么,并通过一个简单的示例项目,并结合TypeScript Documentation
官方文档边实战边解释
总的来说,其使用起来就是:
- 主模块(
tsc --build
作用的模块视为主模块)中通过references
配置项声明依赖的模块 - 被引用模块中开启
composite
和declaration
配置项以支持被引用 - 通过
tsc --build 主模块
才可以启用references
配置项,这在官方文档中被称为Build Mode
,如果直接tsc 主模块
的话,是不会启用references
配置项的,也就导致依然会对项目中的所有ts
文件进行编译(如果没配置include
或files
配置项的话)
希望通过本篇文章,能够让你对Project References
有一个全面了解,也希望能够将其用在你的项目中,提升类型检查或构建(使用 tsc 进行构建的话)的速度,更多关于tsc性能Project References的资料请关注脚本之家其它相关文章!