# 模块

JavaScript 在处理模块化代码的不同方法方面有着悠久的历史。 TypeScript 自 2012 年问世以来,已经实现了对许多这些格式的支持,但随着时间的推移,社区和 JavaScript 规范已经融合到一种称为 ES 模块(或 ES6 模块)的格式上。您可能知道它是 import/export语法。

ES Modules 于 2015 年被添加到 JavaScript 规范中,到 2020 年在大多数 Web 浏览器和 JavaScript 运行时得到广泛支持。

作为重点,该手册将涵盖 ES 模块及其流行的前驱 CommonJS module.exports =语法,您可以在 模块 下的参考部分中找到有关其他模块模式的信息。

# JavaScript 模块是如何定义的

在 TypeScript 中,就像在 ECMAScript 2015 中一样,任何包含顶级 importexport的文件都被视为一个模块。

相反,没有任何顶级导入或导出声明的文件被视为其内容在全局作用域内可用的脚本(因此也可用于模块)。

模块在它们自己的作用域内执行,而不是在全局作用域内。 这意味着在模块中声明的变量、函数、类等在模块外部是不可见的,除非它们使用一种导出形式显式导出。 相反,要使用从不同模块导出的变量、函数、类、接口等,必须使用其中一种导入形式导入。

# 非模块

在开始之前,了解 TypeScript 将什么视为模块非常重要。 JavaScript 规范声明任何没有 export或顶级 await 的 JavaScript 文件都应被视为脚本而不是模块。

在脚本文件中,变量和类型被声明在共享全局作用域内,并且假设您将使用 outFile 编译器选项将多个输入文件连接到一个输出文件中,或者在 HTML 中使用多个 <script>标记来加载这些文件(按正确的顺序!)。

如果您有一个当前没有任何 importexport的文件,但您希望被视为一个模块,请添加以下行:

export {};

这会将文件更改为不导出任何内容的模块。无论您的模块目标如何,此语法都有效。

# TypeScript 中的模块

在 TypeScript 中编写基于模块的代码时,需要考虑三件事:

  • 句法:我想用什么语法来导入和导出东西?
  • 模块解析:模块名称(或路径)与磁盘上的文件有什么关系?
  • 模块输出目标:我发出的 JavaScript 模块应该是什么样子?

# # ES 模块语法

一个文件可以通过 export default声明一个主导出:

// @filename: hello.ts
export default function helloWorld() {
  console.log("Hello, world!");
}

然后通过以下方式导入:

// @filename: hello.ts
export default function helloWorld() {
  console.log("Hello, world!");
}
// @filename: index.ts

import helloWorld from "./hello.js";
helloWorld();

除了默认导出之外,您还可以通过省略 default通过 export导出多个变量和函数:

// @filename: maths.ts
export var pi = 3.14;
export let squareTwo = 1.41;
export const phi = 1.61;

export class RandomNumberGenerator {}

export function absolute(num: number) {
  if (num < 0) return num * -1;
  return num;
}

这些可以通过 import语法在另一个文件中使用:

// @filename: maths.ts
export var pi = 3.14;
export let squareTwo = 1.41;
export const phi = 1.61;
export class RandomNumberGenerator {}
export function absolute(num: number) {
  if (num < 0) return num * -1;
  return num;
}
// @filename: app.ts

import { pi, phi, absolute } from "./maths.js";

console.log(pi);
const absPhi = absolute(phi);
//    ^?

# # 附加导入语法

可以使用 import {old as new}之类的格式重命名导入:

// @filename: maths.ts
export var pi = 3.14;
// @filename: app.ts

import { pi as π } from "./maths.js";

console.log(π);
//          ^?

您可以将上述语法混合并匹配到单个 import中:

// @filename: maths.ts
export const pi = 3.14;
export default class RandomNumberGenerator {}

// @filename: app.ts
import RandomNumberGenerator, { pi as π } from "./maths.js";

RandomNumberGenerator;
// ^?

console.log(π);
//          ^?

您可以使用 * as name将所有导出的对象放入单个命名空间中:

// @filename: maths.ts
export var pi = 3.14;
export let squareTwo = 1.41;
export const phi = 1.61;

export function absolute(num: number) {
  if (num < 0) return num * -1;
  return num;
}

// @filename: app.ts
import * as math from "./maths.js";

console.log(math.pi);
const positivePhi = math.absolute(math.phi);
//    ^?

您可以通过 import "./file"导入文件,但不将任何变量包含到当前模块中:

// @filename: maths.ts
export var pi = 3.14;

// @filename: app.ts
import "./maths.js";

console.log("3.14");

在这种情况下,import什么也不做。但是,maths.ts中的所有代码都已评估,这可能会触发影响其他对象的副作用。

# ### TypeScript 特定的 ES 模块语法

可以使用与 JavaScript 值相同的语法导出和导入类型:

// @filename: animal.ts
export type Cat = { breed: string; yearOfBirth: number };

export interface Dog {
  breeds: string[];
  yearOfBirth: number;
}

// @filename: app.ts
import { Cat, Dog } from "./animal.js";
type Animals = Cat | Dog;

TypeScript 扩展了 import语法,其中包含两个用于声明类型导入的概念:

# ### # import type

这是一个只能导入类型的导入语句:

// @filename: animal.ts
export type Cat = { breed: string; yearOfBirth: number };
export type Dog = { breeds: string[]; yearOfBirth: number };
export const createCatName = () => "fluffy";

// @filename: valid.ts
import type { Cat, Dog } from "./animal.js";
export type Animals = Cat | Dog;

