在TypeScript中利用模块(module)来组织代码。这里将讨论内部和外部模块,以及在什么时候使用哪一种方式更合适,以及怎么使用。固然也会讨论一些高级话题,例如如何使用外部模块,以及在TypeScript中使用模块时常见的一些错误。javascript
第一步
html
让咱们从下面的例子开始,这个例子将会贯穿本文。首先咱们写了一段字符串验证代码,用来验证用户在web页面表单输入的信息,或者是检查外部文件提供的数据格式。java
在单个文件中的Validator
node
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); } });
当增长更多的验证逻辑时,咱们但愿利用某种组织方式跟踪验证类型,而没必要担忧与其余对象的名称冲突。不是将大量的不一样名称放在全局命名空间中,而是将对象封装成模块。web
在下面例子中,咱们将把全部与validator相关的类型都移到一个'Validation'模块中。由于咱们但愿接口和类对外部模块可见,因此咱们在前面加上了'export'关键字导出接口和类。相反,变量lettersRegexp和numberRegexp是实现细节,咱们并不但愿暴露给外部模块。在文件最后面的测试代码中,要想在外部模块使用就须要用模块名称限定类型名称,例如Validation.LettersOnlyValidator。
typescript
模块化的Validators
shell
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;
在前面例子中,当咱们须要引用每一个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('...')) { /* ... */ } }); }
为了描述不是用TypeScript编写的库的shape,须要声明库对外暴露的API。因为大多数JavaScript库对外仅暴露一些顶级对象,模块是表示它们的一种好方法。咱们将不定义实现的声明称为"ambient",一般都定义在.d.ts文件中。若是你熟悉C/C++,能够把这些文件看作.h头文件或'extern'。下面看一些内部和外部例子。
流行库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;
在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