转载:《TypeScript 中文入门教程》 7、模块
版权
文章转载自:https://github.com/zhongsp
建议您直接跳转到上面的网址查看最新版本。
关于术语的一点说明: 请务必注意一点,TypeScript 1.5里术语名已经发生了变化。 “内部模块”现在称做“命名空间”。 “外部模块”现在则简称为“模块”,这是为了与ECMAScript 2015里的术语保持一致,(也就是说
module X {
相当于现在推荐的写法namespace X {
)。
介绍
关于术语的一点说明: 请务必注意一点,TypeScript 1.5里术语名已经发生了变化。 “内部模块”现在称做“命名空间”。 “外部模块”现在则简称为“模块”,这是为了与ECMAScript 2015里的术语保持一致,(也就是说
module X {
相当于现在推荐的写法namespace X {
)。
介绍
从ECMAScript 2015开始,JavaScript引入了模块的概念。TypeScript也沿用这个概念。
模块在其自身的作用域里执行,而不是在全局作用域里;这意味着定义在一个模块里的变量,函数,类等等在模块外部是不可见的,除非你明确地使用export
形式之一导出它们。 相反,如果想使用其它模块导出的变量,函数,类,接口等的时候,你必须要导入它们,可以使用import
形式之一。
模块是自声明的;两个模块之间的关系是通过在文件级别上使用imports和exports建立的。
模块使用模块加载器去导入其它的模块。 在运行时,模块加载器的作用是在执行此模块代码前去查找并执行这个模块的所有依赖。 大家最熟知的JavaScript模块加载器是服务于Node.js的CommonJS和服务于Web应用的Require.js。
TypeScript与ECMAScript 2015一样,任何包含顶级import
或者export
的文件都被当成一个模块。
导出
导出声明
任何声明(比如变量,函数,类,类型别名或接口)都能够通过添加export
关键字来导出。
Validation.ts
export interface StringValidator {
isAcceptable(s: string): boolean;
}
ZipCodeValidator.ts
export const numberRegexp = /^[0-9]+$/;
export class ZipCodeValidator implements StringValidator {
isAcceptable(s: string) {
return s.length === 5 && numberRegexp.test(s);
}
}
导出语句
导出语句很便利,因为我们可能需要对导出的部分重命名,所以上面的例子可以这样改写:
class ZipCodeValidator implements StringValidator {
isAcceptable(s: string) {
return s.length === 5 && numberRegexp.test(s);
}
}
export { ZipCodeValidator };
export { ZipCodeValidator as mainValidator };
重新导出
我们经常会去扩展其它模块,并且只导出那个模块的部分内容。 重新导出功能并不会在当前模块导入那个模块或定义一个新的局部变量。
ParseIntBasedZipCodeValidator.ts
export class ParseIntBasedZipCodeValidator {
isAcceptable(s: string) {
return s.length === 5 && parseInt(s).toString() === s;
}
}
// 导出原先的验证器但做了重命名
export {ZipCodeValidator as RegExpBasedZipCodeValidator} from "./ZipCodeValidator";
或者一个模块可以包裹多个模块,并把他们导出的内容联合在一起通过语法:export * from "module"
。
AllValidators.ts
export * from "./StringValidator"; // exports interface StringValidator
export * from "./LettersOnlyValidator"; // exports class LettersOnlyValidator
export * from "./ZipCodeValidator"; // exports class ZipCodeValidator
导入
模块的导入操作与导出一样简单。 可以使用以下import
形式之一来导入其它模块中的导出内容。
导入一个模块中的某个导出内容
import { ZipCodeValidator } from "./ZipCodeValidator";
var myValidator = new ZipCodeValidator();
可以对导入内容重命名
import { ZipCodeValidator as ZCV } from "./ZipCodeValidator";
var myValidator = new ZCV();
将整个模块导入到一个变量,并通过它来访问模块的导出部分
import * as validator from "./ZipCodeValidator";
var myValidator = new validator.ZipCodeValidator();
具有副作用的导入模块
尽管不推荐这么做,一些模块会设置一些全局状态供其它模块使用。 这些模块可能没有任何的导出或用户根本就不关注它的导出。 使用下面的方法来导入这类模块:
import "./my-module.js";
默认导出
每个模块都可以有一个default
导出。 默认导出使用default
关键字标记;并且一个模块只能够有一个default
导出。 需要使用一种特殊的导入形式来导入default
导出。
default
导出十分便利。 比如,像JQuery这样的类库可能有一个默认导出jQuery
或$
,并且我们基本上也会使用同样的名字jQuery
或$
导出JQuery。
JQuery.d.ts
declare var $: JQuery;
export default $;
App.ts
import $ from "JQuery";
$("button.continue").html( "Next Step..." );
类和函数声明可以直接被标记为默认导出。 标记为默认导出的类和函数的名字是可以省略的。
ZipCodeValidator.ts
export default class ZipCodeValidator {
static numberRegexp = /^[0-9]+$/;
isAcceptable(s: string) {
return s.length === 5 && ZipCodeValidator.numberRegexp.test(s);
}
}
Test.ts
import validator from "./ZipCodeValidator";
var validator = new validator();
或者
StaticZipCodeValidator.ts
const numberRegexp = /^[0-9]+$/;
export default function (s: string) {
return s.length === 5 && numberRegexp.test(s);
}
Test.ts
import validate from "./StaticZipCodeValidator";
var strings = ["Hello", "98052", "101"];
// Use function validate
strings.forEach(s => {
console.log(`"${s}" ${validate(s) ? " matches" : " does not match"}`);
});
default
导出也可以是一个值
OneTwoThree.ts
export default "123";
Log.ts
import num from "./OneTwoThree";
console.log(num); // "123"
export =
和 import = require()
CommonJS和AMD都有一个exports
对象的概念,它包含了一个模块的所有导出内容。
它们也支持把exports
替换为一个自定义对象。 默认导出就好比这样一个功能;然而,它们却并不相互兼容。 TypeScript模块支持export =
语法,以配合传统的CommonJS和AMD的工作流。
export =
语法定义一个模块的导出对象。 它可以是类,接口,命名空间,函数或枚举。
若要导入一个使用了export =
的模块时,必须使用TypeScript提供的特定语法import var = require("module")
。
ZipCodeValidator.ts
var numberRegexp = /^[0-9]+$/;
class ZipCodeValidator {
isAcceptable(s: string) {
return s.length === 5 && numberRegexp.test(s);
}
}
export = ZipCodeValidator;
Test.ts
import zip = require("./ZipCodeValidator");
// Some samples to try
var strings = ["Hello", "98052", "101"];
// Validators to use
var validator = new zip.ZipCodeValidator();
// Show whether each string passed each validator
strings.forEach(s => {
console.log(`"${ s }" - ${ validator.isAcceptable(s) ? "matches" : "does not match" }`);
});
生成模块代码
根据编译时指定的模块目标参数,编译器会生成相应的供Node.js (CommonJS),Require.js (AMD),isomorphic (UMD), SystemJS或ECMAScript 2015 native modules (ES6)模块加载系统使用的代码。 想要了解生成代码中define
,require
和 register
的意义,请参考相应模块加载器的文档。
下面的例子说明了导入导出语句里使用的名字是怎么转换为相应的模块加载器代码的。
SimpleModule.ts
import m = require("mod");
export var t = m.something + 1;
AMD / RequireJS SimpleModule.js
define(["require", "exports", "./mod"], function (require, exports, mod_1) {
exports.t = mod_1.something + 1;
});
CommonJS / Node SimpleModule.js
var mod_1 = require("./mod");
exports.t = mod_1.something + 1;
UMD SimpleModule.js
(function (factory) {
if (typeof module === "object" && typeof module.exports === "object") {
var v = factory(require, exports); if (v !== undefined) module.exports = v;
}
else if (typeof define === "function" && define.amd) {
define(["require", "exports", "./mod"], factory);
}
})(function (require, exports) {
var mod_1 = require("./mod");
exports.t = mod_1.something + 1;
});
System SimpleModule.js
System.register(["./mod"], function(exports_1) {
var mod_1;
var t;
return {
setters:[
function (mod_1_1) {
mod_1 = mod_1_1;
}],
execute: function() {
exports_1("t", t = mod_1.something + 1);
}
}
});
Native ECMAScript 2015 modules SimpleModule.js
import { something } from "./mod";
export var t = something + 1;
简单示例
下面我们来整理一下前面的验证器实现,每个模块只有一个命名的导出。
为了编译,我们必需要在命令行上指定一个模块目标。对于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 { StringValidator } from "./Validation";
const lettersRegexp = /^[A-Za-z]+$/;
export class LettersOnlyValidator implements StringValidator {
isAcceptable(s: string) {
return lettersRegexp.test(s);
}
}
ZipCodeValidator.ts
import { StringValidator } from "./Validation";
const numberRegexp = /^[0-9]+$/;
export class ZipCodeValidator implements StringValidator {
isAcceptable(s: string) {
return s.length === 5 && numberRegexp.test(s);
}
}
Test.ts
import { StringValidator } from "./Validation";
import { ZipCodeValidator } from "./ZipCodeValidator";
import { LettersOnlyValidator } from "./LettersOnlyValidator";
// Some samples to try
let strings = ["Hello", "98052", "101"];
// Validators to use
let 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 }`);
}
});
可选的模块加载和其它高级加载场景
有时候,你只想在某种条件下才加载某个模块。 在TypeScript里,使用下面的方式来实现它和其它的高级加载场景,我们可以直接调用模块加载器并且可以保证类型完全。
编译器会检测是否每个模块都会在生成的JavaScript中用到。 如果一个模块标识符只在类型注解部分使用,并且完全没有在表达式中使用时,就不会生成require
这个模块的代码。 省略掉没有用到的引用对性能提升是很有益的,并同时提供了选择性加载模块的能力。
这种模式的核心是import id = require("...")
语句可以让我们访问模块导出的类型。 模块加载器会被动态调用(通过require
),就像下面if
代码块里那样。 它利用了省略引用的优化,所以模块只在被需要时加载。 为了让这个模块工作,一定要注意import
定义的标识符只能在表示类型处使用(不能在会转换成JavaScript的地方)。
为了确保类型安全性,我们可以使用typeof
关键字。 typeof
关键字,当在表示类型的地方使用时,会得出一个类型值,这里就表示模块的类型。
示例:Node.js里的动态模块加载
declare function require(moduleName: string): any;
import { ZipCodeValidator as Zip } from "./ZipCodeValidator";
if (needZipValidation) {
var ZipCodeValidator: typeof Zip = require("./ZipCodeValidator");
var validator = new ZipCodeValidator();
if (validator.isAcceptable("...")) { /* ... */ }
}
示例:require.js里的动态模块加载
declare function require(moduleNames: string[], onLoad: (...args: any[]) => void): void;
import { ZipCodeValidator as Zip } from "./ZipCodeValidator";
if (needZipValidation) {
require(["./ZipCodeValidator"], (ZipCodeValidator: typeof Zip) => {
var validator = new ZipCodeValidator();
if (validator.isAcceptable("...")) { /* ... */ }
});
}
示例:System.js里的动态模块加载
declare var System: any;
import { ZipCodeValidator as Zip } from "./ZipCodeValidator";
if (needZipValidation) {
System.import("./ZipCodeValidator").then((ZipCodeValidator: typeof Zip) => {
var x = new ZipCodeValidator();
if (x.isAcceptable("...")) { /* ... */ }
});
}
使用其它的JavaScript库
为了描述不是用TypeScript编写的类库的类型,我们需要声明类库导出的API。
我们叫它声明因为它不是外部程序的具体实现。 通常会在.d.ts
里写这些定义。 如果你熟悉C/C++,你可以把它们当做.h
文件。 让我们看一些例子。
外部模块
在Node.js里大部分工作是通过加载一个或多个模块实现的。 我们可以使用顶级的export
声明来为每个模块都定义一个.d.ts
文件,但最好还是写在一个大的.d.ts
文件里。 我们使用与构造一个外部命名空间相似的方法,但是这里使用module
关键字并且把名字用引号括起来,方便之后import
。 例如:
node.d.ts (simplified excerpt)
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 * as URL from "url";
var myUrl = URL.parse("http://www.typescriptlang.org");
创建模块结构指导
尽可能地在顶层导出
用户应该更容易地使用你模块导出的内容。 嵌套层次过多会变得难以处理,因此仔细考虑一下如何组织你的代码。
从你的模块中导出一个命名空间就是一个增加嵌套的例子。 虽然命名空间有时候有它们的用处,在使用模块的时候它们额外地增加了一层。 这对用户来说是很不便的并且通常是多余的。
导出类的静态方法也有同样的问题 - 这个类本身就增加了一层嵌套。 除非它能方便表述或便于清晰使用,否则请考虑直接导出一个辅助方法。
如果仅导出单个 class
或 function
,使用 export default
就像“在顶层上导出”帮助减少用户使用的难度,一个默认的导出也能起到这个效果。 如果一个模块就是为了导出特定的内容,那么你应该考虑使用一个默认导出。 这会令模块的导入和使用变得些许简单。 比如:
MyClass.ts
export default class SomeType {
constructor() { ... }
}
MyFunc.ts
export default function getThing() { return 'thing'; }
Consumer.ts
import t from "./MyClass";
import f from "./MyFunc";
var x = new t();
console.log(f());
对用户来说这是最理想的。他们可以随意命名导入模块的类型(本例为t
)并且不需要多余的(.)来找到相关对象。
如果要导出多个对象,把它们放在顶层里导出
MyThings.ts
export class SomeType { /* ... */ }
export function someFunc() { /* ... */ }
相反地,当导入的时候:
明确地列出导入的名字
Consumer.ts
import { SomeType, SomeFunc } from "./MyThings";
var x = new SomeType();
var y = someFunc();
使用命名空间导入模式当你要导出大量内容的时候
MyLargeModule.ts
export class Dog { ... }
export class Cat { ... }
export class Tree { ... }
export class Flower { ... }
Consumer.ts
import * as myLargeModule from "./MyLargeModule.ts";
var x = new myLargeModule.Dog();
使用重新导出进行扩展
你可能经常需要去扩展一个模块的功能。 JS里常用的一个模式是JQuery那样去扩展原对象。 如我们之前提到的,模块不会像全局命名空间对象那样去合并。 推荐的方案是不要去改变原来的对象,而是导出一个新的实体来提供新的功能。
假设Calculator.ts
模块里定义了一个简单的计算器实现。 这个模块同样提供了一个辅助函数来测试计算器的功能,通过传入一系列输入的字符串并在最后给出结果。
Calculator.ts
export class Calculator {
private current = 0;
private memory = 0;
private operator: string;
protected processDigit(digit: string, currentValue: number) {
if (digit >= "0" && digit <= "9") {
return currentValue * 10 + (digit.charCodeAt(0) - "0".charCodeAt(0));
}
}
protected processOperator(operator: string) {
if (["+", "-", "*", "/"].indexOf(operator) >= 0) {
return operator;
}
}
protected evaluateOperator(operator: string, left: number, right: number): number {
switch (this.operator) {
case "+": return left + right;
case "-": return left - right;
case "*": return left * right;
case "/": return left / right;
}
}
private evaluate() {
if (this.operator) {
this.memory = this.evaluateOperator(this.operator, this.memory, this.current);
}
else {
this.memory = this.current;
}
this.current = 0;
}
public handelChar(char: string) {
if (char === "=") {
this.evaluate();
return;
}
else {
let value = this.processDigit(char, this.current);
if (value !== undefined) {
this.current = value;
return;
}
else {
let value = this.processOperator(char);
if (value !== undefined) {
this.evaluate();
this.operator = value;
return;
}
}
}
throw new Error(`Unsupported input: '${char}'`);
}
public getResult() {
return this.memory;
}
}
export function test(c: Calculator, input: string) {
for (let i = 0; i < input.length; i++) {
c.handelChar(input[i]);
}
console.log(`result of '${input}' is '${c.getResult()}'`);
}
这是使用导出的test
函数来测试计算器。
TestCalculator.ts
import { Calculator, test } from "./Calculator";
var c = new Calculator();
test(c, "1+2*33/11="); // prints 9
现在扩展它,添加支持输入其它进制(十进制以外),让我们来创建ProgrammerCalculator.ts
。
ProgrammerCalculator.ts
import { Calculator } from "./Calculator";
class ProgrammerCalculator extends Calculator {
static digits = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"];
constructor(public base: number) {
super();
if (base <= 0 || base > ProgrammerCalculator.digits.length) {
throw new Error("base has to be within 0 to 16 inclusive.");
}
}
protected processDigit(digit: string, currentValue: number) {
if (ProgrammerCalculator.digits.indexOf(digit) >= 0) {
return currentValue * this.base + ProgrammerCalculator.digits.indexOf(digit);
}
}
}
// Export the new extended calculator as Calculator
export { ProgrammerCalculator as Calculator };
// Also, export the helper function
export { test } from "./Calculator";
新的ProgrammerCalculator
模块导出的API与原先的Calculator
模块很相似,但却没有改变原模块里的对象。 下面是测试ProgrammerCalculator类的代码:
TestProgrammerCalculator.ts
import { Calculator, test } from "./ProgrammerCalculator";
var c = new Calculator(2);
test(c, "001+010="); // prints 3
模块里不要使用命名空间
当初次进入基于模块的开发模式时,可能总会控制不住要将导出包裹在一个命名空间里。 模块具有其自己的作用域,并且只有导出的声明才会在模块外部可见。 记住这点,命名空间在使用模块时几乎没什么价值。
在组织方面,命名空间对于在全局作用域内对逻辑上相关的对象和类型进行分组是很便利的。 例如,在C#里,你会从System.Collections
里找到所有集合的类型。 通过将类型有层次地组织在命名空间里,可以方便用户找到与使用那些类型。 然而,模块本身已经存在于文件系统之中,这是必须的。 我们必须通过路径和文件名找到它们,这已经提供了一种逻辑上的组织形式。 我们可以创建/collections/generic/
文件夹,把相应模块放在这里面。
命名空间对解决全局作用域里命名冲突来说是很重要的。 比如,你可以有一个My.Application.Customer.AddForm
和My.Application.Order.AddForm
-- 两个类型的名字相同,但命名空间不同。 然而,这对于模块来说却不是一个问题。 在一个模块里,没有理由两个对象拥有同一个名字。 从模块的使用角度来说,使用者会挑出他们用来引用模块的名字,所以也没有理由发生重名的情况。
更多关于模块和命名空间的资料查看命名空间和模块
危险信号
以下均为模块结构上的危险信号。重新检查以确保你没有在对模块使用命名空间:
- 文件的顶层声明是
export namespace Foo { ... }
(删除Foo
并把所有内容向上层移动一层) - 文件只有一个
export class
或export function
(考虑使用export default
) - 多个文件的顶层具有同样的
export namespace Foo {
(不要以为这些会合并到一个Foo
中!)
模块是自声明的;两个模块之间的关系是通过在文件级别上使用imports和exports建立的。
模块使用模块加载器去导入其它的模块。 在运行时,模块加载器的作用是在执行此模块代码前去查找并执行这个模块的所有依赖。 大家最熟知的JavaScript模块加载器是服务于Node.js的CommonJS和服务于Web应用的Require.js。
TypeScript与ECMAScript 2015一样,任何包含顶级import
或者export
的文件都被当成一个模块。
导出
导出声明
任何声明(比如变量,函数,类,类型别名或接口)都能够通过添加export
关键字来导出。
Validation.ts
export interface StringValidator {
isAcceptable(s: string): boolean;
}
ZipCodeValidator.ts
export const numberRegexp = /^[0-9]+$/;
export class ZipCodeValidator implements StringValidator {
isAcceptable(s: string) {
return s.length === 5 && numberRegexp.test(s);
}
}
导出语句
导出语句很便利,因为我们可能需要对导出的部分重命名,所以上面的例子可以这样改写:
class ZipCodeValidator implements StringValidator {
isAcceptable(s: string) {
return s.length === 5 && numberRegexp.test(s);
}
}
export { ZipCodeValidator };
export { ZipCodeValidator as mainValidator };
重新导出
我们经常会去扩展其它模块,并且只导出那个模块的部分内容。 重新导出功能并不会在当前模块导入那个模块或定义一个新的局部变量。
ParseIntBasedZipCodeValidator.ts
export class ParseIntBasedZipCodeValidator {
isAcceptable(s: string) {
return s.length === 5 && parseInt(s).toString() === s;
}
}
// 导出原先的验证器但做了重命名
export {ZipCodeValidator as RegExpBasedZipCodeValidator} from "./ZipCodeValidator";
或者一个模块可以包裹多个模块,并把他们导出的内容联合在一起通过语法:export * from "module"
。
AllValidators.ts
export * from "./StringValidator"; // exports interface StringValidator
export * from "./LettersOnlyValidator"; // exports class LettersOnlyValidator
export * from "./ZipCodeValidator"; // exports class ZipCodeValidator
导入
模块的导入操作与导出一样简单。 可以使用以下import
形式之一来导入其它模块中的导出内容。
导入一个模块中的某个导出内容
import { ZipCodeValidator } from "./ZipCodeValidator";
var myValidator = new ZipCodeValidator();
可以对导入内容重命名
import { ZipCodeValidator as ZCV } from "./ZipCodeValidator";
var myValidator = new ZCV();
将整个模块导入到一个变量,并通过它来访问模块的导出部分
import * as validator from "./ZipCodeValidator";
var myValidator = new validator.ZipCodeValidator();
具有副作用的导入模块
尽管不推荐这么做,一些模块会设置一些全局状态供其它模块使用。 这些模块可能没有任何的导出或用户根本就不关注它的导出。 使用下面的方法来导入这类模块:
import "./my-module.js";
默认导出
每个模块都可以有一个default
导出。 默认导出使用default
关键字标记;并且一个模块只能够有一个default
导出。 需要使用一种特殊的导入形式来导入default
导出。
default
导出十分便利。 比如,像JQuery这样的类库可能有一个默认导出jQuery
或$
,并且我们基本上也会使用同样的名字jQuery
或$
导出JQuery。
JQuery.d.ts
declare var $: JQuery;
export default $;
App.ts
import $ from "JQuery";
$("button.continue").html( "Next Step..." );
类和函数声明可以直接被标记为默认导出。 标记为默认导出的类和函数的名字是可以省略的。
ZipCodeValidator.ts
export default class ZipCodeValidator {
static numberRegexp = /^[0-9]+$/;
isAcceptable(s: string) {
return s.length === 5 && ZipCodeValidator.numberRegexp.test(s);
}
}
Test.ts
import validator from "./ZipCodeValidator";
var validator = new validator();
或者
StaticZipCodeValidator.ts
const numberRegexp = /^[0-9]+$/;
export default function (s: string) {
return s.length === 5 && numberRegexp.test(s);
}
Test.ts
import validate from "./StaticZipCodeValidator";
var strings = ["Hello", "98052", "101"];
// Use function validate
strings.forEach(s => {
console.log(`"${s}" ${validate(s) ? " matches" : " does not match"}`);
});
default
导出也可以是一个值
OneTwoThree.ts
export default "123";
Log.ts
import num from "./OneTwoThree";
console.log(num); // "123"
export =
和 import = require()
CommonJS和AMD都有一个exports
对象的概念,它包含了一个模块的所有导出内容。
它们也支持把exports
替换为一个自定义对象。 默认导出就好比这样一个功能;然而,它们却并不相互兼容。 TypeScript模块支持export =
语法,以配合传统的CommonJS和AMD的工作流。
export =
语法定义一个模块的导出对象。 它可以是类,接口,命名空间,函数或枚举。
若要导入一个使用了export =
的模块时,必须使用TypeScript提供的特定语法import var = require("module")
。
ZipCodeValidator.ts
var numberRegexp = /^[0-9]+$/;
class ZipCodeValidator {
isAcceptable(s: string) {
return s.length === 5 && numberRegexp.test(s);
}
}
export = ZipCodeValidator;
Test.ts
import zip = require("./ZipCodeValidator");
// Some samples to try
var strings = ["Hello", "98052", "101"];
// Validators to use
var validator = new zip.ZipCodeValidator();
// Show whether each string passed each validator
strings.forEach(s => {
console.log(`"${ s }" - ${ validator.isAcceptable(s) ? "matches" : "does not match" }`);
});
生成模块代码
根据编译时指定的模块目标参数,编译器会生成相应的供Node.js (CommonJS),Require.js (AMD),isomorphic (UMD), SystemJS或ECMAScript 2015 native modules (ES6)模块加载系统使用的代码。 想要了解生成代码中define
,require
和 register
的意义,请参考相应模块加载器的文档。
下面的例子说明了导入导出语句里使用的名字是怎么转换为相应的模块加载器代码的。
SimpleModule.ts
import m = require("mod");
export var t = m.something + 1;
AMD / RequireJS SimpleModule.js
define(["require", "exports", "./mod"], function (require, exports, mod_1) {
exports.t = mod_1.something + 1;
});
CommonJS / Node SimpleModule.js
var mod_1 = require("./mod");
exports.t = mod_1.something + 1;
UMD SimpleModule.js
(function (factory) {
if (typeof module === "object" && typeof module.exports === "object") {
var v = factory(require, exports); if (v !== undefined) module.exports = v;
}
else if (typeof define === "function" && define.amd) {
define(["require", "exports", "./mod"], factory);
}
})(function (require, exports) {
var mod_1 = require("./mod");
exports.t = mod_1.something + 1;
});
System SimpleModule.js
System.register(["./mod"], function(exports_1) {
var mod_1;
var t;
return {
setters:[
function (mod_1_1) {
mod_1 = mod_1_1;
}],
execute: function() {
exports_1("t", t = mod_1.something + 1);
}
}
});
Native ECMAScript 2015 modules SimpleModule.js
import { something } from "./mod";
export var t = something + 1;
简单示例
下面我们来整理一下前面的验证器实现,每个模块只有一个命名的导出。
为了编译,我们必需要在命令行上指定一个模块目标。对于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 { StringValidator } from "./Validation";
const lettersRegexp = /^[A-Za-z]+$/;
export class LettersOnlyValidator implements StringValidator {
isAcceptable(s: string) {
return lettersRegexp.test(s);
}
}
ZipCodeValidator.ts
import { StringValidator } from "./Validation";
const numberRegexp = /^[0-9]+$/;
export class ZipCodeValidator implements StringValidator {
isAcceptable(s: string) {
return s.length === 5 && numberRegexp.test(s);
}
}
Test.ts
import { StringValidator } from "./Validation";
import { ZipCodeValidator } from "./ZipCodeValidator";
import { LettersOnlyValidator } from "./LettersOnlyValidator";
// Some samples to try
let strings = ["Hello", "98052", "101"];
// Validators to use
let 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 }`);
}
});
可选的模块加载和其它高级加载场景
有时候,你只想在某种条件下才加载某个模块。 在TypeScript里,使用下面的方式来实现它和其它的高级加载场景,我们可以直接调用模块加载器并且可以保证类型完全。
编译器会检测是否每个模块都会在生成的JavaScript中用到。 如果一个模块标识符只在类型注解部分使用,并且完全没有在表达式中使用时,就不会生成require
这个模块的代码。 省略掉没有用到的引用对性能提升是很有益的,并同时提供了选择性加载模块的能力。
这种模式的核心是import id = require("...")
语句可以让我们访问模块导出的类型。 模块加载器会被动态调用(通过require
),就像下面if
代码块里那样。 它利用了省略引用的优化,所以模块只在被需要时加载。 为了让这个模块工作,一定要注意import
定义的标识符只能在表示类型处使用(不能在会转换成JavaScript的地方)。
为了确保类型安全性,我们可以使用typeof
关键字。 typeof
关键字,当在表示类型的地方使用时,会得出一个类型值,这里就表示模块的类型。
示例:Node.js里的动态模块加载
declare function require(moduleName: string): any;
import { ZipCodeValidator as Zip } from "./ZipCodeValidator";
if (needZipValidation) {
var ZipCodeValidator: typeof Zip = require("./ZipCodeValidator");
var validator = new ZipCodeValidator();
if (validator.isAcceptable("...")) { /* ... */ }
}
示例:require.js里的动态模块加载
declare function require(moduleNames: string[], onLoad: (...args: any[]) => void): void;
import { ZipCodeValidator as Zip } from "./ZipCodeValidator";
if (needZipValidation) {
require(["./ZipCodeValidator"], (ZipCodeValidator: typeof Zip) => {
var validator = new ZipCodeValidator();
if (validator.isAcceptable("...")) { /* ... */ }
});
}
示例:System.js里的动态模块加载
declare var System: any;
import { ZipCodeValidator as Zip } from "./ZipCodeValidator";
if (needZipValidation) {
System.import("./ZipCodeValidator").then((ZipCodeValidator: typeof Zip) => {
var x = new ZipCodeValidator();
if (x.isAcceptable("...")) { /* ... */ }
});
}
使用其它的JavaScript库
为了描述不是用TypeScript编写的类库的类型,我们需要声明类库导出的API。
我们叫它声明因为它不是外部程序的具体实现。 通常会在.d.ts
里写这些定义。 如果你熟悉C/C++,你可以把它们当做.h
文件。 让我们看一些例子。
外部模块
在Node.js里大部分工作是通过加载一个或多个模块实现的。 我们可以使用顶级的export
声明来为每个模块都定义一个.d.ts
文件,但最好还是写在一个大的.d.ts
文件里。 我们使用与构造一个外部命名空间相似的方法,但是这里使用module
关键字并且把名字用引号括起来,方便之后import
。 例如:
node.d.ts (simplified excerpt)
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 * as URL from "url";
var myUrl = URL.parse("http://www.typescriptlang.org");
创建模块结构指导
尽可能地在顶层导出
用户应该更容易地使用你模块导出的内容。 嵌套层次过多会变得难以处理,因此仔细考虑一下如何组织你的代码。
从你的模块中导出一个命名空间就是一个增加嵌套的例子。 虽然命名空间有时候有它们的用处,在使用模块的时候它们额外地增加了一层。 这对用户来说是很不便的并且通常是多余的。
导出类的静态方法也有同样的问题 - 这个类本身就增加了一层嵌套。 除非它能方便表述或便于清晰使用,否则请考虑直接导出一个辅助方法。
如果仅导出单个 class
或 function
,使用 export default
就像“在顶层上导出”帮助减少用户使用的难度,一个默认的导出也能起到这个效果。 如果一个模块就是为了导出特定的内容,那么你应该考虑使用一个默认导出。 这会令模块的导入和使用变得些许简单。 比如:
MyClass.ts
export default class SomeType {
constructor() { ... }
}
MyFunc.ts
export default function getThing() { return 'thing'; }
Consumer.ts
import t from "./MyClass";
import f from "./MyFunc";
var x = new t();
console.log(f());
对用户来说这是最理想的。他们可以随意命名导入模块的类型(本例为t
)并且不需要多余的(.)来找到相关对象。
如果要导出多个对象,把它们放在顶层里导出
MyThings.ts
export class SomeType { /* ... */ }
export function someFunc() { /* ... */ }
相反地,当导入的时候:
明确地列出导入的名字
Consumer.ts
import { SomeType, SomeFunc } from "./MyThings";
var x = new SomeType();
var y = someFunc();
使用命名空间导入模式当你要导出大量内容的时候
MyLargeModule.ts
export class Dog { ... }
export class Cat { ... }
export class Tree { ... }
export class Flower { ... }
Consumer.ts
import * as myLargeModule from "./MyLargeModule.ts";
var x = new myLargeModule.Dog();
使用重新导出进行扩展
你可能经常需要去扩展一个模块的功能。 JS里常用的一个模式是JQuery那样去扩展原对象。 如我们之前提到的,模块不会像全局命名空间对象那样去合并。 推荐的方案是不要去改变原来的对象,而是导出一个新的实体来提供新的功能。
假设Calculator.ts
模块里定义了一个简单的计算器实现。 这个模块同样提供了一个辅助函数来测试计算器的功能,通过传入一系列输入的字符串并在最后给出结果。
Calculator.ts
export class Calculator {
private current = 0;
private memory = 0;
private operator: string;
protected processDigit(digit: string, currentValue: number) {
if (digit >= "0" && digit <= "9") {
return currentValue * 10 + (digit.charCodeAt(0) - "0".charCodeAt(0));
}
}
protected processOperator(operator: string) {
if (["+", "-", "*", "/"].indexOf(operator) >= 0) {
return operator;
}
}
protected evaluateOperator(operator: string, left: number, right: number): number {
switch (this.operator) {
case "+": return left + right;
case "-": return left - right;
case "*": return left * right;
case "/": return left / right;
}
}
private evaluate() {
if (this.operator) {
this.memory = this.evaluateOperator(this.operator, this.memory, this.current);
}
else {
this.memory = this.current;
}
this.current = 0;
}
public handelChar(char: string) {
if (char === "=") {
this.evaluate();
return;
}
else {
let value = this.processDigit(char, this.current);
if (value !== undefined) {
this.current = value;
return;
}
else {
let value = this.processOperator(char);
if (value !== undefined) {
this.evaluate();
this.operator = value;
return;
}
}
}
throw new Error(`Unsupported input: '${char}'`);
}
public getResult() {
return this.memory;
}
}
export function test(c: Calculator, input: string) {
for (let i = 0; i < input.length; i++) {
c.handelChar(input[i]);
}
console.log(`result of '${input}' is '${c.getResult()}'`);
}
这是使用导出的test
函数来测试计算器。
TestCalculator.ts
import { Calculator, test } from "./Calculator";
var c = new Calculator();
test(c, "1+2*33/11="); // prints 9
现在扩展它,添加支持输入其它进制(十进制以外),让我们来创建ProgrammerCalculator.ts
。
ProgrammerCalculator.ts
import { Calculator } from "./Calculator";
class ProgrammerCalculator extends Calculator {
static digits = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"];
constructor(public base: number) {
super();
if (base <= 0 || base > ProgrammerCalculator.digits.length) {
throw new Error("base has to be within 0 to 16 inclusive.");
}
}
protected processDigit(digit: string, currentValue: number) {
if (ProgrammerCalculator.digits.indexOf(digit) >= 0) {
return currentValue * this.base + ProgrammerCalculator.digits.indexOf(digit);
}
}
}
// Export the new extended calculator as Calculator
export { ProgrammerCalculator as Calculator };
// Also, export the helper function
export { test } from "./Calculator";
新的ProgrammerCalculator
模块导出的API与原先的Calculator
模块很相似,但却没有改变原模块里的对象。 下面是测试ProgrammerCalculator类的代码:
TestProgrammerCalculator.ts
import { Calculator, test } from "./ProgrammerCalculator";
var c = new Calculator(2);
test(c, "001+010="); // prints 3
模块里不要使用命名空间
当初次进入基于模块的开发模式时,可能总会控制不住要将导出包裹在一个命名空间里。 模块具有其自己的作用域,并且只有导出的声明才会在模块外部可见。 记住这点,命名空间在使用模块时几乎没什么价值。
在组织方面,命名空间对于在全局作用域内对逻辑上相关的对象和类型进行分组是很便利的。 例如,在C#里,你会从System.Collections
里找到所有集合的类型。 通过将类型有层次地组织在命名空间里,可以方便用户找到与使用那些类型。 然而,模块本身已经存在于文件系统之中,这是必须的。 我们必须通过路径和文件名找到它们,这已经提供了一种逻辑上的组织形式。 我们可以创建/collections/generic/
文件夹,把相应模块放在这里面。
命名空间对解决全局作用域里命名冲突来说是很重要的。 比如,你可以有一个My.Application.Customer.AddForm
和My.Application.Order.AddForm
-- 两个类型的名字相同,但命名空间不同。 然而,这对于模块来说却不是一个问题。 在一个模块里,没有理由两个对象拥有同一个名字。 从模块的使用角度来说,使用者会挑出他们用来引用模块的名字,所以也没有理由发生重名的情况。
更多关于模块和命名空间的资料查看命名空间和模块
危险信号
以下均为模块结构上的危险信号。重新检查以确保你没有在对模块使用命名空间:
- 文件的顶层声明是
export namespace Foo { ... }
(删除Foo
并把所有内容向上层移动一层) - 文件只有一个
export class
或export function
(考虑使用export default
) - 多个文件的顶层具有同样的
export namespace Foo {
(不要以为这些会合并到一个Foo
中!)
转载:《TypeScript 中文入门教程》 7、模块的更多相关文章
- 转载:TypeScript 简介与《TypeScript 中文入门教程》
简介 TypeScript是一种由微软开发的自由和开源的编程语言.它是JavaScript的一个超集,而且本质上向这个语言添加了可选的静态类型和基于类的面向对象编程.安德斯·海尔斯伯格,C#的首席架构 ...
- 转载:《TypeScript 中文入门教程》
缘由 事情是这样的,我想搜索 TypeScript 中文教程,结果在 https://www.baidu.com , https://cn.bing.com ,上都找不到官方的翻译,也没有一个像样的翻 ...
- 【转】TypeScript中文入门教程
目录 虽然我是转载的,但看在Copy这么多文章也是很幸苦的好吧,我罗列一个目录. 转载:<TypeScript 中文入门教程> 17.注解 (2015-12-03 11:36) 转载:&l ...
- 《TypeScript 中文入门教程》
转载:<TypeScript 中文入门教程> 17.注解 (2015-12-03 11:36) 转载:<TypeScript 中文入门教程> 16.Symbols (2015- ...
- 转载:《TypeScript 中文入门教程》 5、命名空间和模块
版权 文章转载自:https://github.com/zhongsp 建议您直接跳转到上面的网址查看最新版本. 关于术语的一点说明: 请务必注意一点,TypeScript 1.5里术语名已经发生了变 ...
- 转载:《TypeScript 中文入门教程》 14、输入.d.ts文件
版权 文章转载自:https://github.com/zhongsp 建议您直接跳转到上面的网址查看最新版本. 介绍 当使用外部JavaScript库或新的宿主API时,你需要一个声明文件(.d.t ...
- 转载:《TypeScript 中文入门教程》 11、声明合并
版权 文章转载自:https://github.com/zhongsp 建议您直接跳转到上面的网址查看最新版本. 介绍 TypeScript有一些独特的概念,有的是因为我们需要描述JavaScript ...
- 转载:《TypeScript 中文入门教程》 8、函数
版权 文章转载自:https://github.com/zhongsp 建议您直接跳转到上面的网址查看最新版本. 介绍 函数是JavaScript应用程序的基础. 它帮助你实现抽象层,模拟类,信息隐藏 ...
- 转载:《TypeScript 中文入门教程》 6、命名空间
版权 文章转载自:https://github.com/zhongsp 建议您直接跳转到上面的网址查看最新版本. 关于术语的一点说明: 请务必注意一点,TypeScript 1.5里术语名已经发生了变 ...
随机推荐
- zookeeper分布式锁实现
1.定义分布式锁接口 package com.ljq.lock; import java.util.concurrent.TimeUnit; public interface DistributedL ...
- jsp的 javascript中 嵌套 html 注释
看到公司的代码,我也是蛋疼了,各种乱. 还发现有很多的jsp的 javascript中 嵌套 html 注释, 这个可行? 我之前可是没用过. 后面查找各种资料发现,这个也是可行的,主要是为了兼容不支 ...
- Android—自定义Dialog
在 Android 日常的开发中,Dialog 使用是比较广泛的.无论是提示一个提示语,还是确认信息,还是有一定交互的(弹出验证码,输入账号密码登录等等)对话框. 而我们去看一下原生的对话框,虽然随着 ...
- Android笔记——数据库升级与降级
一.概述 SQLite是Android内置的一个很小的关系型数据库.SQLiteOpenHelper是一个用来辅助管理数据库创建和版本升级问题的抽象类.我们可以继承这个抽象类,实现它的一些方法来对数据 ...
- ORA-01652:无法通过128(在表空间temp中)扩展temp段 解决方法
ORA-01652:无法通过128(在表空间temp中)扩展temp段 解决方法 (2016-10-21 16:49:53) 今天在做一个查询的时候,报了一个"ORA-01652无法通过 ...
- php安装的一点点事 ---wampserver
安装wampserver后,需要配置一些文件 1. 首先修改httpd.conf <Directory /> Options FollowSymLinks AllowOverride No ...
- Sql Server系列:字符串函数
字符串函数用于对字符和二进制字符串进行各种操作,大多数字符串函数只能作用于char.nchar.varchar和nvarchar数据类型.字符串函数可以用在SELECT或者WHERE语句中. 1. A ...
- C#设计模式系列:模板方法模式(Template Method)
你去银行取款的时候,银行会给你一张取款单,这张取款单就是一个模板,它把公共的内容提取到模板中,只留下部分让用户来填写.在软件系统中,将多个类的共有内容提取到一个模板中的思想便是模板方法模式的思想. 模 ...
- Ubuntu 14.04 64bit 安装tensorflow(GPU版本)
本博客主要用于在Ubuntu14.04 64bit 操作系统上搭建google开源的深度学习框架tensorflow. 0.安装CUDA和cuDNN 如果要安装GPU版本的tensorflow,就必须 ...
- ASP.NET Core的配置(4):多样性的配置来源[中篇]
我们在本篇文章中会介绍三种针对物理文件的ConfiguationProvider,它们分别是针对JSON文件的JsonConfiguationProvider,针对XML文件的XmlConfiguat ...