本文概述了TypeScript中如何使用模塊以各種方式來組織代碼。我們將涵括內部和外部的模塊,並且討論他們在適合在何時使用和怎麼使用。我們也會學習一些如何使用外部模塊的高級技巧,並且解決一些當我們使用TypeScript的模塊時遇到的陷阱。 案例的基礎 接下來開始寫程式,我們將會在這裡寫上使用案例。
本文概述了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文件中是不可能的。