模块

这一章我们会讨论TypeScript中的模块。下文中,将会提到内部和外部模块,并且讨论使用它们的时机。然后,我们会探讨一些关于使用外部模块的高级话题,以及使用TypeScript模块时的一些注意点。

第一步

让我们以一个将会不断出现在这整个章节的例子为开始。我们写了一个简单的字符串验证器,可以用以验证用户表单的输入。

  1. interface StringValidator {
  2. isAcceptable(s: string): boolean;
  3. }
  4. var lettersRegexp = /^[A-Za-z]+$/;
  5. var numberRegexp = /^[0-9]+$/;
  6. class LettersOnlyValidator implements StringValidator {
  7. isAcceptable(s: string) {
  8. return lettersRegexp.test(s);
  9. }
  10. }
  11. class ZipCodeValidator implements StringValidator {
  12. isAcceptable(s: string) {
  13. return s.length === 5 && numberRegexp.test(s);
  14. }
  15. }
  16. // Some samples to try
  17. var strings = ['Hello', '98052', '101'];
  18. // Validators to use
  19. var validators: { [s: string]: StringValidator; } = {};
  20. validators['ZIP code'] = new ZipCodeValidator();
  21. validators['Letters only'] = new LettersOnlyValidator();
  22. // Show whether each string passed each validator
  23. strings.forEach(s => {
  24. for (var name in validators) {
  25. console.log('"' + s + '" ' + (validators[name].isAcceptable(s) ? ' matches ' : ' does not match ') + name);
  26. }
  27. });

模块化

当我们添加越来越多的验证器时,我们希望有一种组织它们的方式,来保证它们相互之间不会有命名冲突。所以我们开始使用模块,而不是再将它们命名在全局空间里。

在下面的例子中,我们将验证器相关的类型,转移到了Validation模块下。由于我们希望这些接口和类能被外部所访问,所以我们使用export关键字导出它们。lettersRegexpnumberRegexp变量是实现细节,所以外部并不能访问到它们。在模块外部时,如果我们需要访问模块导出的成员,我们需要在其之前加上模块名,如Validation.LettersOnlyValidator

  1. module Validation {
  2. export interface StringValidator {
  3. isAcceptable(s: string): boolean;
  4. }
  5. var lettersRegexp = /^[A-Za-z]+$/;
  6. var numberRegexp = /^[0-9]+$/;
  7. export class LettersOnlyValidator implements StringValidator {
  8. isAcceptable(s: string) {
  9. return lettersRegexp.test(s);
  10. }
  11. }
  12. export class ZipCodeValidator implements StringValidator {
  13. isAcceptable(s: string) {
  14. return s.length === 5 && numberRegexp.test(s);
  15. }
  16. }
  17. }
  18. // Some samples to try
  19. var strings = ['Hello', '98052', '101'];
  20. // Validators to use
  21. var validators: { [s: string]: Validation.StringValidator; } = {};
  22. validators['ZIP code'] = new Validation.ZipCodeValidator();
  23. validators['Letters only'] = new Validation.LettersOnlyValidator();
  24. // Show whether each string passed each validator
  25. strings.forEach(s => {
  26. for (var name in validators) {
  27. console.log('"' + s + '" ' + (validators[name].isAcceptable(s) ? ' matches ' : ' does not match ') + name);
  28. }
  29. });

跨文件分割

当我们的应用逐渐变大时,为了可维护性,我们需要将代码分割到多个文件里。

这里,我们将我们的Validation模块分割到多个文件中。但是尽管在不同文件中,它们仍被视为是同一个模块,并且可以在同一处被一起使用。因为我们添加了reference标签,来告诉编译器它们之间的关系。

跨文件内部模块

Validation.ts

·

  1. module Validation {
  2. export interface StringValidator {
  3. isAcceptable(s: string): boolean;
  4. }
  5. }

LettersOnlyValidator.ts

  1. /// <reference path="Validation.ts" />
  2. module Validation {
  3. var lettersRegexp = /^[A-Za-z]+$/;
  4. export class LettersOnlyValidator implements StringValidator {
  5. isAcceptable(s: string) {
  6. return lettersRegexp.test(s);
  7. }
  8. }
  9. }

ZipCodeValidator.ts

  1. /// <reference path="Validation.ts" />
  2. module Validation {
  3. var numberRegexp = /^[0-9]+$/;
  4. export class ZipCodeValidator implements StringValidator {
  5. isAcceptable(s: string) {
  6. return s.length === 5 && numberRegexp.test(s);
  7. }
  8. }
  9. }

Test.ts

  1. /// <reference path="Validation.ts" />
  2. /// <reference path="LettersOnlyValidator.ts" />
  3. /// <reference path="ZipCodeValidator.ts" />
  4. // Some samples to try
  5. var strings = ['Hello', '98052', '101'];
  6. // Validators to use
  7. var validators: { [s: string]: Validation.StringValidator; } = {};
  8. validators['ZIP code'] = new Validation.ZipCodeValidator();
  9. validators['Letters only'] = new Validation.LettersOnlyValidator();
  10. // Show whether each string passed each validator
  11. strings.forEach(s => {
  12. for (var name in validators) {
  13. console.log('"' + s + '" ' + (validators[name].isAcceptable(s) ? ' matches ' : ' does not match ') + name);
  14. }
  15. });

一旦涉及到多个文件,我们需要确保所有编译后的代码都被正确加载。有两种办法:

第一种,我们可以使用--out参数来将所有的文件在编译时联结入一个单独的JavaScript文件:

  1. tsc --out sample.js Test.ts

这时编译器就会自动得去寻找输出文件中reference标识所指向的文件。你也可以手动地指定这些文件:

  1. tsc --out sample.js Validation.ts LettersOnlyValidator.ts ZipCodeValidator.ts Test.ts

或者,你也可以一个个地单独编译这些文件。在这些JS文件生成后,我们可以在网页上以适当的顺序,使用<script>标签来加载它们,例子:

MyTestPage.html

  1. <script src="Validation.js" type="text/javascript" />
  2. <script src="LettersOnlyValidator.js" type="text/javascript" />
  3. <script src="ZipCodeValidator.js" type="text/javascript" />
  4. <script src="Test.js" type="text/javascript" />

外部模块

TypeScript还有外部模块的概念。外部模块只会在两种情况下被使用:Node.jsRequire.js。不使用Node.jsRequire.js的应用不需要使用到外部模块,应该使用我们上文提到的内部模块。

当使用外部模块使,文件之间的关系由文件级别的导入和导出所定义。在TypeScript中,任何包含了文件级别的导入或导出的文件,即被视为外部模块。

下文中,我们将把之前的例子变成使用外部模块。注意,我们将不再使用module关键字,因为文件本身就是一个模块,且模块名就是它的文件名。

reference标签将会被import声明所替代。import声明包含两部分:本文件将会使用的该模块的名字,以及require关键字和文件的路径:

  1. import someMod = require('someModule');

我们同过在文件级别使用export关键字来指定哪些成员可以被外部所访问,这与内部模块是类似的。

在编译时,我们必须指定一个模块规范,在使用Node.js时,使用--module commonjs; 在使用Require.js时,使用--module amd。例子:tsc --module commonjs Test.ts

在编译完毕后,每个外部模块都将是一个单独的.js文件。和reference标签一样,编译器将会根据import声明来编译依赖文件。

Validation.ts

  1. export interface StringValidator {
  2. isAcceptable(s: string): boolean;
  3. }

LettersOnlyValidator.ts

  1. import validation = require('./Validation');
  2. var lettersRegexp = /^[A-Za-z]+$/;
  3. export class LettersOnlyValidator implements validation.StringValidator {
  4. isAcceptable(s: string) {
  5. return lettersRegexp.test(s);
  6. }
  7. }

ZipCodeValidator.ts

  1. import validation = require('./Validation');
  2. var numberRegexp = /^[0-9]+$/;
  3. export class ZipCodeValidator implements validation.StringValidator {
  4. isAcceptable(s: string) {
  5. return s.length === 5 && numberRegexp.test(s);
  6. }
  7. }

Test.ts

  1. import validation = require('./Validation');
  2. import zip = require('./ZipCodeValidator');
  3. import letters = require('./LettersOnlyValidator');
  4. // Some samples to try
  5. var strings = ['Hello', '98052', '101'];
  6. // Validators to use
  7. var validators: { [s: string]: validation.StringValidator; } = {};
  8. validators['ZIP code'] = new zip.ZipCodeValidator();
  9. validators['Letters only'] = new letters.LettersOnlyValidator();
  10. // Show whether each string passed each validator
  11. strings.forEach(s => {
  12. for (var name in validators) {
  13. console.log('"' + s + '" ' + (validators[name].isAcceptable(s) ? ' matches ' : ' does not match ') + name);
  14. }
  15. });

外部模块的代码生成

根据指定的模块规范,编译器将会为commonjsAMD产生不同的代码。更多详情,请参阅各自模块的说明文档。

以下是不同模块规范所产生的代码的例子:

SimpleModule.ts

  1. import m = require('mod');
  2. export var t = m.something + 1;

AMD / RequireJS SimpleModule.js

  1. define(["require", "exports", 'mod'], function(require, exports, m) {
  2. exports.t = m.something + 1;
  3. });

CommonJS / Node SimpleModule.js

  1. var m = require('mod');
  2. exports.t = m.something + 1;

Export =

上面的例子里,每个验证器中,每个模块都只导出一个值。但是,当模块只导出一个值时,例子的语法就有点略显笨拙了。

export =语法可以直接定义一个模块导出的单一成员。它可以是一个类,接口,模块,函数,枚举值等等。当被导入后,这个成员可以直接使用。

下面的例子中,每个模块我们都只通过export =语法导出一个对象。这简化了代码,在引用时,我们不必书写zip.ZipCodeValidator,而可以只使用zipValidator

Validation.ts

  1. export interface StringValidator {
  2. isAcceptable(s: string): boolean;
  3. }

LettersOnlyValidator.ts

  1. import validation = require('./Validation');
  2. var lettersRegexp = /^[A-Za-z]+$/;
  3. class LettersOnlyValidator implements validation.StringValidator {
  4. isAcceptable(s: string) {
  5. return lettersRegexp.test(s);
  6. }
  7. }
  8. export = LettersOnlyValidator;

ZipCodeValidator.ts

  1. import validation = require('./Validation');
  2. var numberRegexp = /^[0-9]+$/;
  3. class ZipCodeValidator implements validation.StringValidator {
  4. isAcceptable(s: string) {
  5. return s.length === 5 && numberRegexp.test(s);
  6. }
  7. }
  8. export = ZipCodeValidator;

Test.ts

  1. import validation = require('./Validation');
  2. import zipValidator = require('./ZipCodeValidator');
  3. import lettersValidator = require('./LettersOnlyValidator');
  4. // Some samples to try
  5. var strings = ['Hello', '98052', '101'];
  6. // Validators to use
  7. var validators: { [s: string]: validation.StringValidator; } = {};
  8. validators['ZIP code'] = new zipValidator();
  9. validators['Letters only'] = new lettersValidator();
  10. // Show whether each string passed each validator
  11. strings.forEach(s => {
  12. for (var name in validators) {
  13. console.log('"' + s + '" ' + (validators[name].isAcceptable(s) ? ' matches ' : ' does not match ') + name);
  14. }
  15. });

引用

你也可以通过import q = x.y.z来简化你的代码。不要被import x = require('name')加载外部模块的语法所迷惑,这个语法仅仅是简单地创建了指定目标的一个引用。

  1. module Shapes {
  2. export module Polygons {
  3. export class Triangle { }
  4. export class Square { }
  5. }
  6. }
  7. import polygons = Shapes.Polygons;
  8. var sq = new polygons.Square(); // Same as 'new Shapes.Polygons.Square()'

注意,我们并没有使用require关键字。而是简单把一个引用直接传递了我们想要导入的值。这和使用var类似,但是它也能在类型上使用。值得注意的是,对于被引用的值,import只是深复制了这个原始值,所以更变这个引用并不用影响原始值。

可选模块加载,以及其他高级加载方案

在一些情况下,你可以只在一些情况下,才想加载这些模块。在TypeScript中,我们可以用下面展示的方法来实现它。

编译器会检测每一个模块。对于被require进来,但并没有使用的模块,编译器将会忽略它们。这种做法,对于编译器性能是一个很好的提升,并且将使可选模块加载成为可能。

这种模式的核心概念就是,import id = require('...')将会给予我们一个外部模块暴露出的类型的入口。模块加载器(通过require)是动态执行的,它只会在被需要时加载,如在下面例子中的if语句块中。但是要注意,只有在被导入的目标用于TypeScript类型该出现的位置时,这种做法才有用(千万不要把它们用在JavaScript中也有的位置)。

为了使之类型安全,我们可以使用typeof关键字。当在类型位置使用typeof关键字时,它会产生一个值的类型,在下面例子中,即外部模块导出值的类型。

Node.js中的动态模块加载

  1. declare var require;
  2. import Zip = require('./ZipCodeValidator');
  3. if (needZipValidation) {
  4. var x: typeof Zip = require('./ZipCodeValidator');
  5. if (x.isAcceptable('.....')) { /* ... */ }
  6. }

RequireJS中的动态模块加载

  1. declare var require;
  2. import Zip = require('./ZipCodeValidator');
  3. if (needZipValidation) {
  4. require(['./ZipCodeValidator'], (x: typeof Zip) => {
  5. if (x.isAcceptable('...')) { /* ... */ }
  6. });
  7. }

与其他JavaScript库共同工作

为了描述非TypeScript代码库的类型,我们需要描述它们暴露的API。由于大多数JavaScript库都只暴露少数的顶层对象,所以使用模块可以很好得代表它们。我们把这些定义称作“环境”。大多数时候它们被定义在.d.ts文件中。如果你熟悉C/C++,你可以认为它们就是.h文件或extern。让我们看一些相关的内部模块和外部模块的例子:

内部模块环境

流行的D3库,将它的所有功能定义在一个全局对象D3上。因为这个库是通过script标签加载的,所以我们使用内部模块定义它:

D3.d.ts

  1. declare module D3 {
  2. export interface Selectors {
  3. select: {
  4. (selector: string): Selection;
  5. (element: EventTarget): Selection;
  6. };
  7. }
  8. export interface Event {
  9. x: number;
  10. y: number;
  11. }
  12. export interface Base extends Selectors {
  13. event: Event;
  14. }
  15. }
  16. declare var d3: D3.Base;

外部模块环境

Node.js里,大多数任务都需要通过加载一个或多个模块来完成。我们可以为每个模块都定义一个.d.ts文件。但是,更方便的做法,将它们都写在一个.d.ts文件中。例子:

node.d.ts

  1. declare module "url" {
  2. export interface Url {
  3. protocol?: string;
  4. hostname?: string;
  5. pathname?: string;
  6. }
  7. export function parse(urlStr: string, parseQueryString?, slashesDenoteHost?): Url;
  8. }
  9. declare module "path" {
  10. export function normalize(p: string): string;
  11. export function join(...paths: any[]): string;
  12. export var sep: string;
  13. }

然后我们可以通过/// <reference> node.d.ts来使用它了:

  1. ///<reference path="node.d.ts"/>
  2. import url = require("url");
  3. var myUrl = url.parse("http://www.typescriptlang.org");

陷阱

这里我们将讨论一些内部模块和外部模块的陷阱,以及如何避免它们。

/// 一个外部模块

一个普遍的错误就是使用/// <reference>语法来导入一个外部模块文件,而不是使用import。为了理解这个,我们首先要理解编译器从外部模块加载类型信息的三种方式。

首先是通过import x = require(...);声明来发现.ts文件。这个文件必须在顶层有导入或导出声明。

第二种方法则是通过寻找.d.ts文件,这与上述方式类似,但是不需要实现细节。它仅仅是一个描述文件(同样需要有顶层的导入或导出声明)。

最后一种则是寻找一个“外部模块声明环境”,即我们使用指定名字且使用declare关键字描述了一个模块。

myModules.d.ts

  1. // In a .d.ts file or .ts file that is not an external module:
  2. declare module "SomeModule" {
  3. export function fn(): string;
  4. }

myOtherModule.ts

  1. /// <reference path="myModules.d.ts" />
  2. import m = require("SomeModule");

不需要的命名空间

如果你尝试将一个内部模块转换为外部模块,你可能会写出以下代码:

shapes.ts

  1. export module Shapes {
  2. export class Triangle { /* ... */ }
  3. export class Square { /* ... */ }
  4. }

以上文件中有一个顶层模块Shapes,且包含了两个内部类。这可能会让你的模块使用者产生困惑:

shapeConsumer.ts

  1. import shapes = require('./shapes');
  2. var t = new shapes.Shapes.Triangle(); // shapes.Shapes?

TypeScript的外部模块的一个关键特性就是,不同的外部模块在同一个上文中,永远不会有命名冲突。因为外部模块的使用者会自己决定它的名字。所以将所有的导出成员包含在一个命名空间中是没有必要的。

再次重申,由于外部模块它本身就已经是一个独立的逻辑块,并且它的名字是在导入时决定的。所以用一个额外的命名空间将其包围是没有必要的。

修正过的代码:

shapes.ts

  1. export class Triangle { /* ... */ }
  2. export class Square { /* ... */ }

shapeConsumer.ts

  1. import shapes = require('./shapes');
  2. var t = new shapes.Triangle();

外部模块的权衡

和JS中的模块和文件的一对一关系一样,TypeScript中的外部模块和文件也是一对一的。所以这对使用--out参数来将所有外部模块源文件合并入一个JavaScript文件,是有影响的。