tsconfig.json 详解

文件选项

ts 文件有三种类型,分别是 tsd.tstsx

如果没有任何配置,ts 就会按照默认配置编译当前目录下得所有 ts 文件。

// tsconfig.json

{}
json

运行以下命令,可以看到 ts 项目中所有 ts 文件都会被编译。

tsc
typescript

tsconfig_default.png

files

它是一个文件数组,表示编译器需要编译得单个文件列表。

{
  "files": [
    "src/a.ts"
  ]
}
json

这样再次执行 tsc 就只会编译 a.ts 文件。

include

它也是一个数组,表示编译器需要编译的文件或者目录。

我们可以在 src 目录下新建 lib 目录,创建 lib.ts 文件。

// src/lib/lib.ts

let libs = {}
typescript
{
  "files": [
    "src/a.ts"
  ],
  "include": [
    "src"
  ]
}
json

如果 include 配置为 src,会编译所有文件,包括子目录文件。

include 支持通配符配置。

{
  "files": [
    "src/a.ts"
  ],
  "include": [
    "src/*"
  ]
}
json

src/* 代表只会编译 src 目录下一级目录的文件。子目录不会被编译。

{
  "files": [
    "src/a.ts"
  ],
  "include": [
    "src/*/*"
  ]
}
json

src/*/* 代表只会编译 src 目录下二级目录下的文件。
当前案例下会编译 lib 目录下所有文件以及 a.ts 文件, files 和 include 配置是会合并的。

exclude

表示编译器需要排除的的文件或者文件夹,默认 exclude 会排除 node_modules 目录下所有的文件。也会排除所有的声明文件。

{
  "files": [
    "src/a.ts"
  ],
  "include": [
    "src/*/*"
  ],
  "exclude": [
    "src/lib"
  ]
}
json

上述配置,exclude 会覆盖 include 配置。也就是有 lib 目录不会被编译。

{
  "files": [
    "src/a.ts"
  ],
  "include": [
    "src"
  ],
  "exclude": [
    "src/lib"
  ]
}
json

当前配置编译 src 目录下除 lib 目录的所有文件。

抽离复用

以上就是编译器管理文件的一些选项,此外配置文件之间是可以继承的,我们可以把基础的一些配置抽离出来方便复用。

新建 tsconfig.base.json 文件。

// tsconfig.base.json

{
  "files": [
    "src/a.ts"
  ],
  "include": [
    "src"
  ],
  "exclude": [
    "src/lib"
  ]
}
json
// tsconfig.json

{
  "extends": "./tsconfig.base"
}
json

我们可以用 extends 继承基础配置,执行 tsc 是一样的效果。

其次在 tsconfig.json 文件中,我们也可以覆盖 tsconfig.base.json 文件中的配置。

// tsconfig.json

{
  "extends": "./tsconfig.base",
  "exclude": []
}
json

这里指定不排除任何目录,这样编译器就会编译 src 目录下所有文件。

compileOnSave

它的作用是保存文件时编译器是否自动编译。不过 vscode 并不支持这个配置。你可以用其他编译器尝试使用一下。

// tsconfig.json

{
  "extends": "./tsconfig.base",
  "exclude": [],
  "compileOnSave": true
}
json

编译选项

本小节我们将学习跟编译相关的选项,这些选项有近 100 个,非常庞杂。我们仅介绍一些常用的选项,对于不常用的可以参考官方文档。

incremental

增量编译。ts 编译器可以在第一次编译后生成一个可以存储编译信息的文件,然后在二次编译时会对这个文件进行增量编译。这样可以提高编译速度。执行 tsc 命令会在根目录下生成 tsconfig.tsbuildinfo 文件。

我们可以设置 diagnostics 属性,通过这个可以看出两次编译时间的差距。

{
  "compilerOptions": {
    "incremental": true, // 增量编译
    "diagnostics": true, // 打印诊断信息
  }
}
json

compile.png

我们还可以通过 tsBuildInfoFile 配置增量编译文件的存储位置(路径、文件名称可以自定义)。

