TypeScript Modules(模块)
本文概述了TypeScript中如何使用模块以各种方式来组织代码。我们将涵括内部和外部的模块,并且讨论他们在适合在何时使用和怎么使用。我们也会学习一些如何使用外部模块的高级技巧,并且解决一些当我们使用TypeScript的模块时遇到的陷阱。
案例的基础
接下来开始写程序,我们将会在这里写上使用案例。我们来写个小型的简单字符串验证器,在我们检查网页上表单的input用户名或者检查外部数据文件格式的时候可能会用到。
单一的验证器:
interface StringValidator { isAcceptable(s: string): boolean; } var lettersRegexp = /^[A-Za-z]+$/; var numberRegexp = /^[0-9]+$/; class LettersOnlyValidator implements StringValidator { isAcceptable(s: string) { return lettersRegexp.test(s); } } class ZipCodeValidator implements StringValidator { isAcceptable(s: string) { return s.length === 5 && numberRegexp.test(s); } } // 针对以下集合中的字符串做一些简单的测试 var strings = ['Hello', '98052', '101']; // 使用验证器 var validators: { [s: string]: StringValidator; } = {}; validators['ZIP code'] = new ZipCodeValidator(); validators['Letters only'] = new LettersOnlyValidator(); // 展示每个字符串通过验证器后的结果 strings.forEach(s => { for (var name in validators) { console.log('"' + s + '" ' + (validators[name].isAcceptable(s) ? ' matches ' : ' does not match ') + name); } });
使用模块
当需要添加更多验证的时候,我们想要有一个可以跟踪类型并且不用担心与其他对象名称产生冲突的组织方案。将对象包装成一个模块,代替把大量不同的名称放在全局命名空间中。
在这个例子中,我们把验证器相关的类型都放进一个名为"Validation"的模块。因为我们希望这些接口和类在模块外是可见的,所以对他们进行export。相反, lettersRegexp和numberRegexp变量是实现功能的细节,因此不必要去导出他们,那么他们在模块外是不可见的。在文件底部的测试代码中,当在模块外使用的时候需要指定类型的名称,如"Validation.LettersOnlyValidator"。
模块化的验证器
module Validation { export interface StringValidator { isAcceptable(s: string): boolean; } var lettersRegexp = /^[A-Za-z]+$/; var numberRegexp = /^[0-9]+$/; export class LettersOnlyValidator implements StringValidator { isAcceptable(s: string) { return lettersRegexp.test(s); } } export class ZipCodeValidator implements StringValidator { isAcceptable(s: string) { return s.length === 5 && numberRegexp.test(s); } } } // 针对以下集合中的字符串做一些简单的测试 var strings = ['Hello', '98052', '101']; // 使用验证器 var validators: { [s: string]: Validation.StringValidator; } = {}; validators['ZIP code'] = new Validation.ZipCodeValidator(); validators['Letters only'] = new Validation.LettersOnlyValidator(); // 展示每个字符串通过验证器后的结果 strings.forEach(s => { for (var name in validators) { console.log('"' + s + '" ' + (validators[name].isAcceptable(s) ? ' matches ' : ' does not match ') + name); } });
拆分文件
随着我们应用程序的扩展,我们希望将代码拆分成多个文件使其更方便维护。现在,将上面的验证器模块拆分了放到多个文件中。虽然每个文件是单独的,但他们都在为同一个模块贡献功能,并且在代码中定义他们的时候就会被调用。因为每个文件是相互依赖的,我们已经添加了"reference"标签来告诉编译器文件之间的关系。实际上,我们的测试代码并没有改变。
多文件的内部模块:
Validation.ts
module Validation { export interface StringValidator { isAcceptable(s: string): boolean; } }
LettersOnlyValidator.ts
/// <reference path="Validation.ts" /> module Validation { var lettersRegexp = /^[A-Za-z]+$/; export class LettersOnlyValidator implements StringValidator { isAcceptable(s: string) { return lettersRegexp.test(s); } } }
ZipCodeValidator.ts
/// <reference path="Validation.ts" /> module Validation { var numberRegexp = /^[0-9]+$/; export class ZipCodeValidator implements StringValidator { isAcceptable(s: string) { return s.length === 5 && numberRegexp.test(s); } } }
Test.ts
/// <reference path="Validation.ts" /> /// <reference path="LettersOnlyValidator.ts" /> /// <reference path="ZipCodeValidator.ts" /> // 针对以下集合中的字符串做一些简单的测试 var strings = ['Hello', '98052', '101']; // 使用验证器 var validators: { [s: string]: Validation.StringValidator; } = {}; validators['ZIP code'] = new Validation.ZipCodeValidator(); validators['Letters only'] = new Validation.LettersOnlyValidator(); // 展示每个字符串通过验证器后的结果 strings.forEach(s => { for (var name in validators) { console.log('"' + s + '" ' + (validators[name].isAcceptable(s) ? ' matches ' : ' does not match ') + name); } });
一旦有多个文件参与项目,我们得确保所需编译的代码是否都已加载,有两种方式可以实现。
我们可以使用 -out 将所有的文件内容输出到一个单独的JavaScript文件中:
tsc --out your.js Test.ts
编译器会根据文件中的"reference"标签自动地将输出文件进行有序的排序,你也可以指定输出到单独的文件:
tsc --out your.js Validation.ts LettersOnlyValidator.ts ZipCodeValidator.ts Test.ts
或者我们也可以对每个文件进行单独的编译。如果产生多个js文件,我们就需要使用<script>标签用适当的顺序来加载文件,例如:
MyTestPage.html (文件引用)
<script src="Validation.js" type="text/javascript"></script /> <script src="LettersOnlyValidator.js" type="text/javascript"></script /> <script src="ZipCodeValidator.js" type="text/javascript"></script /> <script src="Test.js" type="text/javascript"></script />
外部模块
TypeScript也有外部模块的概念。外部模块在两个案例中使用:node.js和require.js。不使用Node.js或require.js的应用程序不需要使用外部模块,可以采用上面概述的内部模块概念。
在外部模块中,在外部模块,文件之间的关系是根据文件级别的输入和输出指定的。在TypeScript中,任何包涵最高级别的import或export的文件将被当作一个外部模块。
接下来,我们将之前的例子转换成使用外部模块的。注意,我们将不再使用关键字"module" --- 一个文件本身构成一个模块,并且通过文件名来识别这个模块。
import声明代替了"reference"标签用来指定模块间的依赖关系。import声明由两部分组成:文件的模块名称和指明所需模块路径的关键字。
import someMod = require('someModule');
我们使用"export"关键字的声明来指定对象在模块外是否可见,这个和在内部模块定义公共区域是相似的。node.js=>--module commonjs;require.js=>--module amd.例如:
tsc --module commonjs your.js Test.ts
在编译时,每个外部模块都将是一个单独的.js文件。和"reference"标签功能相似,编译器会引用import声明来处理文件之间的依赖。
Validation.ts
export interface StringValidator { isAcceptable(s: string): boolean; }
LettersOnlyValidator.ts
import validation = require('./Validation'); var lettersRegexp = /^[A-Za-z]+$/; export class LettersOnlyValidator implements validation.StringValidator { isAcceptable(s: string) { return lettersRegexp.test(s); } }
ZipCodeValidator.ts
import validation = require('./Validation'); var numberRegexp = /^[0-9]+$/; export class ZipCodeValidator implements validation.StringValidator { isAcceptable(s: string) { return s.length === 5 && numberRegexp.test(s); } }
Test.ts
import validation = require('./Validation'); import zip = require('./ZipCodeValidator'); import letters = require('./LettersOnlyValidator'); // 针对以下集合中的字符串做一些简单的测试 var strings = ['Hello', '98052', '101']; // 使用验证器 var validators: { [s: string]: validation.StringValidator; } = {}; validators['ZIP code'] = new zip.ZipCodeValidator(); validators['Letters only'] = new letters.LettersOnlyValidator(); // 展示每个字符串通过验证器后的结果 strings.forEach(s => { for (var name in validators) { console.log('"' + s + '" ' + (validators[name].isAcceptable(s) ? ' matches ' : ' does not match ') + name); } });
这里本兽测试使用的是amd规范(require.js):
tsc --module amd Test.js Test.ts
Modules.html:
<script src="require.js" data-main="Test"></script>
外部模块的代码生成
根据编译时指定了module标签,编译器将会生成对应的代码来配合node.js(commonjs)或require.js(AMD)模块加载系统。有关所生成代码中调用的defined或require的更多信息,请查阅对应模块装载程序的文档。
这个简单的例子说明了使用的名称在导入和导出过程中如何被翻译成模块加载代码。
SimpleModule.ts
import m = require('mod'); export var t = m.something + 1;
AMD / RequireJS SimpleModule.js:
define(["require", "exports", 'mod'], function(require, exports, m) { exports.t = m.something + 1; });
CommonJS / Node SimpleModule.js:
var m = require('mod'); exports.t = m.something + 1;
"export ="
在上个例子中,没当使用一次验证器,每个模块只输出一个值。在这种情况下,这些通过限定名称的标识用起来是比较麻烦的,其实一个单一的标识符即可达到一样的效果。
"export = " 语法指定从模块导出单个对象。这可以是一个类,接口,模块,函数,或枚举。当模块输入时,输出标识被直接使用,并且名称不用被限制。
接下来,我们简化下验证器的实现,每个模块使用"export ="语法来输出单一的对象。代码将会得到简化,代替了调用"zip.ZipCodeValidator",我们可以直接用"zipValidator"。
Validation.ts
export interface StringValidator { isAcceptable(s: string): boolean; }
LettersOnlyValidator.ts
import validation = require('./Validation'); var lettersRegexp = /^[A-Za-z]+$/; class LettersOnlyValidator implements validation.StringValidator { isAcceptable(s: string) { return lettersRegexp.test(s); } } export = LettersOnlyValidator;
ZipCodeValidator.ts
import validation = require('./Validation'); var numberRegexp = /^[0-9]+$/; class ZipCodeValidator implements validation.StringValidator { isAcceptable(s: string) { return s.length === 5 && numberRegexp.test(s); } } export = ZipCodeValidator;
Test.ts
import validation = require('./Validation'); import zipValidator = require('./ZipCodeValidator'); import lettersValidator = require('./LettersOnlyValidator'); // 针对以下集合中的字符串做一些简单的测试 var strings = ['Hello', '98052', '101']; // 使用验证器 var validators: { [s: string]: validation.StringValidator; } = {}; validators['ZIP code'] = new zipValidator(); validators['Letters only'] = new lettersValidator(); // 展示每个字符串通过验证器后的结果 strings.forEach(s => { for (var name in validators) { console.log('"' + s + '" ' + (validators[name].isAcceptable(s) ? ' matches ' : ' does not match ') + name); } });
别名
另一种达到模块简化工作的方法是使用 import q = x.y.z 为常用的对象创建一个较短的名称。不要将其和"import x = require('name')"语法混淆,这个语法只是简单的为指定的标识创建一个别名。你可以对任何类型的标识符使用这种方式(通常称为别名),包括模块外创建的对象。
使用案例:
module Shapes { export module Polygons { export class Triangle { } export class Square { } } } import polygons = Shapes.Polygons; var sq = new polygons.Square(); // 和'new Shapes.Polygons.Square()'一样
注意,我们不需要使用require关键字;而是直接将导入的标识符的名称进行赋值。这个使用"var"差不多,但也适用与导入的标识符类型和命名空间存在意义。重要的是,对于值而言,import是来源于原始标识符的引用,所以改变一个var的别名的值的时候,原始的值不会被影响。
可选模块和更高级的加载方案
在某些情况下,你可能需要当满足一些条件的时候才加载模块。在TypeScript中,我们可以使用下面案例的模来实现模块的可选加载,还有更高级的加载方案可以直接调用模块加载器并且避免类型丢失。
编译器检测JavaScript中每个模块是否被用到。如果某个模块只是被作为类型系统的一部分,则不需要调用require加载。从性能优化来说,对未使用的引用进行选择是非常好的,而且还实现了模块的可选加载。
这个模式的核心思想是操作通过"import id = require('...')“声明为我们提供的外部模块所导出的类型。模块加载器是动态调用的(通过require),正如下面 "if" 代码块所示。利用将引用进行过滤,可实现模块只在需要的时候被加载。为了使其运行,需要注意 "import" 定义的标识符只能在类型中使用(比如,不能在会被转换成JavaScript的代码中使用)。
为了确保类型完整,我们需要用到"typeof"关键字。"typeof"关键字可用于类型判断,返回给定值的类型,这里表示模块的类型。
node.js中的模块动态加载
declare var require; import Zip = require('./ZipCodeValidator'); if (needZipValidation) { var x: typeof Zip = require('./ZipCodeValidator'); if (x.isAcceptable('.....')) { /* ... */ } }
require.js中的模块动态加载
declare var require; import Zip = require('./ZipCodeValidator'); if (needZipValidation) { require(['./ZipCodeValidator'], (x: typeof Zip) => { if (x.isAcceptable('...')) { /* ... */ } }); }
与其他JavaScript库配合使用
为了描述不是基于TypeScript来写的类库的类型,我们需要对类库暴露的api进行声明。因为大部分的JavaScript库只暴露一些顶级对象,所以很适合用模块来代表它们。我们称之为未定义执行"环境"的声明。通常这些是定义在.d.ts文件中的(如jquery.d.ts)。如果你熟悉C或者C++,你可以将这些理解为.h文件或者'extern'。接下来就看些例子吧,有内部模块的也有外部模块的。
内部模块
比较常见的一个类库"D3"将这些这些功能定义在一个名为"D3"的全局对象上。因为类库是通过"script"标签加载的(而不是模块加载器),需要在内部模块声明用以定义类库的类型。为了TypeScript编译器能够识别这些类型,我们在内部模块声明。例如:
D3.d.ts (简化后的摘录代码)
declare module D3 { export interface Selectors { select: { (selector: string): Selection; (element: EventTarget): Selection; }; } export interface Event { x: number; y: number; } export interface Base extends Selectors { event: Event; } } declare var d3: D3.Base;
外部模块
在node.js里,大部分工作是通过加载一个或多个模块完成的。我们可以使用顶级的export为每个模块声明相对应的.d.ts文件。不过写一个大的.d.ts文件其实是方便的。这样做之后,我们使用模块的引用名称,方便稍后引入可用。例如:
node.d.ts (简化后的摘录代码)
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 url = require("url")"加载模块。
///<reference path="node.d.ts"/> import url = require("url"); var myUrl = url.parse("http://www.typescriptlang.org");
TypeScript模块的缺陷
在这一节中,我们将介绍使用内部和外部模块的各种常见的陷阱,以及如何避免它们。
/// <reference /> 引入外部模块
一个常见的错误就是尝试使用"/// <reference>"语法来引用一个外部模块文件,而不是使用"import"。要理解他们之间的区别,首先需要了解编译器找到外部模块类型信息的三种方法。
第一种是通过"import x = require(...)"查对应找命名的.ts文件。该文件应该是一个具有顶级import或export声明的执行文件。
第二种是通过.d.ts文件的查找,和上面相似,除了作为一个执行文件,同时也是一个声明文件(也有顶级import或export声明)。
最后一种是通过检测一个"外部模块的声明",在这里我们"declare"一个以匹配名称进行引用的模块。
myModules.d.ts
// 在.d.ts文件或者.ts文件中还不是一个外部模块 declare module "SomeModule" { export function fn(): string; } myOtherModule.ts /// <reference path="myModules.d.ts" /> import m = require("SomeModule");
这里的"reference"标签允许查找包含外部模块声明的声明文件。这也体现了node.d.ts文件在TypeScript中是如何工作的。
不必要的命名空间
如果您将一个程序从内部模块转换为外部模块,它可以很容易地搞定并且得到一个看起来像这样的文件:
shapes.ts
export module Shapes { export class Triangle { /* ... */ } export class Square { /* ... */ } }
在这里顶级模块"Shapes"包装了"Triangle"和"Square"。这也使得模块的处理者感到困惑麻烦:
shapeConsumer.ts import shapes = require('./shapes'); var t = new shapes.Shapes.Triangle(); // shapes.Shapes?
在TypeScript中,外部模块一个关键特征就是两个不同的外部模块不会为同一个作用域提供名称。因为外部模块的消费者决定了它的名字,所以没有必要再一次将输出标识符包装进一个命名空间。
重申下为什么在外部模块不需要使用命名空间,命名空间的主要思想是提供构造的逻辑分组和防止命名冲突。因为外部模块文件本身已经是一个逻辑分组,并且它的顶级名称是由输入的代码定义的,所以不需要使用一个额外的模块层来导出对象。
修订的例子:
shapes.ts
export class Triangle { /* ... */ } export class Square { /* ... */ } shapeConsumer.ts import shapes = require('./shapes'); var t = new shapes.Triangle();
外部模块之间的规定
正是因为每个js文件和模块是"一对一"对应的,TypeScript的外部模块源文件和他们的转换后js文件也是"一对一"对应的。这也导致使用编译器开关"--out"来将多个外部模块源文件联系起来并且放到一个单独的JavaScript文件中是不可能的。