模块
在TypeScript中利用模块(module)来组织代码。这里将讨论内部和外部模块,以及在何时使用哪种方式更合适,以及怎么使用。当然也会讨论一些高级话题,例如如何使用外部模块,以及在TypeScript中使用模块时常见的一些错误。
第一步
让我们从下面的例子开始,这个例子将会贯穿本文。首先我们写了一段字符串验证代码,用来验证用户在web页面表单输入的信息,或者是检查外部文件提供的数据格式。
在单个文件中的Validator
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);
}
}
// Some samples to try
var strings = ['Hello', '98052', '101'];
// Validators to use
var 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 (var name in validators) {
console.log('"' + s + '" ' + (validators[name].isAcceptable(s) ? ' matches ' : ' does not match ') + name);
}
});
增加模块
当增加更多的验证逻辑时,我们希望利用某种组织方式跟踪验证类型,而不必担心与其他对象的名称冲突。不是将大量的不同名称放在全局命名空间中,而是将对象封装成模块。
在下面例子中,我们将把所有与validator相关的类型都移到一个'Validation'模块中。因为我们希望接口和类对外部模块可见,所以我们在前面加上了'export'关键字导出接口和类。相反,变量lettersRegexp和numberRegexp是实现细节,我们并不希望暴露给外部模块。在文件最后面的测试代码中,要想在外部模块使用就需要用模块名称限定类型名称,例如Validation.LettersOnlyValidator。
模块化的Validators
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);
}
}
}
// Some samples to try
var strings = ['Hello', '98052', '101'];
// Validators to use
var validators: { [s: string]: Validation.StringValidator; } = {};
validators['ZIP code'] = new Validation.ZipCodeValidator();
validators['Letters only'] = new Validation.LettersOnlyValidator();
// Show whether each string passed each validator
strings.forEach(s => {
for (var name in validators) {
console.log('"' + s + '" ' + (validators[name].isAcceptable(s) ? ' matches ' : ' does not match ') + name);
}
});
拆分为多个文件
随着应用程序的增长,希望将代码分为多个文件,使维护更容易。
下面,将Validation模块的内容分到多个文件中。虽然这些文件是分开的,但每一个文件都对同一个模块做出贡献,当使用时就像是在一个地方定义的。由于文件之间存在依赖关系,因此我们添加了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" />
// Some samples to try
var strings = ['Hello', '98052', '101'];
// Validators to use
var validators: { [s: string]: Validation.StringValidator; } = {};
validators['ZIP code'] = new Validation.ZipCodeValidator();
validators['Letters only'] = new Validation.LettersOnlyValidator();
// Show whether each string passed each validator
strings.forEach(s => {
for (var name in validators) {
console.log('"' + s + '" ' + (validators[name].isAcceptable(s) ? ' matches ' : ' does not match ') + name);
}
});
一旦涉及多个文件,就需要保证编译器能加载各个文件。这里有两种方式做到:
第一种方式:可以利用 --out flag连接多个输入文件让编译器编译输出一个JavaScript文件。
tsc --out sample.js Test.ts
编译器会根据文件中的reference标签排序输出文件。
也可以指定每一个文件:
tsc --out sample.js Validation.ts LettersOnlyValidator.ts ZipCodeValidator.ts Test.ts
第二种方式:可以对每个输入文件单独编译(缺省方式)生成一个JavaScript文件。如果生成了多个JS文件,可以在web页面按照一定的顺序使用<script>标签加载每一个生成的JS文件,例如:
MyTestPage.html (节选)
<script src="Validation.js" type="text/javascript" />
<script src="LettersOnlyValidator.js" type="text/javascript" />
<script src="ZipCodeValidator.js" type="text/javascript" />
<script src="Test.js" type="text/javascript" />
外部模块
TypeScript还有一个外部模块(external module)的概念。在两种情况下使用外部模块:node.js 和require.js。没有使用node.js或require.js的应用程序不需要使用外部模块,就使用前面讲到的内部模块来组织代码即可。
在外部模块中,文件之间的关系是在文件级根据imports和exports来指定的。在TypeScript中,只要文件包含顶级import 或 export 就认为是一个外部模块。
下面将前一个例子转换为使用外部模块。注意我们不再使用module关键字 – 组成一个模块的文件是通过其文件名来标识的。
前面的reference标签被import 语句替换,这个语句指定了模块之间的依赖关系。import 语句包含两部分:本文件知道的模块名称,require关键字指定所需模块的路径:
import someMod = require('someModule');
我们在最顶端声明中用export 关键字表示哪些对象对模块外部可见,类似于内部模块中用export 定义公开区域。
编译时在命令行中必须指定一个module的target。对于node.js使用--module commonjs; 对于require.js使用--module amd。例如:
tsc --module commonjs 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');
// Some samples to try
var strings = ['Hello', '98052', '101'];
// Validators to use
var validators: { [s: string]: validation.StringValidator; } = {};
validators['ZIP code'] = new zip.ZipCodeValidator();
validators['Letters only'] = new letters.LettersOnlyValidator();
// Show whether each string passed each validator
strings.forEach(s => {
for (var name in validators) {
console.log('"' + s + '" ' + (validators[name].isAcceptable(s) ? ' matches ' : ' does not match ') + name);
}
});
为外部模块生成代码
依赖于编译时指定的模块target,编译器将为node.js(commonjs)或require.js (AMD)模块加载系统生成相应的代码。生成代码中define 调用和 require调用的更多信息,可参见每个模块加载器相关的文档。
下面这个简单例子展示了import与export中的名称是如何翻译为模块加载代码的:
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 =
在前面例子中,当我们需要引用每个validator时,每个module只export一个值。在这些情况下,通过限定名称来引用这些符号就显得很麻烦,实际上用一个标识符就可以做到。
export = 语法指定模块导出的单个对象,这里的对象可以是一个class、interface、module、function或enum。当导入时,就直接引用导出的符号而不用再通过限定名称。
下面,我们利用export = 语法将Validator实现简化为对每个模块仅导出单个对象。这简化了调用代码,不用再引用 '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');
// Some samples to try
var strings = ['Hello', '98052', '101'];
// Validators to use
var validators: { [s: string]: validation.StringValidator; } = {};
validators['ZIP code'] = new zipValidator();
validators['Letters only'] = new lettersValidator();
// Show whether each string passed each validator
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(); // Same as 'new Shapes.Polygons.Square()'
注意这里我们没有使用require 关键字,相反可以从导入符号的限定名称直接赋值。这类似于使用var,但也可以用于导入符号的类型和命名空间。重要的是对于values, import是不同于original符号的一个引用,所以修改别名var 不会影响到original变量。
可选模块加载与其他高级加载场景
在一些情况下,某些条件下只想加载一个模块。在TypeScript中,可以用下面的模式来实现这一点以及其他高级加载场景来直接激活模块加载器而且不损失类型安全。
编译器检测每个模块是否在生成的JavaScript中用到。对那些仅用作部分类型系统的模块,没有require调用。剔除这些没用的references是好的性能优化,还允许模块可选加载。
这个模式的核心思想是import id = require('...') 语句用于访问外部模块暴露的类型。模块加载器通过require被动态激活,在下面的if区块中展示。利用reference剔除优化,模块只有在需要时才被激活。这个模式要想生效,重要的是通过import定义的符号只用于类型定位(即永远不是在会被生成到JavaScript中的某个位置上)。
为了保证类型安全,可以用typeof 关键字。typeof 关键字生成值的类型,即外部模块的类型。
node.js中动态模块加载
declare var require;
import Zip = require('./ZipCodeValidator');
if (needZipValidation) {
var x: typeof Zip = require('./ZipCodeValidator');
if (x.isAcceptable('.....')) { /* ... */ }
}
Sample: require.js中动态模块加载
declare var require;
import Zip = require('./ZipCodeValidator');
if (needZipValidation) {
require(['./ZipCodeValidator'], (x: typeof Zip) => {
if (x.isAcceptable('...')) { /* ... */ }
});
}
与其他JavaScript库工作
为了描述不是用TypeScript编写的库的shape,需要声明库对外暴露的API。由于大多数JavaScript库对外仅暴露一些顶级对象,模块是表示它们的一种好方法。我们将不定义实现的声明称为"ambient",通常都定义在.d.ts文件中。如果你熟悉C/C++,可以把这些文件看做.h头文件或'extern'。下面看一些内部和外部例子。
Ambient内部模块
流行库D3用一个全局对象"D3"来定义其功能。因为这个库是通过一个script标签加载的,不是通过模块加载器来加载的,他鸟巢它的声明是使用internal modules来定义shape的。要让TypeScript编译器看到这个shape,可以用一个ambient内部模块声明。例如:
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;
Ambient外部模块
在node.js中,多数任务都是通过加载一个或多个模块来完成的。可以在每个模块自己的.d.ts文件中用顶级export声明,但更方便的是可以把它们写入一个更大的.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");
模块常见错误
在这一章节中,描述在使用内部模块和外部模块中各种常见错误,以及如何来避免这些错误。
/// <reference> to an external module
一个常见错误是试图用/// <reference> 语法而不是用import来引用一个外部模块文件。为了理解这两者差异,首先需要理解编译器定位外部模块类型信息的三种方式。
第一种方式是通过import x = require(...); 声明来查找对应的.ts文件,这个文件应当是个有顶级import或export声明的实现文件。
第二种方式是查找.d.ts文件,类似于上面第一种方式,但这个文件不是一个实现文件,而是一个声明文件(也是有顶级import或export声明)。
最后一种方式是查找ambient外部模块声明,这里是对模块名称加引号来声明。
myModules.d.ts
// In a .d.ts file or .ts file that is not an external module:
declare module "SomeModule" {
export function fn(): string;
}
myOtherModule.ts
/// <reference path="myModules.d.ts" />
import m = require("SomeModule");
这里的reference标签用于定位包含ambient外部模块声明的声明文件。这就是有好多TypeScript例子使用node.d.ts文件的原因。
不需要的命名空间
如果你正在将内部模块程序转换为外部模块,采用下面这样的文件就很容易:
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文件中。
参考资料
[1] http://www.typescriptlang.org/Handbook#modules
[2] TypeScript - Modules(模块), 破狼blog, http://greengerong.com/blog/2015/04/12/typescript-modules-mo-kuai/
[3] TypeScript系列1-简介及版本新特性, http://my.oschina.net/1pei/blog/493012
[4] TypeScript手册翻译系列1-基础类型, http://my.oschina.net/1pei/blog/493181
[5] TypeScript手册翻译系列2-接口, http://my.oschina.net/1pei/blog/493388
[6] TypeScript手册翻译系列3-类, http://my.oschina.net/1pei/blog/493539