Skip to content
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待

项目引用

项目引用允许你将 TypeScript 程序结构化为更小的部分,此功能在 TypeScript 3.0 及更高版本中可用。

通过这样做,你可以显著改善构建时间,强制组件之间的逻辑分离,并以新的、更好的方式组织代码。

我们还为 tsc 引入了一种新模式,即 --build 标志,它与项目引用协同工作,以实现更快的 TypeScript 构建。

示例项目

让我们看一个相当普通的程序,看看项目引用如何帮助我们更好地组织它。 假设你有一个包含两个模块 converterunits 的项目,以及每个模块对应的测试文件:

/
├── src/
│   ├── converter.ts
│   └── units.ts
├── test/
│   ├── converter-tests.ts
│   └── units-tests.ts
└── tsconfig.json

测试文件导入实现文件并进行一些测试:

ts
// converter-tests.ts
import * as converter from "../src/converter";

assert.areEqual(converter.celsiusToFahrenheit(0), 32);

以前,如果你使用单个 tsconfig 文件,这种结构处理起来相当麻烦:

  • 实现文件可能导入测试文件
  • 无法在不使 src 出现在输出文件夹名称中的情况下同时构建 testsrc,而这可能不是你想要的
  • 仅更改实现文件中的内部内容需要重新类型检查测试,即使这永远不会导致新错误
  • 仅更改测试需要重新类型检查实现,即使没有任何更改

你可以使用多个 tsconfig 文件来解决部分问题,但会出现新问题:

  • 没有内置的最新检查,因此你最终总是运行两次 tsc
  • 调用两次 tsc 会带来更多的启动时间开销
  • tsc -w 无法同时在多个配置文件上运行

项目引用可以解决所有这些以及更多问题。

什么是项目引用?

tsconfig.json 文件有一个新的顶级属性,references。它是一个对象数组,指定要引用的项目:

js
{
    "compilerOptions": {
        // 常规选项
    },
    "references": [
        { "path": "../src" }
    ]
}

每个引用的 path 属性可以指向包含 tsconfig.json 文件的目录,或者指向配置文件本身(可以有任何名称)。