// @filename: app.ts
import type { createCatName } from "./animal.js";
const name = createCatName();

# ### # 内联 type 导入

TypeScript 4.5 还允许单个导入以 type为前缀,以指示导入的引用是一种类型:

// @filename: animal.ts
export type Cat = { breed: string; yearOfBirth: number };
export type Dog = { breeds: string[]; yearOfBirth: number };
export const createCatName = () => "fluffy";

// @filename: app.ts
import { createCatName, type Cat, type Dog } from "./animal.js";

export type Animals = Cat | Dog;
const name = createCatName();

这些一起允许像 Babel、swc 或 esbuild 这样的非 TypeScript 转译器知道可以安全地删除哪些导入。

# ### 具有 CommonJS 行为的 ES 模块语法

TypeScript 具有与 CommonJS 和 AMD require直接相关的 ES 模块语法。在大多数情况下,使用 ES 模块的导入与这些环境中的 require相同,但这种语法可确保您的 TypeScript 文件与 CommonJS 输出有 1 对 1 的匹配:

/// <reference types="node" />

import fs = require("fs");
const code = fs.readFileSync("hello.ts", "utf8");

您可以在 模块参考页面 中了解有关此语法的更多信息。

# CommonJS 语法

CommonJS 是 npm 上大多数模块的交付格式。即使您使用上面的 ES 模块语法进行编写,对 CommonJS 语法的工作原理有一个简要的了解也会帮助您更轻松地进行调试。

# # 导出

通过在名为 module的全局上设置 exports属性来导出标识符。

/// <reference types="node" />

function absolute(num: number) {
  if (num < 0) return num * -1;
  return num;
}

module.exports = {
  pi: 3.14,
  squareTwo: 1.41,
  phi: 1.61,
  absolute,
};

然后可以通过 require语句导入这些文件:

// @filename: maths.ts
/// <reference types="node" />
function absolute(num: number) {
  if (num < 0) return num * -1;
  return num;
}

module.exports = {
  pi: 3.14,
  squareTwo: 1.41,
  phi: 1.61,
  absolute,
};
// @filename: index.ts

const maths = require("maths");
maths.pi;
//    ^?

或者您可以使用 JavaScript 中的解构功能进行一些简化:

// @filename: maths.ts
/// <reference types="node" />
function absolute(num: number) {
  if (num < 0) return num * -1;
  return num;
}

module.exports = {
  pi: 3.14,
  squareTwo: 1.41,
  phi: 1.61,
  absolute,
};
// @filename: index.ts

const { squareTwo } = require("maths");
squareTwo;
// ^?

# # CommonJS 和 ES 模块互操作

关于默认导入和模块命名空间对象导入之间的区别,CommonJS 和 ES 模块之间的功能不匹配。TypeScript 有一个编译器标志来减少与 esModuleInterop 的两组不同约束之间的摩擦。

# TypeScript 的模块解析选项

模块解析是从 importrequire语句中获取字符串并确定该字符串所指的文件的过程。

TypeScript 包含两种解析策略:经典和 Node。Classic 是编译器选项 module 不是 commonjs时的默认值,包含在内以实现向后兼容性。 Node 策略复制了 Node.js 在 CommonJS 模式下的工作方式,并额外检查了 .ts.d.ts

有许多 TSConfig 标志会影响 TypeScript 中的模块策略:moduleResolution、baseUrl、paths、rootDirs。

有关这些策略如何运作的完整详细信息,您可以查阅 模块解析

# TypeScript 的模块输出选项

有两个选项会影响发出的 JavaScript 输出:

  • target 确定哪些 JS 功能被降级(转换为在较旧的 JavaScript 运行时中运行),哪些保持不变
  • module 确定模块之间使用什么代码进行交互

您使用哪个 target 取决于您希望在其中运行 TypeScript 代码的 JavaScript 运行时中可用的功能。这可能是:您支持的最古老的 Web 浏览器,您希望在其上运行的 Node.js 的最低版本,或者可能来自您运行时的独特约束——例如 Electron。

模块之间的所有通信都是通过模块加载器进行的,编译器选项 module 确定使用哪一个。 在运行时,模块加载器负责在执行模块之前定位和执行模块的所有依赖项。

例如,这是一个使用 ES Modules 语法的 TypeScript 文件,展示了 module 的几个不同选项:

// @filename: constants.ts
export const valueOfPi = 3.142;
// @filename: index.ts

import { valueOfPi } from "./constants.js";

export const twoPi = valueOfPi * 2;

# # ES2020

import { valueOfPi } from "./constants.js";

export const twoPi = valueOfPi * 2;

# # CommonJS

import { valueOfPi } from "./constants.js";

export const twoPi = valueOfPi * 2;

# # UMD

import { valueOfPi } from "./constants.js";

export const twoPi = valueOfPi * 2;

请注意,ES2020 实际上与原始 index.ts相同。

您可以看到所有可用的选项以及它们发出的 JavaScript 代码在 module 的 TSConfig 参考手册 中的样子。

# TypeScript 命名空间

TypeScript 有自己的模块格式,称为 namespaces,它早于 ES 模块标准。这种语法对于创建复杂的定义文件有很多有用的特性,并且仍然被积极使用 在绝对类型 。虽然没有被弃用,但命名空间中的大部分功能都存在于 ES 模块中,我们建议您使用它来与 JavaScript 的方向保持一致。您可以在 命名空间参考页面 中了解有关命名空间的更多信息。

Last Updated: 5/25/2023, 2:35:11 PM