关于术语的一点说明: 请务必留意一点,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";
- let myValidator = new ZipCodeValidator();
复制代码 可以对导入内容重命名
- import { ZipCodeValidator as ZCV } from "./ZipCodeValidator";
- let myValidator = new ZCV();
复制代码 将整个模块导入到一个变量,并通过它来访问模块的导出部门
- import * as validator from "./ZipCodeValidator";
- let myValidator = new validator.ZipCodeValidator();
复制代码 具有副作用的导入模块
尽管不推荐这么做,一些模块会设置一些全局状态供别的模块利用。 这些模块可能没有任何的导出或用户根本就不关注它的导出。 利用下面的方法来导入这类模块:
默认导出
每个模块都可以有一个default导出。 默认导出利用 default关键字标记;并且一个模块只可以或许有一个default导出。 须要利用一种特殊的导入形式来导入 default导出。
default导出十分便利。 好比,像JQuery如许的类库可能有一个默认导出 jQuery或$,并且我们根本上也会利用同样的名字jQuery或$导出JQuery。
JQuery.d.ts
- declare let $: 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";
- let myValidator = 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";
- let strings = ["Hello", "98052", "101"];
- // Use function validate
- strings.forEach(s => {
- console.log(`"${s}" ${validate(s) ? " matches" : " does not match"}`);
- });
复制代码 default导出也可以是一个值
OneTwoThree.ts
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 let = require("module")。
ZipCodeValidator.ts
- let 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
- 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" }`);
- });
复制代码 天生模块代码
根据编译时指定的模块目标参数,编译器会天生相应的供Node.js (CommonJS),Require.js (AMD),isomorphic (UMD), SystemJS或ECMAScript 2015 native modules (ES6)模块加载系统利用的代码。 想要了解天生代码中define,require 和 register的意义,请参考相应模块加载器的文档。
下面的例子说明白导入导出语句里利用的名字是怎么转换为相应的模块加载器代码的。
SimpleModule.ts
- import m = require("mod");
- export let 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
- let mod_1 = require("./mod");
- exports.t = mod_1.something + 1;
复制代码 UMD SimpleModule.js
- (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 SimpleModule.js
- 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);
- }
- }
- });
复制代码 Native ECMAScript 2015 modules SimpleModule.js
- import { something } from "./mod";
- export let 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 (let 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) {
- let ZipCodeValidator: typeof Zip = require("./ZipCodeValidator");
- let validator = new ZipCodeValidator();
- if (validator.isAcceptable("...")) { /* ... */ }
- }
复制代码 示例:require.js里的动态模块加载
- 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("...")) { /* ... */ }
- });
- }
复制代码 示例:System.js里的动态模块加载
- 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("...")) { /* ... */ }
- });
- }
复制代码 利用别的的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 let sep: string;
- }
复制代码 现在我们可以/// <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");
复制代码 外部模块简写
假如你不想在利用一个新模块之前花时间去编写声明,你可以采用声明的简写形式以便可以或许快速利用它。
declarations.d.ts
- declare module "hot-new-module";
复制代码 简写模块里全部导出的类型将是any。
- import x, {y} from "hot-new-module";
- x(y);
复制代码 模块声明通配符
某些模块加载器如SystemJS 和 AMD支持导入非JavaScript内容。 它们通常会利用一个前缀或后缀来表示特殊的加载语法。 模块声明通配符可以用来表示这些环境。
- 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;
- }
复制代码 现在你可以就导入匹配"*!text"或"json!*"的内容了。
- import fileContent from "./xyz.txt!text";
- import data from "json!http://example.com/data.json";
- console.log(data, fileContent);
复制代码 UMD模块
有些模块被设计成兼容多个模块加载器,或者不利用模块加载器(全局变量)。 它们以 UMD或Isomorphic模块为代表。 这些库可以通过导入的形式或全局变量的形式访问。 例如:
math-lib.d.ts
- export const isPrime(x: number): boolean;
- export as namespace mathLib;
复制代码 之后,这个库可以在某个模块里通过导入来利用:
- import { isPrime } from "math-lib";
- isPrime(2);
- mathLib.isPrime(2);
- // ERROR: can't use the global definition from inside a module
复制代码 它同样可以通过全局变量的形式利用,但只能在某个脚本里。 (脚本是指一个不带有导入或导出的文件。)
创建模块结构指导
尽可能地在顶层导出
用户应该更轻易地利用你模块导出的内容。 嵌套层次过多会变得难以处置惩罚,因此细致思量一下如何构造你的代码。
从你的模块中导出一个命名空间就是一个增长嵌套的例子。 固然命名空间有时候有它们的用处,在利用模块的时候它们额外地增长了一层。 这对用户来说是很不便的并且通常是多余的。
导出类的静态方法也有同样的问题 - 这个类自己就增长了一层嵌套。 除非它能方便表述或便于清晰利用,否则请思量直接导出一个辅助方法。
如果仅导出单个 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";
- let x = new t();
- console.log(f());
复制代码 对用户来说这是最理想的。他们可以随意命名导入模块的类型(本例为t)并且不须要多余的(.)来找到相关对象。
如果要导出多个对象,把它们放在顶层里导出
MyThings.ts
- export class SomeType { /* ... */ }
- export function someFunc() { /* ... */ }
复制代码 相反地,当导入的时候:
明确地列出导入的名字
Consumer.ts
- import { SomeType, SomeFunc } from "./MyThings";
- let x = new SomeType();
- let y = someFunc();
复制代码 利用命名空间导入模式当你要导出大量内容的时候
MyLargeModule.ts
- export class Dog { ... }
- export class Cat { ... }
- export class Tree { ... }
- export class Flower { ... }
复制代码 Consumer.ts
- import * as myLargeModule from "./MyLargeModule.ts";
- let 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";
- let 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";
- let c = new Calculator(2);
- test(c, "001+010="); // prints 3
复制代码 模块里不要利用命名空间
当初次进入基于模块的开辟模式时,可能总会控制不住要将导出包裹在一个命名空间里。 模块具有其自己的作用域,并且只有导出的声明才会在模块外部可见。 记住这点,命名空间在利用模块时险些没什么价值。
在构造方面,命名空间对于在全局作用域内对逻辑上相关的对象和类型进行分组是很便利的。 例如,在C#里,你会从 System.Collections里找到全部集合的类型。 通过将类型有层次地构造在命名空间里,可以方便用户找到与利用那些类型。 然而,模块自己已经存在于文件系统之中,这是必须的。 我们必须通过路径和文件名找到它们,这已经提供了一种逻辑上的构造形式。 我们可以创建 /collections/generic/文件夹,把相应模块放在这内里。
命名空间对解决全局作用域里命名辩论来说是很重要的。 好比,你可以有一个My.Application.Customer.AddForm和My.Application.Order.AddForm -- 两个类型的名字相同,但命名空间不同。 然而,这对于模块来说却不是一个问题。 在一个模块里,没有理由两个对象拥有同一个名字。 从模块的利用角度来说,利用者会挑出他们用来引用模块的名字,所以也没有理由发生重名的环境。
更多关于模块和命名空间的资料检察[命名空间和模块](./Namespaces and Modules.md)
伤害信号
以下均为模块结构上的伤害信号。重新查抄以确保你没有在对模块利用命名空间:
- 文件的顶层声明是export namespace Foo { ... } (删除Foo并把全部内容向上层移动一层)
- 文件只有一个export class或export function (思量利用export default)
- 多个文件的顶层具有同样的export namespace Foo { (不要以为这些会合并到一个Foo中!)
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |