自从发现了Typescript Project References这项特性,我把部分项目从rollup迁移到了tsc

1,061 阅读4分钟

作者简介:陈富强,前端阿强,目前就职于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",
  },
}

上面这个项目结构存在几个问题:

  1. 如果在math.ts 中不小心import了math.test.ts 文件,这在语言规范上是完全合法的,但这往往可能不是我们想要的结果。

  2. 假设我们的math.ts目标运行环境是浏览器,如果不小心在math.ts中引入了node模块,且项目中安装了 @types/node,那么此时编译不会提示报错,仍然可以编译成功,但是运行时会提示找不到对应的模块。

  3. 不希望math.test.ts 测试文件生成对应的产物math.test.js 文件。

  4. 无法同时编译出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"]                              
  },
}

我们一点一点来看上面这些配置文件是如何解决上述问题的。

  1. types配置成空数组,即把node的类型声明给剔除了,解决了不小心引入node模块但编译不报错的问题。

  2. 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时,一些编译行为和传统的会有些差别

  1. 如果rootDir没有被指定,那么默认值会被设定为当前tsconfig 配置文件所在的目录,传统编译行为默认为所有源代码的共同最长路径目录。

  2. 所有源代码必须遵从files,include,exclude配置约定,否则编译不通过。正是因为这个限制存在,之前的问题1也得以解决。

  3. declaration 配置必须打开。