如何在 TypeScript 中使用模块

作者选择了 COVID-19 救援基金作为 Write for Donations计划的一部分接受捐款。

介绍

Modules 是将您的代码组织成更小的,更易于管理的部件的一种方法,允许程序从应用程序的不同部分导入代码。多年来,有几种策略实现模块化到 JavaScript的代码中。 TypeScriptECMAScript的规格一起演变,为JavaScript程序提供了一个标准模块系统,它具有与这些不同格式工作的灵活性。 TypeScript 提供了与 ES 模块语法相似的统一语法的模块创建和使用的支持,同时允许开发人员输出针对不同模块加载器的代码,如 Nodejs(JavaScript要求。 (js[

在本教程中,您将创建和使用TypeScript中的模块,您将在自己的TypeScript环境中遵循不同的代码样本,展示如何使用导入导出关键字,如何设置默认导出,以及如何使超写导出对象的文件与您的代码兼容。

前提条件

要遵循本教程,您将需要:

  • 联合国 您可以执行 TypeScript 程序的环境与示例一起执行 。 要在您的本地机上设置此功能, 您需要安装如下:
  • 既 [Node] (https://nodejs.org/about/) (又 [npm] (https://docs.npmjs.com/about-npm (或 [yarn] (https://yarnpkg.com/getting-started )) , 以便运行一个处理 TypeScript相关包的开发环境. 这个教程用Node.js版本14.3.0和npm版本6.14.5进行了测试. 要安装在 macOS 或 Ubuntu 18.04 上,请遵循 [如何在 macOS (https://andsky.com/tech/tutorials/how-to-install-node-js-and-create-a-local-development-environment-on-macos) 上安装节点并创建本地开发环境 或 ** 使用 [如何在 Ubuntu 18.04 (https://andsky.com/tech/tutorials/how-to-install-node-js-on-ubuntu-18-04 上安装节点.js 的PPA** 部分。 如果您正在使用 Linux (WSL) (https://andsky.com/tech/tutorials/how-to-install-the-windows-subsystem-for-linux-2-on-microsoft-windows-10) 的 Windows 子系统,此功能也会有效 。
  • 您需要足够的JavaScript知识,特别是ES6+语法,例如解构,休息操作员,以及进出口. 如果您需要更多有关这些主题的信息,请读取我们的[JavaScript series How To Code (https://www.digitalocean.com/community/tutorial_series/how-to-code-in-javascript).
  • 此教程会引用文本编辑器中支持TypeScript并显示行内错误的方面. 这对使用TypeScript是不必要的,但确实更多地利用了TypeScript的特性. 为了获得这些好处,您可以使用像[Visual Studio Code (https://code.visualstudio.com/)这样的文本编辑器,该编辑器对出框的TypeScript有全力支持. (英语)

本教程中的所有示例都是使用TypeScript版本 4.2.2创建的。

创建项目

在此步骤中,您将创建一个样本项目,其中包含两个小类用于处理矢量操作: Vector2Vector3. A vector 在这种情况下,指的是大小和距离的数学测量,通常用于视觉图形程序。

您构建的类将对每个类进行单个操作:矢量添加. 随后,您将使用这些样本类来测试从一个程序导入和导出代码。

首先,为自己创建一个目录,该目录将容纳你的样本代码:

1mkdir vector_project

一旦目录被创建,使其成为您的工作目录:

1cd vector_project

现在你已经在项目的根源,创建你的Node.js应用程序与npm:

1npm init

这将为您的项目创建一个package.json文件。

接下来,添加 TypeScript 作为开发依赖:

1npm install [email protected] --save-dev

这将为您的项目安装TypeScript,TypeScript 编译器将设置为默认设置. 要创建自己的自定义设置,您需要创建一个特定的配置文件。

创建和打开名为 tsconfig.json的文件在你的项目的根部. 要让你的项目与本教程中的练习工作,添加以下内容到文件:

 1[label tsconfig.json]
 2{
 3  "compilerOptions": {
 4    "target": "ES6",
 5    "module": "CommonJS",
 6    "outDir": "./out",
 7    "rootDir": "./src",
 8    "strict": true
 9  }
10}

在此代码中,您正在为 TypeScript 编译器设置多个配置。 目标`: `ES6` 决定了您的代码将被编译的环境,而 outDir: ./outrootDir: ./src 指定了哪个目录将分别保留您的编译器的输出和输入。 ``strict: true 设置了一个强大的类型检查水平。 最后, ``模块: CommonJS 将模块系统指定为 `CommonJS’。

随着项目的设置,您现在可以继续使用基本语法创建模块。

使用导出创建 TypeScript 模块

在本节中,您将使用TypeScript模块语法创建TypeScript中的模块。

默认情况下,TypeScript 中的文件被视为全球脚本,这意味着在文件中声明的任何 变量, , 函数或其他构件都可以在全球范围内使用。

要在行动中展示这一点,你将创建你的第一个类: Vector2. 在项目的根部创建一个名为 src/的新目录:

1mkdir src

这是您在您的 tsconfig.json 文件中设置为 root 目录的目录。

在此文件夹中,创建一个名为 vector2.ts 的新文件. 在你最喜欢的文本编辑器中打开这个文件,然后写入你的 Vector2 类:

1[label vector_project/src/vector2.ts]
2class Vector2 {
3  constructor(public x: number, public y: number) {}
4
5  add(otherVector2: Vector2) {
6    return new Vector2(this.x + otherVector2.x, this.y + otherVector2.y);
7  }
8}

在此代码中,您声明一个名为Vector2的类,该类是通过将两个数字作为参数来创建的,设置为属性xy

由于你的文件目前没有使用模块,所以你的‘Vector2’是全球范围的。 要将你的文件变成一个模块,你只需要导出你的‘Vector2’类:

1[label vector_project/src/vector2.ts]
2export class Vector2 {
3  constructor(public x: number, public y: number) {}
4
5  add(otherVector2: Vector2) {
6    return new Vector2(this.x + otherVector2.x, this.y + otherVector2.y);
7  }
8}

文件 src/vector2.ts 现在是一个模块,有一个单一的出口: Vector2 类. 保存并关闭您的文件。

然后,你可以创建你的Vector3类,在src/目录中创建vector3.ts文件,然后在你最喜欢的文本编辑器中打开该文件,然后写下以下代码:

 1[label vector_project/src/vector3.ts]
 2export class Vector3 {
 3  constructor(public x: number, public y: number, public z: number) {}
 4
 5  add(otherVector3: Vector3) {
 6    return new Vector3(
 7      this.x + otherVector3.x,
 8      this.y + otherVector3.y,
 9      this.z + otherVector3.z
10    );
11  }
12}

此代码创建了一个类似于‘Vector2’的类别,但具有一个额外的‘z’属性,该属性将矢量存储在三个维度中。

现在你有两个文件,‘vector2.ts’和‘vector3.ts’,这两个都是模块. 每个文件都有一个单一的导出,这是他们所代表的矢量类别。

在 TypeScript 中使用导入的模块

在上一节中,您看到如何创建模块,在本节中,您将导入这些模块,以便在代码中的其他地方使用。

在 TypeScript 中使用模块时,一个常见的场景是有一个单个文件,该文件收集多个模块,并将其重新导出为一个模块. 为了证明这一点,在你的src/目录中创建一个名为vectors.ts的文件,然后在你最喜欢的编辑器中打开这个文件,然后写下以下内容:

1[label vector_project/src/vectors.ts]
2import { Vector2 } from "./vector2";
3import { Vector3 } from "./vector3";

要导入您项目中可用的另一个模块,您可以使用在导入语句中对该文件的相对路径. 在这种情况下,您正在导入来自./vector2./vector3的两个模块,这些都是当前文件的相对路径到src/vector2.tssrc/vector3.ts的文件。

现在您已经导入了矢量,您可以将它们重新导出到一个单个模块中,使用以下突出语法:

1[label vector_project/src/vectors.ts]
2import { Vector2 } from "./vector2";
3import { Vector3 } from "./vector3";
4
5export { Vector2, Vector3 };

在这种情况下,您正在使用单个出口声明导出Vector2Vector3类。

您还可以使用两个单独的出口陈述,如下:

1[label vector_project/src/vectors.ts]
2import { Vector2 } from "./vector2";
3import { Vector3 } from "./vector3";
4
5export { Vector2 };
6export { Vector3 };

这与以前的代码片段具有相同的含义。

由于src/vectors.ts只导入两个类,然后再导出它们,您可以使用更简短的语法形式:

1[label vector_project/src/vectors.ts]
2export { Vector2 } from "./vector2";
3export { Vector3 } from "./vector3";

这里的导入声明是默认的,TypeScript 编译器会自动包含它,然后将立即以相同的名称导出文件。

现在,您正在从src/vectors.ts文件中导出您的两个矢量类,创建一个名为src/index.ts的新文件,然后打开该文件并写下列代码:

 1[label vector_project/src/index.ts]
 2import { Vector2, Vector3 } from "./vectors";
 3
 4const vec2a = new Vector2(1, 2);
 5const vec2b = new Vector2(2, 1);
 6
 7console.log(vec2a.add(vec2b));
 8
 9const vec3a = new Vector3(1, 2, 3);
10const vec3b = new Vector3(3, 2, 1);
11
12console.log(vec3a.add(vec3b));

在此代码中,您正在从使用相对路径 ./vectorssrc/vectors.ts 文件中导入两个矢量类,然后使用 add 方法创建几个矢量实例,将它们添加在一起,然后记录结果。

在导入命名导出时,您还可以使用不同的名称,这有助于避免文件内部的命名碰撞。

 1[label vector_project/src/index.ts]
 2import { Vector2 as Vec2, Vector3 as Vec3 } from "./vectors";
 3
 4const vec2a = new Vec2(1, 2);
 5const vec2b = new Vec2(2, 1);
 6
 7console.log(vec2a.add(vec2b));
 8
 9const vec3a = new Vec3(1, 2, 3);
10const vec3b = new Vec3(3, 2, 1);
11
12console.log(vec3a.add(vec3b));

在这里,您正在使用as关键字来为导入的类设置名称Vec2Vec3

注意您如何导入 `./vector 中所有可用的内容. 此文件只导出这两个类,因此您可以使用以下语法将所有内容导入到单个变量中:

 1[label vector_project/src/index.ts]
 2import * as vectors from "./vectors";
 3
 4const vec2a = new vectors.Vector2(1, 2);
 5const vec2b = new vectors.Vector2(2, 1);
 6
 7console.log(vec2a.add(vec2b));
 8
 9const vec3a = new vectors.Vector3(1, 2, 3);
10const vec3b = new vectors.Vector3(3, 2, 1);
11
12console.log(vec3a.add(vec3b));

在上面提到的代码中,您使用导入 * 作为语法将模块导出的一切导入到单个变量中,您还必须改变您使用的Vector2Vector3类的方式,因为它们现在可以在导入过程中创建的矢量对象中可用。

如果您保存文件并使用 tsc 编译项目:

1npx tsc

TypeScript 编译器将创建out/目录(考虑到您在tsconfig.json文件中设置的compileOptions.outDir选项),然后将目录填充到 JavaScript 文件中。

在您最喜欢的文本编辑器中打开可用的out/index.js编译文件,它将看起来像这样:

 1[label vector_project/out/index.js]
 2"use strict";
 3Object.defineProperty(exports, "__esModule", { value: true });
 4const vectors = require("./vectors");
 5const vec2a = new vectors.Vector2(1, 2);
 6const vec2b = new vectors.Vector2(2, 1);
 7console.log(vec2a.add(vec2b));
 8const vec3a = new vectors.Vector3(1, 2, 3);
 9const vec3b = new vectors.Vector3(3, 2, 1);
10console.log(vec3a.add(vec3b));

由于您的tsconfig.json 文件中的compilerOptions.module选项设置为CommonJS,TypeScript 编译器会创建与 Node.js 模块系统兼容的代码。

接下来,看看已编译的 src/vectors.ts 文件,该文件在 `out/vectors.js 中可用:

1[label vector_project/out/vectors.js]
2"use strict";
3Object.defineProperty(exports, "__esModule", { value: true });
4exports.Vector3 = exports.Vector2 = void 0;
5var vector2_1 = require("./vector2");
6Object.defineProperty(exports, "Vector2", { enumerable: true, get: function () { return vector2_1.Vector2; } });
7var vector3_1 = require("./vector3");
8Object.defineProperty(exports, "Vector3", { enumerable: true, get: function () { return vector3_1.Vector3; } });

在这里,TypeScript 编译器创建了与使用 CommonJS 导出模块的方式兼容的代码,该代码将导出值分配给导出对象。

现在你已经尝试了导入和导出文件的语法,然后看到它们是如何编译到JavaScript的,你可以继续在你的文件中声明默认导出。

使用默认出口

在本节中,您将探讨从模块导出一个称为默认导出的值的另一种方法,该方法将特定导出设置为从模块导入的假设导入。

再次打开src/vector2.ts文件:

1[label vector_project/src/vector2.ts]
2export class Vector2 {
3  constructor(public x: number, public y: number) {}
4
5  add(otherVector2: Vector2) {
6    return new Vector2(this.x + otherVector2.x, this.y + otherVector2.y);
7  }
8}

注意您是如何从文件/模块中导出单个值的。您可以通过使用默认导出来写出导出的另一种方式。

若要将导出更改为默认导出,请添加以下突出代码:

1[label vector_project/src/vector2.ts]
2export default class Vector2 {
3  constructor(public x: number, public y: number) {}
4
5  add(otherVector2: Vector2) {
6    return new Vector2(this.x + otherVector2.x, this.y + otherVector2.y);
7  }
8}

保存文件,然后在src/vector3.ts文件中做同样的事情:

 1[label vector_project/src/vector3.ts]
 2export default class Vector3 {
 3  constructor(public x: number, public y: number, public z: number) {}
 4
 5  add(otherVector3: Vector3) {
 6    return new Vector3(
 7      this.x + otherVector3.x,
 8      this.y + otherVector3.y,
 9      this.z + otherVector3.z
10    );
11  }
12}

要导入默认导出,保存您的文件,然后打开src/vectors.ts文件,并将其内容更改为以下:

1[label vector_project/src/vectors.ts]
2import Vector2 from "./vector2";
3import Vector3 from "./vector3";
4
5export { Vector2, Vector3 };

请注意,对于两个导入,你只是给你的导入一个名称,而不是用破坏来导入一个特定的值。

每个具有默认导出的模块都有一个名为默认导出的特殊导出,可用于访问默认导出值。

1[label vector_project/src/vectors.ts]
2export { default as Vector2 } from "./vector2";
3export { default as Vector3 } from "./vector3";

现在您正在以特定名称重新导出每个模块的默认导出。

使用「輸出 = 」和「輸入 = 要求()」為兼容性

一些模块加载器,如AMD和CommonJS,有一个被称为出口的对象,其中包含一个模块导出的所有值。当使用这些模块加载器时,可以通过改变导出对象的值来重写导出的对象。这类似于ES模块中的默认导出,因此也包含TypeScript本身。然而,这两种语法是不可兼容的。 在本节中,您将看看TypeScript如何以与默认导出兼容的方式处理这种行为。

在TypeScript中,当针对支持重写导出对象的模块加载器时,您可以使用导出 =语法更改导出对象的值。

<$>[注] 注: 请确保在您的 tsconfig.json 文件中的 compilerOptions.module 选项设置为 CommonJS,然后进行下列任何更改。

想象一下,你想在每个矢量文件中更改导出对象,以指向矢量类本身。

1[label vector_project/src/vector2.ts]
2export = class Vector2 {
3  constructor(public x: number, public y: number) {}
4
5  add(otherVector2: Vector2) {
6    return new Vector2(this.x + otherVector2.x, this.y + otherVector2.y);
7  }
8};

保存这些,然后对src/vector3.ts文件做同样的事情:

 1[label vector_project/src/vector3.ts]
 2export = class Vector3 {
 3  constructor(public x: number, public y: number, public z: number) {}
 4
 5  add(otherVector3: Vector3) {
 6    return new Vector3(
 7      this.x + otherVector3.x,
 8      this.y + otherVector3.y,
 9      this.z + otherVector3.z
10    );
11  }
12};

最后,将您的vectors.ts更改为以下:

1[label vector_project/src/vectors.ts]
2import Vector2 from "./vector2";
3import Vector3 from "./vector3";
4
5export { Vector2, Vector3 };

保存这些文件,然后运行 TypeScript 编译器:

1npx tsc

TypeScript 编译器会为您提供多个错误,包括以下:

 1[secondary_label Output]
 2src/vectors.ts:1:8 - error TS1259: Module '"~/project/src/vector2"' can only be default-imported using the 'esModuleInterop' flag
 3
 41 import Vector2 from "./vector2";
 5         ~~~~~~~
 6
 7  src/vector2.ts:1:1
 8      1 export = class Vector2 {
 9        ~~~~~~~~~~~~~~~~~~~~~~~~
10      2 constructor(public x: number, public y: number) {}
11        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
12    ... 
13      6   }
14        ~~~
15      7 }
16        ~
17    This module is declared with using 'export =', and can only be used with a default import when using the 'esModuleInterop' flag.
18
19src/vectors.ts:2:8 - error TS1259: Module '"~/project/src/vector3"' can only be default-imported using the 'esModuleInterop' flag
20
212 import Vector3 from "./vector3";
22         ~~~~~~~
23
24  src/vector3.ts:1:1
25      1 export = class Vector3 {
26        ~~~~~~~~~~~~~~~~~~~~~~~~
27      2 constructor(public x: number, public y: number, public z: number) {}
28        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
29    ... 
30     10   }
31        ~~~
32     11 }
33        ~
34    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文件中对您的代码做出以下突出更改:

1[label vector_project/src/vectors.ts]
2import Vector2 = require("./vector2");
3import Vector3 = require("./vector3");
4
5export { Vector2, Vector3 };

「import = require()」语法使用「export」对象作为导入本身的值,允许您使用每个矢量类。

解决 TypeScript 错误 1259' 的另一种方法是将选项 compilerOptions.esModuleInterop设置为true' 在 tsconfig.json' 文件中. 默认情况下,此值为 false'。 当此值设置为 true' 时,TypeScript 编译器会发出额外的 JavaScript 来检查被导出对象,以检测它是否是默认的导出或被写过的 exports' 对象,然后相应地使用它。

它按预期运行,并允许您将代码保存在src/vectors.ts内部,如前所述。 有关esModuleInterop如何工作以及对发行的JavaScript代码进行哪些更改的更多信息,请参阅esModuleInterop的TypeScript文档(https://www.typescriptlang.org/tsconfig#esModuleInterop)。

<$>[注] **注:**本节所做的所有更改假定您正在瞄准一个支持超写模块导出对象的模块系统。

例如,如果compilerOptions.module配置被设置为ES6,以针对ES模块系统,TypeScript 编译器会给我们多个错误,这只会导致两个重复的错误,即12021203:

 1[secondary_label "Output"]
 2src/vector2.ts:1:1 - error TS1203: Export assignment cannot be used when targeting ECMAScript modules. Consider using 'export default' or another module format instead.
 3
 4  1 export = class Vector2 {
 5    ~~~~~~~~~~~~~~~~~~~~~~~~
 6  2 constructor(public x: number, public y: number) {}
 7    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 8...
 9  6   }
10    ~~~
11  7 };
12    ~~
13
14src/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.
15
161 import Vector2 = require("./vector2");

要了解更多有关针对 ES 模块系统的信息,请参阅 TypeScript 编译器文档为模块属性

结论

TypeScript 提供了由 ES 模块规格启发的全功能模块系统,同时允许开发人员在发行的 JavaScript 代码中瞄准各种其他模块系统,如 CommonJS, AMD, UMD, SystemJS,和 ES6

有关TypeScript的更多教程,请参阅我们的 How To Code in TypeScript 系列页面

Published At
Categories with 技术
comments powered by Disqus