作者简介:陈富强,前端阿强,目前就职于Tiktok直播前端团队,擅长传统Web/Web Hybrid方向,目前团队还有HC名额,感兴趣的同学欢迎来撩我!
Typescript 3.0 新增了一项特性Project References,这项特性能够让你将你的项目拆分成互相独立的几个部分,从而提升tsc的编译速度。除此之外,Project References 特性还能让我们执行一次命令,同时生成不同格式(cjs/esm)的产物,而笔者我,在发现这项特性之后,也把自己部分项目中的rollup迁移到了tsc。
一般来说,一个项目在根目录下会有一个全局的tsconfig.json 文件,假设有个项目存在如下结构:
.
├── src
│ ├── math.test.ts
│ └── math.ts
└── tsconfig.json
tsconfig.json 配置如下
{
"include": ["./src/**/*.ts"],
"compilerOptions": {
"target": "es2016",
"module": "esnext",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"rootDir": "src",
"outDir": "dist",
},
}
上面这个项目结构存在几个问题:
-
如果在math.ts 中不小心import了math.test.ts 文件,这在语言规范上是完全合法的,但这往往可能不是我们想要的结果。
-
假设我们的math.ts目标运行环境是浏览器,如果不小心在math.ts中引入了node模块,且项目中安装了
@types/node
,那么此时编译不会提示报错,仍然可以编译成功,但是运行时会提示找不到对应的模块。 -
不希望math.test.ts 测试文件生成对应的产物math.test.js 文件。
-
无法同时编译出esmodule/commonjs 格式的产物。
我们可以通过增加tsconfig的配置来解决部分上面这些问题,我们将源代码分成两部分,一部分是测试代码,一部分是生产代码,生产代码再细分,还可以划分为esmodule格式和commonjs格式,我们按照这样的划分来拆分我们的tsconfig配置文件,改造后的文件目录如下:
.
├── src
│ ├── math.test.ts
│ └── math.ts
├── tsconfig.json
├── tsconfig.cjs.json
└── tsconfig.test.json
tsconfig.json
{
"include": ["./src/**/*.ts"],
"exclude": ["./src/**/*.test.ts"],
"compilerOptions": {
"target": "es2016",
"module": "esnext",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"rootDir": "src",
"outDir": "dist/esm",
"lib": ["dom", "esnext"],
"types": []
},
}
tsconfig.cjs.json
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "commonjs",
"outDir": "dist/cjs",
},
}
tsconfig.test.json
{
"extends": "./tsconfig.json",
"include": ["./src/**/*.ts"],
"exclude": [],
"compilerOptions": {
"module": "commonjs",
"noEmit": true,
"types": ["node", "jest"]
},
}
我们一点一点来看上面这些配置文件是如何解决上述问题的。
-
types配置成空数组,即把node的类型声明给剔除了,解决了不小心引入node模块但编译不报错的问题。
-
tsconfig.test.json 中的noEmit 配置使得对应的编译不会生成实际的产物。
于是乎,我们可以执行以下命令来编译整个项目
tsc -p tsconfig.json # 编译esm格式的产物
tsc -p tsconfig.cjs.json # 编译commonjs格式的产物
tsc -p tsconfig.test.json # 校验测试文件是否能编译通过
可以看到我们虽然解决了部分问题,但是问题1和问题4仍没有被解决,问题4可以通过引入rollup等打包工具来解决,这也是我一直以来的解决方式,直到我发现了Typescript Project References
这个特性,我们接下来来介绍这个特性。
Typescript Project References 可以将项目拆分成更小的几个部分,结合tsc 的build模式,能够实现上面三条命令并行执行的效果,同时,每个部分的编译都是独立的,互不干扰,也就是说,如果只改了math.test.ts文件,只会重新执行与测试相关的文件编译,从而提升编译速度,以上面这个项目为基础,我们继续做改造。
.
├── src
│ ├── math.test.ts
│ └── math.ts
├── tsconfig.json
├── tsconfig.esm.json
├── tsconfig.cjs.json
└── tsconfig.test.json
tsconfig.json
{
"files": [],
"compilerOptions": {
"target": "es2016",
"module": "esnext",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"rootDir": "src",
"lib": ["dom", "esnext"],
"types": []
},
"references": [
{ "path": "./tsconfig.esm.json" },
{ "path": "./tsconfig.cjs.json" },
{ "path": "./tsconfig.test.json" }
],
}
tsconfig.esm.json
{
"extends": "./tsconfig.json",
"include": ["./src/**/*.ts"],
"exclude": ["./src/**/*.test.ts"],
"compilerOptions": {
"module": "commonjs",
"outDir": "dist/esm",
"composite": true
},
}
tsconfig.cjs.json
{
"extends": "./tsconfig.json",
"include": ["./src/**/*.ts"],
"exclude": ["./src/**/*.test.ts"],
"compilerOptions": {
"module": "commonjs",
"outDir": "dist/cjs",
"composite": true
},
}
tsconfig.test.json
{
"extends": "./tsconfig.json",
"include": ["./src/**/*.ts"],
"exclude": [],
"compilerOptions": {
"module": "commonjs",
"noEmit": true,
"types": ["node", "jest"],
"composite": true
},
}
references 是tsconfig配置文件的根属性,值类型为一个数组,数组的每一项都表示着项目中的一部分,其中path属性可以是一个包含tsconfig.json的文件夹路径,也可以是一个tsconfig 配置文件的路径。此时如果我们执行tsc —build ,tsc 会自动帮我们分别生成esmodule/commonjs 格式的产物。执行完效果如下:
.
├── dist
│ ├── esm
│ │ ├── math.js
│ │ └── math.d.ts
│ └── cjs
│ ├── math.js
│ └── math.d.ts
├── src
│ ├── math.test.ts
│ └── math.ts
├── tsconfig.json
├── tsconfig.esm.json
├── tsconfig.cjs.json
└── tsconfig.test.json
除了基础的tsconfig.json 配置文件之外,我们会发现其他被引用的tsconfig配置文件中都指定了composite
为true,这个在被引用的配置文件中是必须配置的,当composite
为true时,一些编译行为和传统的会有些差别
-
如果
rootDir
没有被指定,那么默认值会被设定为当前tsconfig 配置文件所在的目录,传统编译行为默认为所有源代码的共同最长路径目录。 -
所有源代码必须遵从
files
,include
,exclude
配置约定,否则编译不通过。正是因为这个限制存在,之前的问题1也得以解决。 -
declaration 配置必须打开。