Modules

模块(Modules)

关于术语的说明:重要的是要注意,在 TypeScript 1.5 中,命名已经改变。“内部模块”现在是“命名空间”。“外部模块”现在只是“模块”,与 ECMAScr ipt 2015的术语一致(即module X {相当于现在的首选namespace X {)。

介绍

从 ECMAScript 2015开始,JavaScript 具有模块的概念。TypeScript 分享这个概念。

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

模块是声明性的;模块之间的关系根据文件级别的导入和导出来指定。

模块使用模块加载器相互导入。在运行时,模块加载器负责在执行之前查找并执行模块的所有依赖关系。在 JavaScript 中使用的著名模块加载器是用于 Node.js 的 CommonJS 模块加载器和用于 Web 应用程序的 require.js。

在 TypeScript 中,与在 ECMAScript 2015 中一样,任何包含顶级import或被export视为模块的文件。

出口

导出声明

任何声明(例如变量,函数,类,类型别名或接口)都可以通过添加export关键字来导出。

Validation.ts

export interface StringValidator { isAcceptable(s: string): boolean; }

ZipCodeValidator.ts

export const numberRegexp = /^[0-9]+$/; export class ZipCodeValidator implements StringValidator { isAcceptable(s: string) { return s.length === 5 && numberRegexp.test(s } }

导出语句

当需要为消费者重新命名导出时,导出语句非常方便,因此上面的示例可以写为:

class ZipCodeValidator implements StringValidator { isAcceptable(s: string) { return s.length === 5 && numberRegexp.test(s } } export { ZipCodeValidator }; export { ZipCodeValidator as mainValidator };

再出口

通常模块扩展了其他模块,并部分暴露了一些功能。重新导出不会在本地导入它,也不会引入局部变量。

ParseIntBasedZipCodeValidator.ts

export class ParseIntBasedZipCodeValidator { isAcceptable(s: string) { return s.length === 5 && parseInt(s).toString() === s; } } // Export original validator but rename it export {ZipCodeValidator as RegExpBasedZipCodeValidator} from "./ZipCodeValidator";

可选地,模块可以包装一个或多个模块并使用export * from "module"语法组合它们的所有输出。

AllValidators.ts

export * from "./StringValidator"; // exports interface 'StringValidator' export * from "./LettersOnlyValidator"; // exports class 'LettersOnlyValidator' export * from "./ZipCodeValidator"; // exports class 'ZipCodeValidator'

进口

导入与从模块导出一样简单。导入一个导出的声明是通过使用import下面的一种形式完成的:

从模块导入单个导出

import { ZipCodeValidator } from "./ZipCodeValidator"; let myValidator = new ZipCodeValidator(

导入也可以重命名

import { ZipCodeValidator as ZCV } from "./ZipCodeValidator"; let myValidator = new ZCV(

将整个模块导入一个变量,并使用它访问模块导出

import * as validator from "./ZipCodeValidator"; let myValidator = new validator.ZipCodeValidator(

仅为副作用导入模块

虽然不推荐实践,但某些模块设置了一些可供其他模块使用的全局状态。这些模块可能没有任何出口,或者消费者对其任何出口不感兴趣。要导入这些模块,请使用:

import "./my-module.js";

默认出口

每个模块可以选择导出一个default导出。默认出口标有关键字default; default每个模块只能有一个导出。default导出使用不同的导入格式导入。

default出口非常方便。例如,像 JQuery 这样的库可能有一个的默认导出jQuery或者$,我们也可能会在这个名称$或导入下导出jQuery

JQuery.d.ts

declare let $: JQuery; export default $;

App.ts

import $ from "JQuery"; $("button.continue").html( "Next Step..."

类和函数声明可以作为默认导出直接创作。默认导出类和函数声明名称是可选的。

ZipCodeValidator.ts

export default class ZipCodeValidator { static numberRegexp = /^[0-9]+$/; isAcceptable(s: string) { return s.length === 5 && ZipCodeValidator.numberRegexp.test(s } }

Test.ts

import validator from "./ZipCodeValidator"; let myValidator = new validator(

or

StaticZipCodeValidator.ts

const numberRegexp = /^[0-9]+$/; export default function (s: string) { return s.length === 5 && numberRegexp.test(s }

Test.ts

import validate from "./StaticZipCodeValidator"; let strings = ["Hello", "98052", "101"]; // Use function validate strings.forEach(s => { console.log(`"${s}" ${validate(s) ? " matches" : " does not match"}` }

default 输出也可以是价值:

OneTwoThree.ts

export default "123";

Log.ts

import num from "./OneTwoThree"; console.log(num // "123"

export = 和 import = require()

CommonJS 和 AMD 都有一个exports对象的概念,它包含了一个模块的所有输出。

他们还支持用exports一个自定义单个对象替换对象。默认导出是为了替代这种行为; 然而,这两者是不相容的。TypeScript 支持export =对传统的 CommonJS 和 AMD 工作流进行建模。

export =语法指定从模块中导出的单个对象。这可以是类,接口,名称空间,函数或枚举。

在使用模块导入模块时export =import module = require("module")必须使用特定于 TypeScript 的模块导入模块。

ZipCodeValidator.ts

let numberRegexp = /^[0-9]+$/; class ZipCodeValidator { isAcceptable(s: string) { return s.length === 5 && numberRegexp.test(s } } export = ZipCodeValidator;

Test.ts

import zip = require("./ZipCodeValidator" // Some samples to try let strings = ["Hello", "98052", "101"]; // Validators to use let validator = new zip( // Show whether each string passed each validator strings.forEach(s => { console.log(`"${ s }" - ${ validator.isAcceptable(s) ? "matches" : "does not match" }` }

代码生成模块

根据编译期间指定的模块目标,编译器将为 Node.js(CommonJS),require.js(AMD),同构(UMD),SystemJS 或 ECMAScript 2015 本机模块(ES6)模块加载系统生成适当的代码。关于什么的更多信息definerequireregister在生成的代码调用做,请参见各模块加载的文档。

这个简单的例子展示了在导入和导出过程中使用的名称如何转换为模块加载代码。

SimpleModule.ts

import m = require("mod" export let t = m.something + 1;

AMD / RequireJS SimpleModule.js

define(["require", "exports", "./mod"], function (require, exports, mod_1) { exports.t = mod_1.something + 1; }

CommonJS / Node SimpleModule.js

var mod_1 = require("./mod" exports.t = mod_1.something + 1;

UMD SimpleModule.js

(function (factory) { if (typeof module === "object" && typeof module.exports === "object") { var v = factory(require, exports if (v !== undefined) module.exports = v; } else if (typeof define === "function" && define.amd) { define(["require", "exports", "./mod"], factory } })(function (require, exports) { var mod_1 = require("./mod" exports.t = mod_1.something + 1; }

System SimpleModule.js

System.register(["./mod"], function(exports_1) { var mod_1; var t; return { setters:[ function (mod_1_1) { mod_1 = mod_1_1; }], execute: function() { exports_1("t", t = mod_1.something + 1 } } }

Native ECMAScript 2015 modules SimpleModule.js

import { something } from "./mod"; export var t = something + 1;

简单的例子

下面,我们已经整合了前面示例中使用的 Validator 实现,以仅从每个模块中导出一个命名导出。

为了编译,我们必须在命令行上指定一个模块目标。对于 Node.js,使用--module commonjs; 对于 require.js,请使用--module amd。例如:

tsc --module commonjs Test.ts

编译时,每个模块将成为一个单独的.js文件。与引用标签一样,编译器将按照import语句来编译相关文件。

Validation.ts

export interface StringValidator { isAcceptable(s: string): boolean; }

LettersOnlyValidator.ts

import { StringValidator } from "./Validation"; const lettersRegexp = /^[A-Za-z]+$/; export class LettersOnlyValidator implements StringValidator { isAcceptable(s: string) { return lettersRegexp.test(s } }

ZipCodeValidator.ts

import { StringValidator } from "./Validation"; const numberRegexp = /^[0-9]+$/; export class ZipCodeValidator implements StringValidator { isAcceptable(s: string) { return s.length === 5 && numberRegexp.test(s } }

Test.ts

import { StringValidator } from "./Validation"; import { ZipCodeValidator } from "./ZipCodeValidator"; import { LettersOnlyValidator } from "./LettersOnlyValidator"; // Some samples to try let strings = ["Hello", "98052", "101"]; // Validators to use let validators: { [s: string]: StringValidator; } = {}; validators["ZIP code"] = new ZipCodeValidator( validators["Letters only"] = new LettersOnlyValidator( // Show whether each string passed each validator strings.forEach(s => { for (let name in validators) { console.log(`"${ s }" - ${ validators[name].isAcceptable(s) ? "matches" : "does not match" } ${ name }` } }

可选模块加载和其他高级加载方案

在某些情况下,您可能只想在某些条件下加载模块。在 TypeScript 中,我们可以使用下面显示的模式来实现这个和其他高级加载场景,以直接调用模块加载器而不会丢失类型安全性。

编译器检测每个模块是否在发射的 JavaScript 中使用。如果模块标识符只被用作类型注释的一部分,而不是表达式,则不会require为该模块发出调用。未使用引用的这种省略是一个很好的性能优化,并且还允许可选地加载这些模块。

该模式的核心思想是该import id = require("...")语句使我们可以访问模块公开的类型。模块加载器被require动态调用(通过),如if下面的块所示。这利用了 reference-elision 优化,以便只在需要时加载模块。为了使这种模式起作用,重要的是通过一个定义的符号import仅用于类型位置(即永远不会放在 JavaScript 中)。

为了保持类型安全,我们可以使用typeof关键字。typeof关键字,在一个类型的位置使用时,产生一个值的类型,在这种情况下,模块的类型。

Dynamic Module Loading in Node.js

declare function require(moduleName: string): any; import { ZipCodeValidator as Zip } from "./ZipCodeValidator"; if (needZipValidation) { let ZipCodeValidator: typeof Zip = require("./ZipCodeValidator" let validator = new ZipCodeValidator( if (validator.isAcceptable("...")) { /* ... */ } }

Sample: Dynamic Module Loading in require.js

declare function require(moduleNames: string[], onLoad: (...args: any[]) => void): void; import * as Zip from "./ZipCodeValidator"; if (needZipValidation) { require(["./ZipCodeValidator"], (ZipCodeValidator: typeof Zip) => { let validator = new ZipCodeValidator.ZipCodeValidator( if (validator.isAcceptable("...")) { /* ... */ } } }

Sample: Dynamic Module Loading in System.js

declare const System: any; import { ZipCodeValidator as Zip } from "./ZipCodeValidator"; if (needZipValidation) { System.import("./ZipCodeValidator").then((ZipCodeValidator: typeof Zip) => { var x = new ZipCodeValidator( if (x.isAcceptable("...")) { /* ... */ } } }

使用其他 JavaScript 库

为了描述不是用 TypeScript 编写的库的形状,我们需要声明库公开的 API。

我们称这些声明没有定义“环境”实现。通常,这些是在.d.ts文件中定义的。如果您熟悉 C / C ++,则可以将它们视为.h文件。我们来看几个例子。

环境模块

在 Node.js 中,大多数任务都是通过加载一个或多个模块来完成的。我们可以在每个模块的.d.ts顶层导出声明中定义它们自己的文件,但将它们作为一个更大的.d.ts文件编写起来会更方便。为此,我们使用类似于环境名称空间的构造,但是我们使用module模块的关键字和引用名称,以供以后导入时使用。例如:

node.d.ts (simplified excerpt)

declare module "url" { export interface Url { protocol?: string; hostname?: string; pathname?: string; } export function parse(urlStr: string, parseQueryString?, slashesDenoteHost?): Url; } declare module "path" { export function normalize(p: string): string; export function join(...paths: any[]): string; export var sep: string; }

现在我们可以/// <reference> node.d.ts或加载模块使用import * as URL from "url"或者import url = require("url"。

/// <reference path="node.d.ts"/> import * as URL from "url"; let myUrl = URL.parse("http://www.typescriptlang.org"

速记环境模块

如果您在使用新模块之前不想花时间写出声明,则可以使用速记声明快速开始。

declarations.d.ts

declare module "hot-new-module";

从速记模块导入的所有文件都将具有any类型。

import x, {y} from "hot-new-module"; x(y

通配符模块声明

一些模块加载器如 SystemJS 和 AMD 允许导入非 JavaScript 内容。这些通常使用前缀或后缀来指示特殊的加载语义。通配符模块声明可用于涵盖这些情况。

declare module "*!text" { const content: string; export default content; } // Some do it the other way around. declare module "json!*" { const value: any; export default value; }

现在你可以导入匹配"*!text"或匹配的东西"json!*"

import fileContent from "./xyz.txt!text"; import data from "json!http://example.com/data.json"; console.log(data, fileContent

UMD 模块

一些库被设计用于许多模块加载器,或者没有模块加载(全局变量)。这些被称为 UMD 或同构模块。这些库可以通过导入或全局变量来访问。例如:

math-lib.d.ts

export const isPrime(x: number): boolean; export as namespace mathLib;

该库然后可以用作模块内的导入:

import { isPrime } from "math-lib"; isPrime(2 mathLib.isPrime(2 // ERROR: can't use the global definition from inside a module

它也可以用作全局变量,但只能在脚本中使用。(脚本是一个没有导入或导出的文件。)

mathLib.isPrime(2

构建模块的指导

尽可能靠近顶层进行导出

使用您输出的东西时,您的模块的消费者应尽可能少摩擦。添加太多的嵌套层次往往很麻烦,所以仔细想想你想如何构造东西。

从模块中导出命名空间是添加太多嵌套层的例子。虽然命名空间有时候会用到它们,但它们在使用模块时会增加额外的间接级别。这可能很快成为用户的痛点,通常是不必要的。

导出类的静态方法也有类似的问题 - 类本身添加了一层嵌套。除非以明显有用的方式增加表达能力或意图,否则可以考虑简单地导出辅助函数。

如果您只导出单个class或function使用export default

正如“靠近顶层出口”一样,可以减少模块用户的摩擦,因此不会引入默认导出。如果模块的主要目的是容纳一个特定的导出,那么您应该考虑将其导出为默认导出。这使得导入和实际使用导入变得更容易一些。例如:

MyClass.ts

export default class SomeType { constructor() { ... } }

MyFunc.ts

export default function getThing() { return "thing"; }

Consumer.ts

import t from "./MyClass"; import f from "./MyFunc"; let x = new t( console.log(f()

这对消费者来说是最佳的。无论他们想要什么(t在这种情况下),他们都可以命名你的类型,并且不必做任何过多的点击来找到你的对象。

如果您要导出多个对象,请将它们全部置于顶层

MyThings.ts

export class SomeType { /* ... */ } export function someFunc() { /* ... */ }

相反,当进口时:

显式列出导入的名称

Consumer.ts

import { SomeType, someFunc } from "./MyThings"; let x = new SomeType( let y = someFunc(

如果您要导入大量内容,请使用命名空间导入模式

MyLargeModule.ts

export class Dog { ... } export class Cat { ... } export class Tree { ... } export class Flower { ... }

Consumer.ts

import * as myLargeModule from "./MyLargeModule.ts"; let x = new myLargeModule.Dog(

重新导出以扩展

通常您需要在模块上扩展功能。一个常见的 JS 模式是用扩展来扩充原始对象,类似于 JQuery 扩展的工作方式。正如我们之前提到的,模块像全局名称空间对象那样合并。推荐的解决方案是改变原始对象,而是导出一个提供新功能的新实体。

考虑一个在模块中定义的简单计算器实现Calculator.ts。该模块还会导出一个辅助函数,通过传递一系列输入字符串并在最后写入结果来测试计算器的功能。

Calculator.ts

export class Calculator { private current = 0; private memory = 0; private operator: string; protected processDigit(digit: string, currentValue: number) { if (digit >= "0" && digit <= "9") { return currentValue * 10 + (digit.charCodeAt(0) - "0".charCodeAt(0) } } protected processOperator(operator: string) { if (["+", "-", "*", "/"].indexOf(operator) >= 0) { return operator; } } protected evaluateOperator(operator: string, left: number, right: number): number { switch (this.operator) { case "+": return left + right; case "-": return left - right; case "*": return left * right; case "/": return left / right; } } private evaluate() { if (this.operator) { this.memory = this.evaluateOperator(this.operator, this.memory, this.current } else { this.memory = this.current; } this.current = 0; } public handelChar(char: string) { if (char === "=") { this.evaluate( return; } else { let value = this.processDigit(char, this.current if (value !== undefined) { this.current = value; return; } else { let value = this.processOperator(char if (value !== undefined) { this.evaluate( this.operator = value; return; } } } throw new Error(`Unsupported input: '${char}'` } public getResult() { return this.memory; } } export function test(c: Calculator, input: string) { for (let i = 0; i < input.length; i++) { c.handelChar(input[i] } console.log(`result of '${input}' is '${c.getResult()}'` }

下面是使用暴露test函数的计算器的简单测试。

TestCalculator.ts

import { Calculator, test } from "./Calculator"; let c = new Calculator( test(c, "1+2*33/11=" // prints 9

现在为了扩展这个以增加对基数不是10的输入的支持,让我们来创建 ProgrammerCalculator.ts

ProgrammerCalculator.ts

import { Calculator } from "./Calculator"; class ProgrammerCalculator extends Calculator { static digits = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"]; constructor(public base: number) { super( if (base <= 0 || base > ProgrammerCalculator.digits.length) { throw new Error("base has to be within 0 to 16 inclusive." } } protected processDigit(digit: string, currentValue: number) { if (ProgrammerCalculator.digits.indexOf(digit) >= 0) { return currentValue * this.base + ProgrammerCalculator.digits.indexOf(digit } } } // Export the new extended calculator as Calculator export { ProgrammerCalculator as Calculator }; // Also, export the helper function export { test } from "./Calculator";

新模块ProgrammerCalculator导出类似于原始Calculator模块的API形状,但不会增加原始模块中的任何对象。这是我们的 ProgrammerCalculator 类的测试:

TestProgrammerCalculator.ts

import { Calculator, test } from "./ProgrammerCalculator"; let c = new Calculator(2 test(c, "001+010=" // prints 3

不要在模块中使用名称空间

当首先转向基于模块的组织时,常见的趋势是将导出包装在额外的命名空间层中。模块有自己的范围,并且只有导出的声明在模块外部可见。考虑到这一点,命名空间在使用模块时提供的值很少(如果有的话)。

在组织方面,命名空间可以方便地将全局范围内的逻辑相关对象和类型组织在一起。例如,在 C#中,您将在 System.Collections 中查找所有集合类型。通过将我们的类型组织到分层名称空间中,我们为这些类型的用户提供了良好的“发现”体验。另一方面,模块已经存在于文件系统中,必然存在。我们必须通过路径和文件名来解决它们,所以我们使用逻辑组织方案。我们可以在其中包含一个带有列表模块的 / collections / generic / 文件夹。

命名空间对于避免在全局范围内命名冲突很重要。例如,你可能有My.Application.Customer.AddFormMy.Application.Order.AddForm-两种类型名称相同,但不同的命名空间。但是,这不是模块的问题。在一个模块中,有两个同名的对象没有合理的理由。从消费方面来说,任何给定模块的消费者都可以选择他们将用来引用模块的名称,这样意外的命名冲突是不可能的。

有关模块和名称空间的更多讨论,请参阅命名空间和模块。

红色标旗(Red Flags)

以下所有内容均为模块结构的红色标志。仔细检查一下,如果这些模块中的任何一个适用于您的文件,您并不试图命名空间您的外部模块:

  • 一个只有顶层声明的文件export namespace Foo { ... }(删除Foo并移动所有'up'级别)

  • 具有单个export classexport function(考虑使用export default

  • export namespace Foo {在顶层有多个相同的文件(不要认为这些文件会合并为一个Foo!)