当你引用一个项目时,会发生新的事情:

  • 从引用的项目导入模块将改为加载其输出声明文件(.d.ts
  • 如果引用的项目生成 outFile,则输出文件 .d.ts 文件的声明将在此项目中可见
  • 构建模式(见下文)将在需要时自动构建引用的项目

通过拆分为多个项目,你可以显著提高类型检查和编译的速度,减少编辑器使用时的内存使用,并改进程序逻辑分组的强制执行。

composite

被引用的项目必须启用新的 composite 设置。 此设置是确保 TypeScript 能够快速确定被引用项目的输出位置所必需的。 启用 composite 标志会改变几件事:

  • 如果未显式设置,rootDir 设置默认为包含 tsconfig 文件的目录
  • 所有实现文件必须由 include 模式匹配或在 files 数组中列出。如果违反此约束,tsc 将告知你哪些文件未指定
  • 必须打开 declaration

declarationMap

我们还增加了对声明源映射的支持。 如果你启用了 declarationMap,你将能够使用编辑器功能,如“转到定义”和重命名,在受支持的编辑器中跨项目边界透明地导航和编辑代码。

项目引用的注意事项

项目引用有一些权衡,你应该注意。

由于依赖项目使用从其依赖项构建的 .d.ts 文件,你将要么需要检入某些构建输出,要么在克隆项目后构建它,然后才能在编辑器中导航项目而不会看到虚假错误。

当使用 VS Code(自 TS 3.7 起)时,我们在后台有一个内存中的 .d.ts 生成过程,应该能够缓解这个问题,但它有一些性能影响。对于非常大的复合项目,你可能希望使用 disableSourceOfProjectReferenceRedirect 选项禁用它。

此外,为了与现有的构建工作流程保持兼容,tsc 不会自动构建依赖项,除非使用 --build 开关调用。 让我们更多地了解 --build

TypeScript 的构建模式

一个期待已久的功能是 TypeScript 项目的智能增量构建。 在 3.0 中,你可以将 --build 标志与 tsc 一起使用。 这实际上是 tsc 的一个新入口点,它的行为更像一个构建编排器,而不是一个简单的编译器。

运行 tsc --build(简称 tsc -b)将执行以下操作:

  • 查找所有引用的项目
  • 检测它们是否是最新的
  • 按正确顺序构建过时的项目

你可以为 tsc -b 提供多个配置文件路径(例如 tsc -b src test)。 就像 tsc -p 一样,如果配置文件名为 tsconfig.json,则无需指定文件名本身。

tsc -b 命令行

你可以指定任意数量的配置文件:

shell
> tsc -b                            # 使用当前目录中的 tsconfig.json
> tsc -b src                        # 使用 src/tsconfig.json
> tsc -b foo/prd.tsconfig.json bar  # 使用 foo/prd.tsconfig.json 和 bar/tsconfig.json

不用担心在命令行上传递文件的顺序 - tsc 会在需要时重新排序它们,以便始终先构建依赖项。

还有一些特定于 tsc -b 的标志:

  • --verbose:打印详细的日志以解释正在发生的事情(可以与其他任何标志组合)
  • --dry:显示将做什么,但实际不构建任何东西
  • --clean:删除指定项目的输出(可以与 --dry 组合)
  • --force:就好像所有项目都已过时一样
  • --watch:监视模式(不能与除 --verbose 之外的任何标志组合)

注意事项

通常,tsc 会在存在语法或类型错误时产生输出(.js.d.ts),除非启用了 noEmitOnError。 在增量构建系统中这样做会很糟糕——如果你的一个过时的依赖项有一个新错误,你只会看到它一次,因为后续构建会跳过构建现在已更新的项目。 因此,tsc -b 实际上对所有项目都像启用了 noEmitOnError 一样。

如果你检入了任何构建输出(.js.d.ts.d.ts.map 等),你可能需要在某些源代码控制操作之后运行 --force 构建,具体取决于你的源代码控制工具是否在本地副本和远程副本之间保留时间戳。

MSBuild

如果你有一个 msbuild 项目,可以通过将以下内容添加到项目文件中来启用构建模式:

xml
    <TypeScriptBuildMode>true</TypeScriptBuildMode>

这将启用自动增量构建以及清理。

请注意,与 tsconfig.json / -p 一样,现有的 TypeScript 项目属性将不被尊重——所有设置都应使用你的 tsconfig 文件进行管理。

一些团队已经设置了基于 msbuild 的工作流程,其中 tsconfig 文件与其配对的托管项目具有相同的隐式图排序。 如果你的解决方案是这样的,你可以继续将 msbuildtsc -p 以及项目引用一起使用;这些是完全可互操作的。

指南

整体结构

随着更多的 tsconfig.json 文件,你通常希望使用配置文件继承来集中你的公共编译器选项。 这样你可以在一个文件中更改设置,而不必编辑多个文件。

另一个好的做法是有一个“解决方案” tsconfig.json 文件,它简单地具有对所有叶节点项目的 references,并将 files 设置为空数组(否则解决方案文件将导致文件被双重编译)。请注意,从 3.0 开始,如果 tsconfig.json 文件中至少有一个 reference,则空 files 数组不再是一个错误。

这提供了一个简单的入口点;例如,在 TypeScript 仓库中,我们只需运行 tsc -b src 来构建所有端点,因为我们在 src/tsconfig.json 中列出了所有子项目。

你可以在 TypeScript 仓库中看到这些模式 - 请参见 src/tsconfig-base.jsonsrc/tsconfig.jsonsrc/tsc/tsconfig.json 作为关键示例。

针对相对模块的结构化

通常,转换使用相对模块的仓库不需要太多工作。 只需在给定父文件夹的每个子目录中放置一个 tsconfig.json 文件,并向这些配置文件添加 reference 以匹配程序的预期分层。 你需要将 outDir 设置为输出文件夹的显式子文件夹,或者将 rootDir 设置为所有项目文件夹的公共根目录。

针对 outFiles 的结构化

使用 outFile 的编译布局更加灵活,因为相对路径不那么重要。 TypeScript 仓库本身就是一个很好的参考 - 我们有一些“库”项目和一些“端点”项目;“端点”项目尽可能小,只引入它们需要的库。