本文概述了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文件中是不可能的。

TypeScript Modules(模块)的更多相关文章

  1. TypeScript 素描 - 模块解析、声明合并

    模块解析 模块解析有两种方式 相对方式  也就是以/或 ./或-/开头的,比如import jq  from "/jq" 非相对方式  比如 import model  from ...

  2. TypeScript 之 模块

    https://m.runoob.com/manual/gitbook/TypeScript/_book/doc/handbook/Modules.html 外部模块简写 外部模块简写:declare ...

  3. TypeScript 素描 - 模块

    /* 其实前面一些都是废话,因为都和C#类似.从模块开始就需要深入的去理解了 文档反复声明了 内部模块现在称做 命令空间 外部模块称为 模块 模块在其自身的作用域里执行,而不是在全局作用域里,也就是说 ...

  4. TypeScript 素描 - 模块、命名空间

    /* 其实前面一些都是废话,因为都和C#类似.从模块开始就需要深入的去理解了 文档反复声明了 内部模块现在称做 命令空间 外部模块称为 模块 模块在其自身的作用域里执行,而不是在全局作用域里,也就是说 ...

  5. Ubuntu16.04下的modules模块编译加载

    一.首先编写对应的驱动程序的相关内容:(最简单的hello.c程序) #include<linux/init.h> #include<linux/module.h> MODUL ...

  6. python sys.modules模块

    sys.modules是一个全局字典,该字典是python启动后就加载在内存中.每当程序员导入新的模块,sys.modules都将记录这些模块.字典sys.modules对于加载模块起到了缓冲的作用. ...

  7. yii2 modules模块配置指南

    在Yii2 中模块是可以无限级嵌套的,也就是说,模块可以包含另一个包含模块的模块,我们称前者为父模块,后者为子模块, 子模块必须在父模块的yiibaseModule::modules属性中申明,例如: ...

  8. typescript - 7.模块

    我们可以把一些公共的功能单独抽离成一个文件作为一个模块. 模块里面的变量 函数 类等默认是私有的,如果我们要在外部访问模块里面的数据(变量.函数.类), 我们需要通过export暴露模块里面的数据(变 ...

  9. TypeScript入门九:TypeScript的模块

    关于TypeScript模块的基本使用方法 Ts的模块化语法与ES6的语法基本是一致(关于一些细节特性没有测试,请各自自行测试),然后再由tsconfig.json的module字段来描述转码类型,具 ...

随机推荐

  1. c++虚函数注意事项

    >在基类方法声明中使用关键字virtual,可以使该方法在基类及所有的派生类中是虚的 >如果使用指向对象的引用或指针来调用虚方法,程序将使用对象类型定义的方法,而不使用为引用或指针类型定义 ...

  2. 深入理解OOP(四): 多态和继承(抽象类)

    在本文中,我们讨论OOP中的热点之一:抽象类.抽象类在各个编程语言中概念是一致的,但是C#稍微有些不一样.本文中我们会通过代码来实现抽象类,并一一进行解析. 深入理解OOP(一):多态和继承(初期绑定 ...

  3. 单从Advice(通知)实现AOP

    如果你在实际开发中没感觉到OOP的一些缺陷,就不要往下看了! 如果你不了解AOP,或类似AOP的思路,请先去了解一下AOP相关的认识. 如果你是概念党,或是经验党,或是从众党,也请不要看了! 我实现的 ...

  4. Windows Server+AMD GPU+HDMI时_黑边_不铺满问题的解决办法

    HDMI接显示器或电视,有黑边或者被放大了是个很常见的问题,显卡设置界面里改下Scale或者Overscan/Underscan就行,可问题是WindowsServer版的CCC没有控制颜色对比度和缩 ...

  5. 三言两语聊Python模块–文档测试模块doctest

    doctest是属于测试模块里的一种,对注释文档里的示例进行检测. 给出一个例子: splitter.pydef split(line, types=None, delimiter=None): &q ...

  6. Sql视图创建语句

    create view [dbo].[AllUsers] as select u.UserId, u.Firstname, u.Lastname, u.ts, am.Email, au.UserNam ...

  7. [BZOJ1193][HNOI2006]马步距离(贪心+dfs)

    题目:http://www.lydsy.com:808/JudgeOnline/problem.php?id=1193 分析: 首先小范围可以直接暴力.(其实只要用上题目中的表就行了) 如果范围比较大 ...

  8. C语言函数sscanf()的用法

    从文件读取数据是一件很麻烦的事,所幸有sscanf()函数. C语言函数sscanf()的用法 sscanf() - 从一个字符串中读进与指定格式相符的数据. 函数原型: int sscanf( st ...

  9. ipython又一方便的调试和应用工具!!!

    控制台下://ipython 命令丰富 比如:ls 显示目录  ipython --pylab %run -p *.py quit关闭     示例: In []: %run -p test.py H ...

  10. Linux 使用 iptables屏蔽IP段

    netfilter/iptables IP 信息包过滤系统是一种功能强大的工具,可用于添加.编辑和除去规则,这些规则是在做信息包过滤决定时,防火墙所遵循和组成的规则.这些规则存储在专用的信息包过滤表中 ...