TypeScript手冊翻譯系列4-模塊

模塊

在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;

Export =

在前面例子中,當咱們須要引用每一個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('...')) { /* ... */ }
    });
}

與其餘JavaScript庫工做

爲了描述不是用TypeScript編寫的庫的shape,須要聲明庫對外暴露的API。因爲大多數JavaScript庫對外僅暴露一些頂級對象,模塊是表示它們的一種好方法。咱們將不定義實現的聲明稱爲"ambient",一般都定義在.d.ts文件中。若是你熟悉C/C++,能夠把這些文件看作.h頭文件或'extern'。下面看一些內部和外部例子。

Ambient內部模塊

流行庫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;

Ambient外部模塊

在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

相關文章
相關標籤/搜索