{
  "compilerOptions": {
    "incremental": true, // 增量编译
    "tsBuildInfoFile": ".tsbuildinfo", // 增量编译文件的存储位置
    "diagnostics": true, // 打印诊断信息
  }
}
json

target/module

{
  "compilerOptions": {
    "target": "ES3", // 目标语言的版本
    "module": "commonjs", // 生成代码的模块标准
  }
}
json

outFile

{
  "compilerOptions": {
    "target": "ES3",
    "module": "amd",
    "outFile": "./app.js", // 将多个相互依赖的文件生成一个文件,可以用在 AMD 模块中
  }
}
json

我们定义一个 amd.ts 文件。

// src/amd.ts

let a = 1

export = a
typescript

然后在 index.ts 中引入它。

// src/index.ts

import a = require('./amd')
typescript

运行 tsc 命令会在根目录下生成 app.js 文件。会将 index.tsamd.ts 这两个文件合并打包成一个文件。

// app.js

var s = 'a';
define("amd", ["require", "exports"], function (require, exports) {
    "use strict";
    var a = 1;
    return a;
});
define("index", ["require", "exports"], function (require, exports) {
    "use strict";
    exports.__esModule = true;
    (function () {
        var hello = 'hello world';
        document.querySelectorAll('.app')[0].innerHTML = hello;
    })();
});
typescript

lib

{
  "compilerOptions": {
    "lib": [], // TS 需要引用的库声明文件,es5 默认 "dom", "es5", "scripthost"
  }
}
json

如果这个配置不指定,也会默认导入一些类库。当 target 为 es5 时,会默认导入 "dom","es5","scripthost"

如果我们要在程序中使用 ES 高级版本的特性,就需要用到这个属性。比如我们需要用到 es2019 的特性。

console.log([1, 2, [3, 4]].flat())
typescript
{
  "compilerOptions": {
    "lib": ["dom", "es5", "scripthost", "ES2019.Array"],
  }
}
json

allowJs/checkJs

{
  "compilerOptions": {
    "allowJs": true, // 允许编译器编译 JS 文件(js、jsx)
    "checkJs": true, // 允许在 JS 文件中报错,通常与 allowJs 一起使用
  }
}
json

再次执行 tsc 编译会有报错信息。

Cannot write file 'D:/workspace/notes/typescript/plus/ts-config/build/webpack.base.config.js' because it would overwrite input file.
# ...
shell

这是因为 ts 编译器会尝试编译当前目录下的所有 js 文件。包括 webpack 的配置文件。我们需要排除它,只编译 src 目录下文件。

{
  "include": ["src"], // 只编译 src 目录下的文件
  "compilerOptions": {
    "allowJs": true, // 允许编译器编译 JS 文件(js、jsx)
    "checkJs": true, // 允许在 JS 文件中报错,通常与 allowJs 一起使用
  }
}
json

这样再次编译就不会有报错了。

outDir/rootDir

我们还可以指定输出目录。编译后的文件就会被打包到指定目录。

{
  "include": ["src"], // 只编译 src 目录下的文件
  "compilerOptions": {
    "allowJs": true, // 允许编译器编译 JS 文件(js、jsx)
    "checkJs": true, // 允许在 JS 文件中报错,通常与 allowJs 一起使用
    "outDir": "./temp", // 指定输出目录
  }
}
json

outDir.png

我们还可以定义 rootDir 指定输入文件目录,默认就是当前目录。

{
  "include": ["src"], // 只编译 src 目录下的文件
  "compilerOptions": {
    "allowJs": true, // 允许编译器编译 JS 文件(js、jsx)
    "checkJs": true, // 允许在 JS 文件中报错,通常与 allowJs 一起使用
    "outDir": "./temp", // 指定输出目录
   	"rootDir": "./" , // 指定输入文件目录(用于输出)
  }
}
json

rootDir.png

rootDir 指定的是当前目录,所以输出目录中就会包含 src 目录。如果我们把输入目录指定为 src。

{
  "include": ["src"], // 只编译 src 目录下的文件
  "compilerOptions": {
    "allowJs": true, // 允许编译器编译 JS 文件(js、jsx)
    "checkJs": true, // 允许在 JS 文件中报错,通常与 allowJs 一起使用
    "outDir": "./temp", // 指定输出目录
   	"rootDir": "./src" , // 指定输入文件目录(用于输出)
  }
}
json

rootDir02.png

从图中可以看到,输出的目录就不包含 src 目录。这个选项是用来控制输出目录结构的。

sourcemap

{
  "compilerOptions": {
    "sourceMap": true, // 生成目标文件的 sourceMap
  }
}
json

开启 sourceMap 会为我们自动生成 sourceMap 文件。

sourceMap.png

// amd.js.map

{"version":3,"file":"amd.js","sourceRoot":"","sources":["amd.ts"],"names":[],"mappings":";AAAA,IAAI,CAAC,GAAG,CAAC,CAAA;AAET,iBAAS,CAAC,CAAA"}
js

我们还可以使用 inlineSourceMap

{
  "compilerOptions": {
    // "sourceMap": true, // 生成目标文件的 sourceMap
    "inlineSourceMap": true, // 生成目标文件的 inline sourceMap
  }
}
json
// a.js

var s = 'a';
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbImEudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsSUFBSSxDQUFDLEdBQVcsR0FBRyxDQUFBIn0=
js

inlineSourceMap 会包含在生成的 js 文件中。

声明文件

{
  "compilerOptions": {
    "declaration": true, // 生成声明文件
  }
}
json

如果我们配置 declaration,当编译时会自动生成声明文件(默认情况下与文件同级)。

我们还可以控制声明文件路径。

{
  "compilerOptions": {
    "declaration": true, // 生成声明文件
    "declarationDir": "./typings", // 声明文件路径
  }
}
json

这样声明文件就会输出到 typings 目录下。但是这样会存在一个问题,ts 编译器也会执行编译,生成 js 文件。

我们可以告知 ts 编译器只生成声明文件,而不进行文件编译。

{
  "compilerOptions": {
    "declaration": true, // 生成声明文件
    "declarationDir": "./typings", // 声明文件路径
    "emitDeclarationOnly": true, // 只生成声明文件
  }
}
json

我们还可以为声明文件生成 sourceMap

{
  "compilerOptions": {
    "declaration": true, // 生成声明文件
    "declarationDir": "./typings", // 声明文件路径
    "emitDeclarationOnly": true, // 只生成声明文件
    "declarationMap": true, // 生成声明文件的 sourceMap
  }
}
json

sourceMap02.png

// a.d.ts.map

{"version":3,"file":"a.d.ts","sourceRoot":"","sources":["../src/a.ts"],"names":[],"mappings":"AAAA,QAAA,IAAI,CAAC,EAAE,MAAY,CAAA"}
js

下面我们再来看下另外两个配置项。

{
  "compilerOptions": {
    "typeRoots": [], // 声明文件目录,默认查找 node_modules/@types
    "types": [] , // 声明文件包,指定需要加载的声明文件的包,会在 @types 目录下查找
  }
}
json

对于 types 配置,如果我们指定某一个包,ts 只会加载这个包的声明文件。

removeComments

{
  "compilerOptions": {
    "removeComments": true, // 删除注释
  }
}
json

noEmit/noEmitOnError

{
  "compilerOptions": {
    "noEmit": true, // 不输出文件
  }
}
json

noEmit 不输出任何文件,也就是不做任何事。还有一个类似的选项。

{
  "compilerOptions": {
    // "noEmit": true, // 不输出文件
    "noEmitOnError": true, // 发生错误时不输出文件
  }
}
json

helpers

我们先来看一下 noEmitHelpers

{
  "compilerOptions": {
    "noEmitHelpers": true, // 不生成 helper 函数,需要额外安装 ts-helpers
  }
}
json

这个选项会涉及一个类的继承。

// src/index.ts

class A {}
class B extends A {}
typescript

我们先来看一下没有配置 noEmitHelpers 的编译后文件。

"use strict";
var __extends = (this && this.__extends) || (function () {
    var extendStatics = function (d, b) {
        extendStatics = Object.setPrototypeOf ||
            ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
            function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
        return extendStatics(d, b);
    };
    return function (d, b) {
        if (typeof b !== "function" && b !== null)
            throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
        extendStatics(d, b);
        function __() { this.constructor = d; }
        d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
    };
})();
exports.__esModule = true;
(function () {
    var hello = 'hello world';
    document.querySelectorAll('.app')[0].innerHTML = hello;
})();
var A = /** @class */ (function () {
    function A() {
    }
    return A;
}());
var B = /** @class */ (function (_super) {
    __extends(B, _super);
    function B() {
        return _super !== null && _super.apply(this, arguments) || this;
    }
    return B;
}(A));
js

在编译结果中我们可以看到,除了我们的代码,还引入了其他的工具库函数。它会使我们编译后的代码体积增加。通过 noEmitHelpers 我们可以控制是否生成 helpers 函数。下面再来看一下配置 noEmitHelpers 编译后的文件。

"use strict";
exports.__esModule = true;
(function () {
    var hello = 'hello world';
    document.querySelectorAll('.app')[0].innerHTML = hello;
})();
var A = /** @class */ (function () {
    function A() {
    }
    return A;
}());
var B = /** @class */ (function (_super) {
    __extends(B, _super);
    function B() {
        return _super !== null && _super.apply(this, arguments) || this;
    }
    return B;
}(A));
js

可以看到 helpers 函数已经没有了。但是代码中的 __extends 函数是未定义的,需要我们额外安装 ts-helpers

之前的做法需要我们额外安装 ts-helpers ,现在 ts 为我们提供了另外一种方式去引入 helpers 函数。

{
  "compilerOptions": {
    "noEmitHelpers": true, // 不生成 helper 函数,需要额外安装 ts-helpers
    "importHelpers": true, // 通过 tslib 引入 helper 函数,文件必须是模块
  }
}
json

我们可以通过 importHelpers 配置,使用 ts 内置的库把 helper 函数引入进来(文件必须是一个模块)。我们需要改造一下代码。

// src/index.ts

class A {}
class B extends A {}

export = B
typescript

再次执行 tsc 编译代码。

"use strict";
var tslib_1 = require("tslib");
(function () {
    var hello = 'hello world';
    document.querySelectorAll('.app')[0].innerHTML = hello;
})();
var A = /** @class */ (function () {
    function A() {
    }
    return A;
}());
var B = /** @class */ (function (_super) {
    tslib_1.__extends(B, _super);
    function B() {
        return _super !== null && _super.apply(this, arguments) || this;
    }
    return B;
}(A));
module.exports = B;
js

可以看到编译后的代码引入了 tslib 的库,lib 中就包含了一些 helper 函数。这样就可以减少打包后代码体积。

downlevelIteration

{
  "compilerOptions": {
    "downlevelIteration": true, // 降级遍历器的实现(es3/5)
  }
}
json

如果我们的目标语言是 es3 或者 es5,就会对遍历器有一个降级的实现。

// src/index.ts

const arr = [1, 2, 3]
const arr2 = [4, ...arr]
typescript

期间可能会报错,请考虑升级 “tslib” 的版本。ts(2343)。我们可以手动安装 tslib 来解决它。

pnpm i tslib --save-dev

"use strict";
var tslib_1 = require("tslib");
(function () {
    var hello = 'hello world';
    document.querySelectorAll('.app')[0].innerHTML = hello;
})();
var A = /** @class */ (function () {
    function A() {
    }
    return A;
}());
var B = /** @class */ (function (_super) {
    tslib_1.__extends(B, _super);
    function B() {
        return _super !== null && _super.apply(this, arguments) || this;
    }
    return B;
}(A));
var arr = [1, 2, 3];
var arr2 = tslib_1.__spreadArray([4], tslib_1.__read(arr), false);
module.exports = B;
js

类型检查

{
  "compilerOptions": {
    "strict": true, // 开启严格代码检查
    // "alwaysStrict": false, // 在代码中注入 "use strict";
    // "noImplicitAny": false, // 不允许隐式的 any 类型
    // "strictNullChecks": false, // 不允许把 null、undefined 赋值给其他类型变量
    // "strictFunctionTypes": false, // 不允许函数参数双向协变
    // "strictPropertyInitialization": false, // 类的实例属性必须初始化
    // "strictBindCallApply": false, // 严格的 bind/call/apply 检查
    // "noImplicitThis": false, // 不允许 this 有隐式的 any 类型
  }
}
json

如果开启 strict,strict 下面所有注释掉的配置也会默认开启。

function add(x: number, y: number) {
  return x + y
}
add.call(undefined, 1, 2)
add.call(undefined, 1, '2') // 类型“string”的参数不能赋给类型“number”的参数。
typescript

上述代码如果在严格模式下,会报错,如果关闭 strictBindCallApply 。ts 就会放过类型检查。

class A {
  a: number = 1

  getA () {
    return function() {
      console.log(this.a) // "this" 隐式具有类型 "any",因为它没有类型注释。
    }
  }
}
const a = new A().getA()
a()
typescript

上述代码说的就是 noImplicitThis ,这里的 this 在某些环境下可能是 undefined。比如运行上面代码就会报错。

函数相关

{
  "compilerOptions": {
    // "noUnusedLocals": true, // 检查只声明,未使用的局部变量
    // "noUnusedParameters": true, // 检查只声明的函数参数
    // "noFallthroughCasesInSwitch": true, // 防止 switch 语句贯穿
    // "noImplicitReturns": true, // 每个分支都要有返回值
  }
}
json

上述的配置会为我们提示错误,但是不会阻碍编译。

switch 语句贯穿的意思就是就是:如果某一个分支没有 break 语句,下面的一系列语句都会依次执行。

模块相关

{
  "compilerOptions": {
    // "esModuleInterop": true, // 允许 export = 导出,由 import from 导入
    // "allowUmdGlobalAccess": true, // 允许在模块中访问 UMD 全局变量
    "moduleResolution": "Node", // 模块解析策略
  }
}
json

关于 moduleResolution ,ts 默认使用 node 解析策略。还有其他选项,例如 ClassicNode16NodeNext 等 。

classic 模块解析策略

用于 amd、system js、es2015 模块。

相对导入:

// /root/src/moduleA.ts
import { b } from './moduleB'

// 文件解析策略

// 1. /root/src/moduleB.ts
// 2. /root/src/moduleB.d.ts
typescript

非相对导入:

// /root/src/moduleA.ts
import { b } from './moduleB'

// 文件解析策略

// 1. /root/src/node_modules/moduleB.ts
// 2. /root/src/node_modules/moduleB.d.ts

// 如果本地目录没有,会依次向上查找 ...

// 3. /root/node_modules/moduleB.ts
// 4. /root/node_modules/moduleB.d.ts

// 5. node_modules/moduleB.ts
// 6. node_modules/moduleB.d.ts
typescript

node 模块解析策略

相对导入:

// /root/src/moduleA.ts
import { b } from './moduleB'

// 文件解析策略

// 1. /root/src/moduleB.ts 
// 2. /root/src/moduleB.tsx
// 3. /root/src/moduleB.d.ts
// 4. /root/src/moduleB/package.json ("types" 属性,优先查找 "types" 属性,如果没有会查找 index)
// 5. /root/src/moduleB/index.ts 
// 6. /root/src/moduleB/index.tsx
// 7. /root/src/moduleB/index.d.ts
typescript

非相对导入:

// /root/src/moduleA.ts
import { b } from './moduleB'

// 文件解析策略

// 1. /root/src/moduleB.ts 
// 2. /root/src/moduleB.tsx
// 3. /root/src/moduleB.d.ts
// 4. /root/src/moduleB/package.json
// 5. /root/src/moduleB/index.ts 
// 6. /root/src/moduleB/index.tsx
// 7. /root/src/moduleB/index.d.ts

// 8. /root/moduleB.ts 
// 9. /root/moduleB.tsx
// 10. /root/moduleB.d.ts
// 11. /root/moduleB/package.json
// 12. /root/moduleB/index.ts 
// 13. /root/moduleB/index.tsx
// 14. /root/moduleB/index.d.ts

// 如果本地目录没有,会依次向上查找,直到根目录下的 node_modules
json

下面我们再来看下其他配置。

{
  "compilerOptions": {
    "baseUrl": "./", // 解析非相对模块的基地址,默认是当前目录
    // 路径映射(如果我们不想导入 jquery 的默认版本,而是当导入精简版本,就可以在这里指定)
    "paths": {
      "jquery": ["node_modules/jquery/dist/jquery.slim.min.js"]
    },
    "rootDirs": ["src", "temp"], // 将多个目录放到一个虚拟目录下,用于运行时
  }
} 
json

这里的 rootDirs 和前面的 rootDir 完全是两个概念,它可以为多个目录建立一个虚拟目录。应用场景如下:

我们新建 temp 目录,用来存放构建之后的文件。

// temp/utils.ts

export const util = {}
typescript

我们可以单独编译一下这个文件。

tsc .\temp\utils.ts -d # 编译文件并生成声明文件
shell

下面我们把源文件删除。即 temp/utils.ts 文件。有些时候我们构件好一个类库后,就不会二次构建了。它会永远放到这个输出目录中。如果在其他位置想引用这个类库,我们可以这样做。

// src/index.ts

import { util } from './utils'
console.log(util)
typescript

因为 index.ts 编译后也会输出到 temp 目录,所以我们要想编译后也可以引用 util 文件,可以假定当前就在 temp 目录。但是这样很明显是有问题的。找不到模块“./util”或其相应的类型声明。ts(2307)

这时我们就需要用到 rootDirs 配置。

{
  "compilerOptions": {
    "rootDirs": ["src", "temp"], // 将多个目录放到一个虚拟目录下,用于运行时
  }
} 
json

这样编译后就会认为它们在一个目录下。这样我们就可以在 src 目录下直接使用 temp 中的 util。编译后引用路径也不会发生改变。

打印相关

{
  "compilerOptions": {
    "listEmittedFiles": true, // 打印输出文件
    "listFiles": true, // 打印编译文件(包括引用的声明文件)
  }
} 
json

可以在命令行打印相关信息。

emitter.png

总结

本小节我们介绍了很多编译配置项,不需要你全都记住,建议遵循这样的原则,遇到问题解决问题。

如果在开发中遇到不太清楚的报错,可以去配置中找一找,也许你的报错通过一个配置项就可以解决。同时你可以发现编码中不规范的地方。此外还有一些配置项没有说明,我们后面再来讲解。

工程引用

工程引用是 ts 3.0 引入的新特性。我们来看下具体的应用场景。

project.png

我们有一个项目,是一个前后端混合项目,并且有公共的引用文件,且服务端和客户端代码的测试用例都在一个目录中。

// src/common/index.ts
export function getTime() {
  const time = new Date()
  return `${time.getFullYear()}-${time.getMonth() + 1}-${time.getDate()}`
}

// src/client/index.ts
import { getTime } from '../common'

console.log(`Client Time: ${getTime()}`)

class Client {} 

export = Client

// src/server/index.ts
import { getTime } from '../common'

console.log(`Server Time: ${getTime()}`)

class Server {} 

export = Server

// test/client.test.ts
import Client = require('../src/client')

const c = new Client()

// test/server.test.ts
import Server = require('../src/server')

const c = new Server()
ts
// tsconfig.json

{
  "compilerOptions": {
    "target": "ES5",
    "module": "CommonJS",
    "strict": true,
    "outDir": "./dist"
  }
}
json

以上就是目前工程的所有代码。根据我们的配置,ts 编译器会将编译后的文件输出到 dist 目录,我们可以使用 tsc 命令执行构建。

编译后的文件目录如下。

project02.png

我们并不希望构建后的目录有一个 src 层级,我们想把 client、common、server 直接构建到 dist 目录下。

我们可以在配置文件中加入 include 实现。

{
  "compilerOptions": {
    "target": "ES5",
    "module": "CommonJS",
    "strict": true,
    "outDir": "./dist"
  },
  "include": ["src"]
}
json

这样可以实现我们的目的,但是你会发现这样就把 test 目录给遗忘。还有一些不方便的地方就是,我们不能单独的构建客户端应用或者服务端应用。此外,我们也不想把测试用例构建到 dist 目录下。以上的问题都是通过单个配置文件不能解决的。工程引用就是用来解决这类问题的。它可以灵活控制输出目录,还可以使工程之间产生依赖关系,可以把一个大的项目拆分成小的项目,同时还可以利用增量编译提升编译速度。下面来看一下使用工程引用改造后的项目。

