關(guān)于術(shù)語的一點說明: 請務(wù)必注意一點,TypeScript 1.5里術(shù)語名已經(jīng)發(fā)生了變化。 “內(nèi)部模塊”現(xiàn)在稱做“命名空間”。 “外部模塊”現(xiàn)在則簡稱為“模塊”,這是為了與 ECMAScript 2015里的術(shù)語保持一致,(也就是說
module X {
相當(dāng)于現(xiàn)在推薦的寫法namespace X {
)。
從ECMAScript 2015開始,JavaScript引入了模塊的概念。TypeScript也沿用這個概念。
模塊在其自身的作用域里執(zhí)行,而不是在全局作用域里;這意味著定義在一個模塊里的變量,函數(shù),類等等在模塊外部是不可見的,除非你明確地使用export
形式之一導(dǎo)出它們。 相反,如果想使用其它模塊導(dǎo)出的變量,函數(shù),類,接口等的時候,你必須要導(dǎo)入它們,可以使用 import
形式之一。
模塊是自聲明的;兩個模塊之間的關(guān)系是通過在文件級別上使用imports和exports建立的。
模塊使用模塊加載器去導(dǎo)入其它的模塊。 在運行時,模塊加載器的作用是在執(zhí)行此模塊代碼前去查找并執(zhí)行這個模塊的所有依賴。 大家最熟知的JavaScript模塊加載器是服務(wù)于Node.js的 CommonJS和服務(wù)于Web應(yīng)用的Require.js。
TypeScript與ECMAScript 2015一樣,任何包含頂級import
或者export
的文件都被當(dāng)成一個模塊。
任何聲明(比如變量,函數(shù),類,類型別名或接口)都能夠通過添加export
關(guān)鍵字來導(dǎo)出。
export interface StringValidator {
isAcceptable(s: string): boolean;
}
export const numberRegexp = /^[0-9]+$/;
export class ZipCodeValidator implements StringValidator {
isAcceptable(s: string) {
return s.length === 5 && numberRegexp.test(s);
}
}
導(dǎo)出語句很便利,因為我們可能需要對導(dǎo)出的部分重命名,所以上面的例子可以這樣改寫:
class ZipCodeValidator implements StringValidator {
isAcceptable(s: string) {
return s.length === 5 && numberRegexp.test(s);
}
}
export { ZipCodeValidator };
export { ZipCodeValidator as mainValidator };
我們經(jīng)常會去擴展其它模塊,并且只導(dǎo)出那個模塊的部分內(nèi)容。 重新導(dǎo)出功能并不會在當(dāng)前模塊導(dǎo)入那個模塊或定義一個新的局部變量。
export class ParseIntBasedZipCodeValidator {
isAcceptable(s: string) {
return s.length === 5 && parseInt(s).toString() === s;
}
}
// 導(dǎo)出原先的驗證器但做了重命名
export {ZipCodeValidator as RegExpBasedZipCodeValidator} from "./ZipCodeValidator";
或者一個模塊可以包裹多個模塊,并把他們導(dǎo)出的內(nèi)容聯(lián)合在一起通過語法:export * from "module"
。
export * from "./StringValidator"; // exports interface StringValidator
export * from "./LettersOnlyValidator"; // exports class LettersOnlyValidator
export * from "./ZipCodeValidator"; // exports class ZipCodeValidator
模塊的導(dǎo)入操作與導(dǎo)出一樣簡單。 可以使用以下 import
形式之一來導(dǎo)入其它模塊中的導(dǎo)出內(nèi)容。
import { ZipCodeValidator } from "./ZipCodeValidator";
let myValidator = new ZipCodeValidator();
可以對導(dǎo)入內(nèi)容重命名
import { ZipCodeValidator as ZCV } from "./ZipCodeValidator";
let myValidator = new ZCV();
import * as validator from "./ZipCodeValidator";
let myValidator = new validator.ZipCodeValidator();
盡管不推薦這么做,一些模塊會設(shè)置一些全局狀態(tài)供其它模塊使用。 這些模塊可能沒有任何的導(dǎo)出或用戶根本就不關(guān)注它的導(dǎo)出。 使用下面的方法來導(dǎo)入這類模塊:
import "./my-module.js";
每個模塊都可以有一個default
導(dǎo)出。 默認(rèn)導(dǎo)出使用 default
關(guān)鍵字標(biāo)記;并且一個模塊只能夠有一個default
導(dǎo)出。 需要使用一種特殊的導(dǎo)入形式來導(dǎo)入 default
導(dǎo)出。
default
導(dǎo)出十分便利。 比如,像JQuery這樣的類庫可能有一個默認(rèn)導(dǎo)出 jQuery
或$
,并且我們基本上也會使用同樣的名字jQuery
或$
導(dǎo)出JQuery。
declare let $: JQuery;
export default $;
import $ from "JQuery";
$("button.continue").html( "Next Step..." );
類和函數(shù)聲明可以直接被標(biāo)記為默認(rèn)導(dǎo)出。 標(biāo)記為默認(rèn)導(dǎo)出的類和函數(shù)的名字是可以省略的。
export default class ZipCodeValidator {
static numberRegexp = /^[0-9]+$/;
isAcceptable(s: string) {
return s.length === 5 && ZipCodeValidator.numberRegexp.test(s);
}
}
import validator from "./ZipCodeValidator";
let myValidator = new validator();
或者
const numberRegexp = /^[0-9]+$/;
export default function (s: string) {
return s.length === 5 && numberRegexp.test(s);
}
import validate from "./StaticZipCodeValidator";
let strings = ["Hello", "98052", "101"];
// Use function validate
strings.forEach(s => {
console.log(`"${s}" ${validate(s) ? " matches" : " does not match"}`);
});
default
導(dǎo)出也可以是一個值
export default "123";
import num from "./OneTwoThree";
console.log(num); // "123"
export =
和 import = require()
CommonJS和AMD都有一個exports
對象的概念,它包含了一個模塊的所有導(dǎo)出內(nèi)容。
它們也支持把exports
替換為一個自定義對象。 默認(rèn)導(dǎo)出就好比這樣一個功能;然而,它們卻并不相互兼容。 TypeScript模塊支持 export =
語法以支持傳統(tǒng)的CommonJS和AMD的工作流模型。
export =
語法定義一個模塊的導(dǎo)出對象。 它可以是類,接口,命名空間,函數(shù)或枚舉。
若要導(dǎo)入一個使用了export =
的模塊時,必須使用TypeScript提供的特定語法import let = require("module")
。
let numberRegexp = /^[0-9]+$/;
class ZipCodeValidator {
isAcceptable(s: string) {
return s.length === 5 && numberRegexp.test(s);
}
}
export = ZipCodeValidator;
import zip = require("./ZipCodeValidator");
// Some samples to try
let strings = ["Hello", "98052", "101"];
// Validators to use
let validator = new zip();
// Show whether each string passed each validator
strings.forEach(s => {
console.log(`"${ s }" - ${ validator.isAcceptable(s) ? "matches" : "does not match" }`);
});
根據(jù)編譯時指定的模塊目標(biāo)參數(shù),編譯器會生成相應(yīng)的供Node.js (CommonJS),Require.js (AMD),isomorphic (UMD), SystemJS或ECMAScript 2015 native modules (ES6)模塊加載系統(tǒng)使用的代碼。 想要了解生成代碼中define
,require
和 register
的意義,請參考相應(yīng)模塊加載器的文檔。
下面的例子說明了導(dǎo)入導(dǎo)出語句里使用的名字是怎么轉(zhuǎn)換為相應(yīng)的模塊加載器代碼的。
import m = require("mod");
export let t = m.something + 1;
define(["require", "exports", "./mod"], function (require, exports, mod_1) {
exports.t = mod_1.something + 1;
});
let mod_1 = require("./mod");
exports.t = mod_1.something + 1;
(function (factory) {
if (typeof module === "object" && typeof module.exports === "object") {
let 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) {
let mod_1 = require("./mod");
exports.t = mod_1.something + 1;
});
System.register(["./mod"], function(exports_1) {
let mod_1;
let t;
return {
setters:[
function (mod_1_1) {
mod_1 = mod_1_1;
}],
execute: function() {
exports_1("t", t = mod_1.something + 1);
}
}
});
import { something } from "./mod";
export let t = something + 1;
下面我們來整理一下前面的驗證器實現(xiàn),每個模塊只有一個命名的導(dǎo)出。
為了編譯,我們必需要在命令行上指定一個模塊目標(biāo)。對于Node.js來說,使用--module commonjs
; 對于Require.js來說,使用``--module amd`。比如:
tsc --module commonjs Test.ts
編譯完成后,每個模塊會生成一個單獨的.js
文件。 好比使用了reference標(biāo)簽,編譯器會根據(jù) import
語句編譯相應(yīng)的文件。
export interface StringValidator {
isAcceptable(s: string): boolean;
}
import { StringValidator } from "./Validation";
const lettersRegexp = /^[A-Za-z]+$/;
export class LettersOnlyValidator implements StringValidator {
isAcceptable(s: string) {
return lettersRegexp.test(s);
}
}
import { StringValidator } from "./Validation";
const numberRegexp = /^[0-9]+$/;
export class ZipCodeValidator implements StringValidator {
isAcceptable(s: string) {
return s.length === 5 && numberRegexp.test(s);
}
}
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 (let name in validators) {
console.log(`"${ s }" - ${ validators[name].isAcceptable(s) ? "matches" : "does not match" } ${ name }`);
}
});
有時候,你只想在某種條件下才加載某個模塊。 在TypeScript里,使用下面的方式來實現(xiàn)它和其它的高級加載場景,我們可以直接調(diào)用模塊加載器并且可以保證類型完全。
編譯器會檢測是否每個模塊都會在生成的JavaScript中用到。 如果一個模塊標(biāo)識符只在類型注解部分使用,并且完全沒有在表達(dá)式中使用時,就不會生成 require
這個模塊的代碼。 省略掉沒有用到的引用對性能提升是很有益的,并同時提供了選擇性加載模塊的能力。
這種模式的核心是import id = require("...")
語句可以讓我們訪問模塊導(dǎo)出的類型。 模塊加載器會被動態(tài)調(diào)用(通過 require
),就像下面if
代碼塊里那樣。 它利用了省略引用的優(yōu)化,所以模塊只在被需要時加載。 為了讓這個模塊工作,一定要注意 import
定義的標(biāo)識符只能在表示類型處使用(不能在會轉(zhuǎn)換成JavaScript的地方)。
為了確保類型安全性,我們可以使用typeof
關(guān)鍵字。 typeof
關(guān)鍵字,當(dāng)在表示類型的地方使用時,會得出一個類型值,這里就表示模塊的類型。
declare function require(moduleName: string): any;
import { ZipCodeValidator as Zip } from "./ZipCodeValidator";
if (needZipValidation) {
let ZipCodeValidator: typeof Zip = require("./ZipCodeValidator");
let validator = new ZipCodeValidator();
if (validator.isAcceptable("...")) { /* ... */ }
}
declare function require(moduleNames: string[], onLoad: (...args: any[]) => void): void;
import * as Zip from "./ZipCodeValidator";
if (needZipValidation) {
require(["./ZipCodeValidator"], (ZipCodeValidator: typeof Zip) => {
let validator = new ZipCodeValidator.ZipCodeValidator();
if (validator.isAcceptable("...")) { /* ... */ }
});
}
declare const System: any;
import { ZipCodeValidator as Zip } from "./ZipCodeValidator";
if (needZipValidation) {
System.import("./ZipCodeValidator").then((ZipCodeValidator: typeof Zip) => {
var x = new ZipCodeValidator();
if (x.isAcceptable("...")) { /* ... */ }
});
}
要想描述非TypeScript編寫的類庫的類型,我們需要聲明類庫所暴露出的API。
我們叫它聲明因為它不是“外部程序”的具體實現(xiàn)。 它們通常是在 .d.ts
文件里定義的。 如果你熟悉C/C++,你可以把它們當(dāng)做 .h
文件。 讓我們看一些例子。
在Node.js里大部分工作是通過加載一個或多個模塊實現(xiàn)的。 我們可以使用頂級的 export
聲明來為每個模塊都定義一個.d.ts
文件,但最好還是寫在一個大的.d.ts
文件里。 我們使用與構(gòu)造一個外部命名空間相似的方法,但是這里使用 module
關(guān)鍵字并且把名字用引號括起來,方便之后import
。 例如:
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 let sep: string;
}
現(xiàn)在我們可以/// <reference>
node.d.ts
并且使用import url = require("url");
加載模塊。
/// <reference path="node.d.ts"/>
import * as URL from "url";
let myUrl = URL.parse("http://www.typescriptlang.org");
假如你不想在使用一個新模塊之前花時間去編寫聲明,你可以采用聲明的簡寫形式以便能夠快速使用它。
declare module "hot-new-module";
簡寫模塊里所有導(dǎo)出的類型將是any
。
import x, {y} from "hot-new-module";
x(y);
某些模塊加載器如SystemJS 和 AMD支持導(dǎo)入非JavaScript內(nèi)容。 它們通常會使用一個前綴或后綴來表示特殊的加載語法。 模塊聲明通配符可以用來表示這些情況。
declare module "*!text" {
const content: string;
export default content;
}
// Some do it the other way around.
declare module "json!*" {
const value: any;
export default value;
}
現(xiàn)在你可以就導(dǎo)入匹配"*!text"
或"json!*"
的內(nèi)容了。
import fileContent from "./xyz.txt!text";
import data from "json!http://example.com/data.json";
console.log(data, fileContent);
有些模塊被設(shè)計成兼容多個模塊加載器,或者不使用模塊加載器(全局變量)。 它們以 UMD或Isomorphic模塊為代表。 這些庫可以通過導(dǎo)入的形式或全局變量的形式訪問。 例如:
export const isPrime(x: number): boolean;
export as namespace mathLib;
之后,這個庫可以在某個模塊里通過導(dǎo)入來使用:
import { isPrime } from "math-lib";
isPrime(2);
mathLib.isPrime(2); // ERROR: can't use the global definition from inside a module
它同樣可以通過全局變量的形式使用,但只能在某個腳本里。 (腳本是指一個不帶有導(dǎo)入或?qū)С龅奈募?。?/p>
mathLib.isPrime(2);
用戶應(yīng)該更容易地使用你模塊導(dǎo)出的內(nèi)容。 嵌套層次過多會變得難以處理,因此仔細(xì)考慮一下如何組織你的代碼。
從你的模塊中導(dǎo)出一個命名空間就是一個增加嵌套的例子。 雖然命名空間有時候有它們的用處,在使用模塊的時候它們額外地增加了一層。 這對用戶來說是很不便的并且通常是多余的。
導(dǎo)出類的靜態(tài)方法也有同樣的問題 - 這個類本身就增加了一層嵌套。 除非它能方便表述或便于清晰使用,否則請考慮直接導(dǎo)出一個輔助方法。
class
或 function
,使用 export default
就像“在頂層上導(dǎo)出”幫助減少用戶使用的難度,一個默認(rèn)的導(dǎo)出也能起到這個效果。 如果一個模塊就是為了導(dǎo)出特定的內(nèi)容,那么你應(yīng)該考慮使用一個默認(rèn)導(dǎo)出。 這會令模塊的導(dǎo)入和使用變得些許簡單。 比如:
export default class SomeType {
constructor() { ... }
}
export default function getThing() { return 'thing'; }
import t from "./MyClass";
import f from "./MyFunc";
let x = new t();
console.log(f());
對用戶來說這是最理想的。他們可以隨意命名導(dǎo)入模塊的類型(本例為t
)并且不需要多余的(.)來找到相關(guān)對象。
export class SomeType { /* ... */ }
export function someFunc() { /* ... */ }
相反地,當(dāng)導(dǎo)入的時候:
import { SomeType, SomeFunc } from "./MyThings";
let x = new SomeType();
let y = someFunc();
export class Dog { ... }
export class Cat { ... }
export class Tree { ... }
export class Flower { ... }
import * as myLargeModule from "./MyLargeModule.ts";
let x = new myLargeModule.Dog();
你可能經(jīng)常需要去擴展一個模塊的功能。 JS里常用的一個模式是JQuery那樣去擴展原對象。 如我們之前提到的,模塊不會像全局命名空間對象那樣去 合并。 推薦的方案是 不要去改變原來的對象,而是導(dǎo)出一個新的實體來提供新的功能。
假設(shè)Calculator.ts
模塊里定義了一個簡單的計算器實現(xiàn)。 這個模塊同樣提供了一個輔助函數(shù)來測試計算器的功能,通過傳入一系列輸入的字符串并在最后給出結(jié)果。
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()}'`);
}
這是使用導(dǎo)出的test
函數(shù)來測試計算器。
import { Calculator, test } from "./Calculator";
let c = new Calculator();
test(c, "1+2*33/11="); // prints 9
現(xiàn)在擴展它,添加支持輸入其它進(jìn)制(十進(jìn)制以外),讓我們來創(chuàng)建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
模塊導(dǎo)出的API與原先的Calculator
模塊很相似,但卻沒有改變原模塊里的對象。 下面是測試ProgrammerCalculator類的代碼:
import { Calculator, test } from "./ProgrammerCalculator";
let c = new Calculator(2);
test(c, "001+010="); // prints 3
當(dāng)初次進(jìn)入基于模塊的開發(fā)模式時,可能總會控制不住要將導(dǎo)出包裹在一個命名空間里。 模塊具有其自己的作用域,并且只有導(dǎo)出的聲明才會在模塊外部可見。 記住這點,命名空間在使用模塊時幾乎沒什么價值。
在組織方面,命名空間對于在全局作用域內(nèi)對邏輯上相關(guān)的對象和類型進(jìn)行分組是很便利的。 例如,在C#里,你會從 System.Collections
里找到所有集合的類型。 通過將類型有層次地組織在命名空間里,可以方便用戶找到與使用那些類型。 然而,模塊本身已經(jīng)存在于文件系統(tǒng)之中,這是必須的。 我們必須通過路徑和文件名找到它們,這已經(jīng)提供了一種邏輯上的組織形式。 我們可以創(chuàng)建 /collections/generic/
文件夾,把相應(yīng)模塊放在這里面。
命名空間對解決全局作用域里命名沖突來說是很重要的。 比如,你可以有一個My.Application.Customer.AddForm
和My.Application.Order.AddForm
-- 兩個類型的名字相同,但命名空間不同。 然而,這對于模塊來說卻不是一個問題。 在一個模塊里,沒有理由兩個對象擁有同一個名字。 從模塊的使用角度來說,使用者會挑出他們用來引用模塊的名字,所以也沒有理由發(fā)生重名的情況。
更多關(guān)于模塊和命名空間的資料查看[命名空間和模塊](./Namespaces and Modules.md)
以下均為模塊結(jié)構(gòu)上的危險信號。重新檢查以確保你沒有在對模塊使用命名空間:
export namespace Foo { ... }
(刪除Foo
并把所有內(nèi)容向上層移動一層)export class
或export function
(考慮使用export default
)export namespace Foo {
(不要以為這些會合并到一個Foo
中?。?/li>
更多建議: