如何在TypeScript中使用模块
作者选择了 COVID-19 Relief Fund 作为 Write for DOnations 计划的一部分来接受捐赠。
介绍
Modules 是一种将代码组织成更小、更易于管理的部分的方法,允许程序从应用程序的不同部分导入代码。 多年来,有一些策略可以在 JavaScript 代码中实现模块化。 TypeScript 与 ECMAScript 规范一起发展,为 JavaScript 程序提供了一个标准模块系统,可以灵活地处理这些不同的格式。 TypeScript 支持创建和使用具有类似于 ES 模块语法 的统一语法的模块,同时允许开发人员输出针对不同模块加载器的代码,例如 Node.js (CommonJS[ X238X])、require.js (AMD)、UMD、SystemJS 或 ECMAScript 2015 原生模块 (ES6)。
在本教程中,您将在 TypeScript 中创建和使用模块。 您将在自己的 TypeScript 环境中遵循不同的代码示例,展示如何使用 import
和 export
关键字,如何设置默认导出,以及如何使用覆盖的 [X202X ] 对象与您的代码兼容。
先决条件
要遵循本教程,您将需要:
- 一个环境,您可以在其中执行 TypeScript 程序以跟随示例。 要在本地计算机上进行设置,您将需要以下内容: 为了运行处理 TypeScript 相关包的开发环境,同时安装了 Node 和 npm(或 yarn)。 本教程使用 Node.js 版本 14.3.0 和 npm 版本 6.14.5 进行了测试。 要在 macOS 或 Ubuntu 18.04 上安装,请按照如何在 macOS 上安装 Node.js 和创建本地开发环境或如何在 Ubuntu 18.04 上安装 Node.js 的使用 PPA 安装部分中的步骤进行操作。 如果您使用的是适用于 Linux 的 Windows 子系统 (WSL),这也适用。
- 您将需要足够的 JavaScript 知识,尤其是 ES6+ 语法,例如 解构、剩余运算符 和 导入/导出 。 如果您需要有关这些主题的更多信息,建议阅读我们的 如何在 JavaScript 中编码系列。
- 本教程将参考支持 TypeScript 并显示内联错误的文本编辑器的各个方面。 这不是使用 TypeScript 所必需的,但确实可以更多地利用 TypeScript 功能。 为了获得这些好处,您可以使用像 Visual Studio Code 这样的文本编辑器,它完全支持开箱即用的 TypeScript。
本教程中显示的所有示例都是使用 TypeScript 4.2.2 版创建的。
设置项目
在此步骤中,您将创建一个示例项目,其中包含两个用于处理向量运算的小类:Vector2
和 Vector3
。 在这种情况下,vector 是指幅度和距离的数学测量,通常用于可视图形程序中。
您构建的类将对每个类进行一个操作:向量加法。 稍后,您将使用这些示例类来测试代码从一个程序到另一个程序的导入和导出。
首先,为自己创建一个存放示例代码的目录:
mkdir vector_project
创建目录后,将其设为您的工作目录:
cd vector_project
现在您位于项目的根目录,使用 npm
创建您的 Node.js 应用程序:
npm init
这将为您的项目创建一个 package.json
文件。
接下来,添加 TypeScript 作为开发依赖项:
npm install typescript@4.2.2 --save-dev
这会将 TypeScript 安装到您的项目中,并将 TypeScript 编译器设置为其默认设置。 要进行自己的自定义设置,您需要创建一个特定的配置文件。
在项目的根目录中创建并打开一个名为 tsconfig.json
的文件。 要让您的项目使用本教程中的练习,请将以下内容添加到文件中:
tsconfig.json
{ "compilerOptions": { "target": "ES6", "module": "CommonJS", "outDir": "./out", "rootDir": "./src", "strict": true } }
在此代码中,您正在为 TypeScript 编译器设置多个配置。 "target": "ES6"
确定编译代码的环境,"outDir": "./out"
和 "rootDir": "./src"
分别指定哪些目录将保存编译器的输出和输入。 "strict": true
设置强类型检查的级别。 最后,"module": "CommonJS"
指定模块系统为CommonJS
。 您将使用它来模拟使用 Node.js 应用程序。
设置好项目后,您现在可以继续使用基本语法创建模块。
使用 export
在 TypeScript 中创建模块
在本节中,您将使用 TypeScript 模块语法在 TypeScript 中创建模块。
默认情况下,TypeScript 中的文件被视为全局脚本。 这意味着文件中声明的任何 变量、 类、 函数 或其他构造都是全局可用的。 一旦您开始在文件中使用模块,该文件就会成为模块范围,并且不再全局执行。
为了展示这一点,您将创建您的第一个类:Vector2
。 在项目的根目录中创建一个名为 src/
的新目录:
mkdir src
这是您在 tsconfig.json
文件中设置为根目录 (rootDir
) 的目录。
在此文件夹中,创建一个名为 vector2.ts
的新文件。 在你喜欢的文本编辑器中打开这个文件,然后编写你的 Vector2
类:
vector_project/src/vector2.ts
class Vector2 { constructor(public x: number, public y: number) {} add(otherVector2: Vector2) { return new Vector2(this.x + otherVector2.x, this.y + otherVector2.y); } }
在这段代码中,您声明了一个名为 Vector2
的类,它是通过将两个数字作为参数传递来创建的,并设置为属性 x
和 y
。 这个类有一种方法,它通过组合各自的 x
和 y
值来为自己添加一个向量。
由于您的文件当前未使用模块,因此您的 Vector2
是全局范围的。 要将文件转换为模块,您只需导出 Vector2
类:
vector_project/src/vector2.ts
export class Vector2 { constructor(public x: number, public y: number) {} add(otherVector2: Vector2) { return new Vector2(this.x + otherVector2.x, this.y + otherVector2.y); } }
文件 src/vector2.ts
现在是一个具有单个导出的模块:Vector2
类。 保存并关闭您的文件。
接下来,您可以创建 Vector3
类。 在 src/
目录中创建 vector3.ts
文件,然后在您喜欢的文本编辑器中打开该文件并编写以下代码:
vector_project/src/vector3.ts
export class Vector3 { constructor(public x: number, public y: number, public z: number) {} add(otherVector3: Vector3) { return new Vector3( this.x + otherVector3.x, this.y + otherVector3.y, this.z + otherVector3.z ); } }
此代码创建了一个类似于 Vector2
的类,但具有一个额外的 z
属性,用于在三个维度中存储向量。 注意 export
关键字已经使这个文件成为它自己的模块。 保存并关闭此文件。
现在你有两个文件,vector2.ts
和 vector3.ts
,它们都是模块。 每个文件都有一个导出,这是它们所代表的向量的类。 在下一节中,您将使用 import
语句将这些模块放入其他文件中。
通过 import
在 TypeScript 中使用模块
在上一节中,您看到了如何创建模块。 在本节中,您将导入这些模块以在代码的其他地方使用。
在 TypeScript 中使用模块时的一个常见场景是拥有一个文件,该文件收集多个模块并将它们重新导出为一个模块。 为了演示这一点,请在 src/
目录中创建一个名为 vectors.ts
的文件,然后在您喜欢的编辑器中打开此文件并编写以下内容:
vector_project/src/vectors.ts
import { Vector2 } from "./vector2"; import { Vector3 } from "./vector3";
要导入项目中可用的另一个模块,请在 import
语句中使用文件的相对路径。 在这种情况下,您将从 ./vector2
和 ./vector3
导入两个模块,它们是从当前文件到文件 src/vector2.ts
和 src/vector3.ts
的相对路径。
现在您已经导入了向量,您可以使用以下突出显示的语法将它们重新导出到单个模块中:
vector_project/src/vectors.ts
import { Vector2 } from "./vector2"; import { Vector3 } from "./vector3"; export { Vector2, Vector3 };
export {}
语法允许您导出多个标识符。 在这种情况下,您将使用单个 export
声明导出 Vector2
和 Vector3
类。
您还可以使用两个单独的 export
语句,如下所示:
vector_project/src/vectors.ts
import { Vector2 } from "./vector2"; import { Vector3 } from "./vector3"; export { Vector2 }; export { Vector3 };
这与前面的代码片段具有相同的含义。
由于您的 src/vectors.ts
仅导入两个类以稍后重新导出它们,因此您可以使用更简洁的语法形式:
vector_project/src/vectors.ts
export { Vector2 } from "./vector2"; export { Vector3 } from "./vector3";
import
语句在这里是隐含的,TypeScript Compiler 会自动包含它。 然后文件将立即以相同的名称导出。 保存此文件。
现在您正在从 src/vectors.ts
文件中导出两个矢量类,创建一个名为 src/index.ts
的新文件,然后打开该文件并编写以下代码:
vector_project/src/index.ts
import { Vector2, Vector3 } from "./vectors"; const vec2a = new Vector2(1, 2); const vec2b = new Vector2(2, 1); console.log(vec2a.add(vec2b)); const vec3a = new Vector3(1, 2, 3); const vec3b = new Vector3(3, 2, 1); console.log(vec3a.add(vec3b));
在此代码中,您将从 src/vectors.ts
文件中导入两个矢量类,该文件使用相对路径 ./vectors
。 然后,您将创建一些向量实例,使用 add
方法将它们相加,然后记录结果。
导入命名导出时,您还可以使用不同的别名,这有助于避免文件内的命名冲突。 要尝试此操作,请对您的文件进行以下突出显示的更改:
vector_project/src/index.ts
import { Vector2 as Vec2, Vector3 as Vec3 } from "./vectors"; const vec2a = new Vec2(1, 2); const vec2b = new Vec2(2, 1); console.log(vec2a.add(vec2b)); const vec3a = new Vec3(1, 2, 3); const vec3b = new Vec3(3, 2, 1); console.log(vec3a.add(vec3b));
在这里,您使用 as
关键字为导入的类设置别名 Vec2
和 Vec3
。
请注意您是如何导入 ./vectors
中可用的所有内容的。 该文件仅导出这两个类,因此您可以使用以下语法将所有内容导入单个变量:
vector_project/src/index.ts
import * as vectors from "./vectors"; const vec2a = new vectors.Vector2(1, 2); const vec2b = new vectors.Vector2(2, 1); console.log(vec2a.add(vec2b)); const vec3a = new vectors.Vector3(1, 2, 3); const vec3b = new vectors.Vector3(3, 2, 1); console.log(vec3a.add(vec3b));
在上面突出显示的代码中,您使用 import * as
语法将模块导出的所有内容导入到单个变量中。 您还必须更改使用 Vector2
和 Vector3
类的方式,因为它们现在可以在导入期间创建的 vectors
对象中使用。
如果保存文件并使用 tsc
编译项目:
npx tsc
TypeScript 编译器将创建 out/
目录(给定您在 tsconfig.json
文件中设置的 compileOptions.outDir
选项),然后使用 JavaScript 文件填充该目录。
在您喜欢的文本编辑器中打开位于 out/index.js
的已编译文件。 它看起来像这样:
vector_project/out/index.js
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const vectors = require("./vectors"); const vec2a = new vectors.Vector2(1, 2); const vec2b = new vectors.Vector2(2, 1); console.log(vec2a.add(vec2b)); const vec3a = new vectors.Vector3(1, 2, 3); const vec3b = new vectors.Vector3(3, 2, 1); console.log(vec3a.add(vec3b));
由于 tsconfig.json
文件中的 compilerOptions.module
选项设置为 CommonJS
,TypeScript 编译器会创建与 Node.js 模块系统兼容的代码。 这使用 require
函数将其他文件作为模块加载。
接下来,看一下编译后的 src/vectors.ts
文件,该文件位于 out/vectors.js
:
vector_project/out/vectors.js
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Vector3 = exports.Vector2 = void 0; var vector2_1 = require("./vector2"); Object.defineProperty(exports, "Vector2", { enumerable: true, get: function () { return vector2_1.Vector2; } }); var vector3_1 = require("./vector3"); Object.defineProperty(exports, "Vector3", { enumerable: true, get: function () { return vector3_1.Vector3; } });
在这里,TypeScript 编译器创建的代码与使用 CommonJS 时导出模块的方式兼容,即将导出的值分配给 exports
对象。
现在您已经尝试了导入和导出文件的语法,然后了解了它们是如何编译成 JavaScript 的,您可以继续在文件中声明默认导出。
使用默认导出
在本节中,您将研究另一种从称为默认导出的模块导出值的方法,它将特定导出设置为从模块中假定的导入。 这可以在您导入文件时简化代码。
再次打开src/vector2.ts
文件:
vector_project/src/vector2.ts
export class Vector2 { constructor(public x: number, public y: number) {} add(otherVector2: Vector2) { return new Vector2(this.x + otherVector2.x, this.y + otherVector2.y); } }
请注意您是如何从文件/模块中导出单个值的。 您可以编写导出的另一种方法是使用默认导出。 每个文件最多可以有一个默认导出,所以这在这里很方便。
要将导出更改为默认导出,请添加以下突出显示的代码:
vector_project/src/vector2.ts
export default class Vector2 { constructor(public x: number, public y: number) {} add(otherVector2: Vector2) { return new Vector2(this.x + otherVector2.x, this.y + otherVector2.y); } }
保存文件,然后在 src/vector3.ts
文件中执行相同操作:
vector_project/src/vector3.ts
export default class Vector3 { constructor(public x: number, public y: number, public z: number) {} add(otherVector3: Vector3) { return new Vector3( this.x + otherVector3.x, this.y + otherVector3.y, this.z + otherVector3.z ); } }
要导入默认导出,请保存文件,然后打开 src/vectors.ts
文件并将其内容更改为以下内容:
vector_project/src/vectors.ts
import Vector2 from "./vector2"; import Vector3 from "./vector3"; export { Vector2, Vector3 };
请注意,对于这两个导入,您只是为导入命名,而不必使用解构来导入特定值。 这将自动导入每个模块的默认导出。
每个具有默认导出的模块还有一个名为 default
的特殊导出,可用于访问默认导出值。 要使用您之前使用的 export ... from
速记语法,您可以使用该命名导出:
vector_project/src/vectors.ts
export { default as Vector2 } from "./vector2"; export { default as Vector3 } from "./vector3";
现在,您正在以特定名称重新导出每个模块的默认导出。
使用 export =
和 import = require()
进行兼容性
一些模块加载器,如 AMD 和 CommonJS,有一个名为 exports
的对象,其中包含模块导出的所有值。 当使用任何这些模块加载器时,可以通过更改 exports
对象的值来覆盖导出的对象。 这类似于 ES 模块中可用的默认导出,因此也类似于 TypeScript 本身。 但是,这两种语法是不兼容的。 在本节中,您将了解 TypeScript 如何以与默认导出兼容的方式处理此行为。
在 TypeScript 中,当以支持覆盖导出对象的模块加载器为目标时,您可以使用 export =
语法更改导出对象的值。 为此,您将导出对象的值分配给 export
标识符。 如果您过去使用过 Node.js,这与使用 exports =
相同。
注意: 在进行以下任何更改之前,请确保 tsconfig.json
文件中的选项 compilerOptions.module
设置为 CommonJS
。
想象一下,您想将每个矢量文件中的导出对象更改为指向矢量类本身。 打开文件 src/vector2.ts
。 要更改导出对象本身的值,请进行以下突出显示的更改:
vector_project/src/vector2.ts
export = class Vector2 { constructor(public x: number, public y: number) {} add(otherVector2: Vector2) { return new Vector2(this.x + otherVector2.x, this.y + otherVector2.y); } };
保存此文件,然后对文件 src/vector3.ts
执行相同操作:
vector_project/src/vector3.ts
export = class Vector3 { constructor(public x: number, public y: number, public z: number) {} add(otherVector3: Vector3) { return new Vector3( this.x + otherVector3.x, this.y + otherVector3.y, this.z + otherVector3.z ); } };
最后,将 vectors.ts
改回如下:
vector_project/src/vectors.ts
import Vector2 from "./vector2"; import Vector3 from "./vector3"; export { Vector2, Vector3 };
保存这些文件,然后运行 TypeScript 编译器:
npx tsc
TypeScript 编译器会给你多个错误,包括:
Outputsrc/vectors.ts:1:8 - error TS1259: Module '"~/project/src/vector2"' can only be default-imported using the 'esModuleInterop' flag 1 import Vector2 from "./vector2"; ~~~~~~~ src/vector2.ts:1:1 1 export = class Vector2 { ~~~~~~~~~~~~~~~~~~~~~~~~ 2 constructor(public x: number, public y: number) {} ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ... 6 } ~~~ 7 } ~ This module is declared with using 'export =', and can only be used with a default import when using the 'esModuleInterop' flag. src/vectors.ts:2:8 - error TS1259: Module '"~/project/src/vector3"' can only be default-imported using the 'esModuleInterop' flag 2 import Vector3 from "./vector3"; ~~~~~~~ src/vector3.ts:1:1 1 export = class Vector3 { ~~~~~~~~~~~~~~~~~~~~~~~~ 2 constructor(public x: number, public y: number, public z: number) {} ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ... 10 } ~~~ 11 } ~ This module is declared with using 'export =', and can only be used with a default import when using the 'esModuleInterop' flag.
此错误是由于您在 src/vectors.ts
中导入矢量文件的方式造成的。 该文件仍在使用导入模块默认导出的语法,但您现在正在覆盖导出的对象,因此您不再有默认导出。 同时使用这两种语法是不兼容的。
有两种方法可以解决这个问题:使用 import = require()
并在 TypeScript 编译器配置文件中将 esModuleInterop
属性设置为 true
。
首先,您将通过对 src/vectors.ts
文件中的代码进行以下突出显示的更改来尝试导入此类模块的正确语法:
vector_project/src/vectors.ts
import Vector2 = require("./vector2"); import Vector3 = require("./vector3"); export { Vector2, Vector3 };
import = require()
语法使用 exports
对象作为导入本身的值,允许您使用每个向量类。 编译您的代码现在将按预期工作。
解决 TypeScript 错误 1259
的另一种方法是在 tsconfig.json
文件中将选项 compilerOptions.esModuleInterop
设置为 true
。 默认情况下,此值为 false
。 当它设置为 true
时,TypeScript 编译器将发出额外的 JavaScript 来检查导出的对象,以检测它是默认导出还是被覆盖的 exports
对象,然后相应地使用它。
这可以按预期工作,并允许您像以前一样将代码保留在 src/vectors.ts
中。 有关 esModuleInterop
如何工作以及对发出的 JavaScript 代码进行了哪些更改的更多信息,请查看 esModuleInterop 选项的 TypeScript 文档。
注意: 本节中所做的所有更改都假定您的目标是支持覆盖模块导出对象的模块系统。 目前,这些模块是 AMD 和 CommonJS。 如果你的目标是不同的模块系统,TypeScript 编译器会在编译期间给你一个错误。
例如,如果 compilerOptions.module
配置设置为 ES6
以针对 ES 模块系统,TypeScript 编译器会给我们多个错误,归结为只有两个重复错误,[ X204X] 和 1203
:
"Output"src/vector2.ts:1:1 - error TS1203: Export assignment cannot be used when targeting ECMAScript modules. Consider using 'export default' or another module format instead. 1 export = class Vector2 { ~~~~~~~~~~~~~~~~~~~~~~~~ 2 constructor(public x: number, public y: number) {} ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ... 6 } ~~~ 7 }; ~~ src/vectors.ts:1:1 - error TS1202: Import assignment cannot be used when targeting ECMAScript modules. Consider using 'import * as ns from "mod"', 'import {a} from "mod"', 'import d from "mod"', or another module format instead. 1 import Vector2 = require("./vector2");
要了解有关针对 ES 模块系统的更多信息,请查看 模块属性 的 TypeScript 编译器文档。
结论
TypeScript 提供了一个功能齐全的模块系统,其语法受 ES 模块规范启发,同时允许开发人员在发出的 JavaScript 代码中定位各种其他模块系统,如 CommonJS、AMD[X238X ]、UMD、SystemJS 和 ES6。 通过使用 TypeScript 中可用的 import
和 export
选项,您可以确保您的代码是模块化的并与更大的 JavaScript 环境兼容。 了解如何使用模块将使您能够简洁有效地组织应用程序代码,因为模块是拥有易于扩展和维护的结构良好的代码库的基本部分。
有关 TypeScript 的更多教程,请查看我们的 How To Code in TypeScript 系列页面。