new_project.png

上述代码基本没有改动,只是我们为每个目录增加了自己的 tsconfig.json 文件。

// tsconfig.json
{
  "compilerOptions": {
    "target": "ES5",
    "module": "CommonJS",
    "strict": true,
    "composite": true, // 工程可以被引用并且可以增量编译
    "declaration": true, // 生成声明文件
  },
}

// src/client/tsconfig.json
{
  "extends": "../../tsconfig.json",
  "compilerOptions": {
    "outDir": "../../dist/client"
  },
  "references": [
    {
      "path": "../common"
    }
  ]
}

// src/server/tsconfig.json
{
  "extends": "../../tsconfig.json",
  "compilerOptions": {
    "outDir": "../../dist/server"
  },
  "references": [
    {
      "path": "../common"
    }
  ]
}

// src/common/tsconfig.json
{
  "extends": "../../tsconfig.json",
  "compilerOptions": {
    "outDir": "../../dist/common"
  }
}

// test/tsconfig.json
{
  "extends": "../tsconfig.json",
  "references": [
    { "path": "../src/client" },
    { "path": "../src/server" }
  ]
}
json

ts 为了支持工程引用,为我们提供了一种新的构建模式。即 build 构建模式,它可以单独构建一个工程。相关的依赖也会被自动构建。

tsc -b src/server --verbose # 构建 server 工程
tsc -b src/client --verbose # 构建 client 工程
shell

构建完毕后生成目录如下:

new_project02.png

再次执行构建命令,你会发现构建速度也很快。因为我们启用了增量编译。

下面我们再来构建一下测试用例。

tsc -b test --verbose # 构建测试用例
shell

你会发现 test 目录下会生成构建后的文件。

new_project03.png

我们可以用以下命令清空构建文件。

tsc -b test --clean
shell

从以上可以看出,工程引用的优点。首先它解决了输出目录结构的问题,其次它解决了单个工程构建的问题,然后它还可以通过增量编译提升构建速度。其实 typescript 项目本身已经使用工程引用改造过。

我们可以来看一下 typescript 工程 的 src 目录。当前目录下有一个基础配置,tsconfig-base.json 文件。

// src/tsconfig.json

{
    "compilerOptions": {
        "pretty": true,
        "lib": ["es2015.iterable", "es2015.generator", "es5"],
        "target": "es5",
        "moduleResolution": "node",
        "rootDir": ".",

        "declaration": true,
        "declarationMap": true,
        "sourceMap": true,
        "composite": true,
        "noEmitOnError": true,

        "strictNullChecks": true,
        "noImplicitAny": true,
        "noImplicitThis": true,
        "strictPropertyInitialization": true,
        "noUnusedLocals": true,
        "noUnusedParameters": true,

        "skipLibCheck": true,

        "alwaysStrict": true,
        "preserveConstEnums": true,
        "newLine": "lf",

        "types": []
    }
}
json

其他的文件夹都是一个子工程。以 tsc 目录为例。

// src/tsc/tsconfig.hson

{
    "extends": "../tsconfig-noncomposite-base",
    "compilerOptions": {
        "outFile": "../../built/local/tsc.js"
    },
    "files": [
        "tsc.ts"
    ],
    "references": [
        { "path": "../compiler", "prepend": true },
        { "path": "../executeCommandLine", "prepend": true }
    ]
}
json

ts 官方配置是一个很好的参照,你可以自己去学习一下。看看它是怎么利用工程引用特性来支撑这样一个庞大项目的构建。

如何在根目录一次性构建所有工程?

可以在 src 下新建一个 tsconfig.json 文件,通过运行 tsc -b src 来一次性构建所有工程。

{
  "files": [],
  "include": [],
  "references": [
    { "path": "./client" },
    { "path": "./common" },
    { "path": "./server" }
  ]
}
json
tsc -b src --verbose # 构建 src 目录下所有工程
shell