一、可选的模块加载和其它高级加载场景

有的时候可能会需要在某种条件下再去加载某个模块,而在 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(moduleName: 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 文件。

使用与构造一个外部命名空间相似的方法,但是这里使用 module 关键字,并将名字用引号括起来,方便之后 import

node.d.ts:

declare module "url" {
    export interface Url {
        protocol?: string;
        hostname?: string;
        pathname?: string;
    }
    export function parse(urlStr: string, parseQueryString?: any, slashesDenoteHost?: any) :Url;
}

declare module "path" {
    export function normalize(p: string): string;
    export function join(...paths: any[]): string;
    export let sep: string;
}

声明好之后可以 /// <refrence> node.d.ts 并且使用 import url = require('url'); 或者 import * as URL from 'url' 加载模块。

/// <refrence> node.d.ts
import * as URL from 'url';
let myUrl = URL.parse('http://www.typescriptlang.com');

外部模块简写

如果不想在使用一个新模块之前花时间去写声明,可以采用声明的简写形式以便能够快速使用

declaration.d.ts:

declare module "hot-new-module";

简写模块里所有的导出类型都是 any:

import x, {y} from 'hot-new-module';
x(y);

模块声明通配符

有的模块加载器比如 SystemJSAMD 支持导入非 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 模块。这些库可以通过导入的形式或者全局变量的形式访问:

math-lib.d.ts:

export function isPrime(x: number) :boolean;
export as namespace mathLib;

然后在这个库中去使用这个模块:

import {isPrime} from 'math-lib';
isPrime(2);
mathLib.isPrime(2);// 错误,不能再模块内部使用全局定义

不过同样可以在某个脚本(不带模块导入导出的脚本文件)中:

mathLib.isPrime(2);

三、创建模块结构导出

尽可能在顶层导出

用户应该更容易使用你模块导出的内容。嵌套层级过多会变得难以处理,因此仔细考虑一下如何组织你的代码。

从你的模块中导出一个命名空间就是一个增加嵌套的例子。虽然命名空间有时候有它们的用处,在使用模块的时候,它们额外的增加了一层。对于用户来说是不方便而且是多余的。

导出类的静态方法也有一样的问题 - 类本身就已经多了一层嵌套了。除非它能够方便表述或者便于清晰使用,否则请考虑直接导出一个辅助方法。

如果仅导出单个 classfunction,使用 export default

默认导出能够起到与 “在顶层导出” 一样的效果来帮助减少用户试用的难度,如果一个模块就是为了导出特定的内容,那么应该考虑使用一个默认导出。这能够使得模块的导入和使用变得些许简单。

MyClass.ts:

exports default class SomeType {
    constructor() { ... }
}

NyFunc.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 模块里面定义了一个简单的计算器实现,这个模块同样提供了一个辅助函数来测试计算器的功能,通过传入一系列输入的字符串并在最后给出结果,

Calcalator.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 handleChar(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.handleChar(input[i]);
    }

    console.log(`result of '${input}' is '${c.getResult()}'`);
}

下面使用 test 函数来测试计算器。

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();
        const maxBase = ProgrammerCalculator.digits.length;
        if (base <= 0 || base > maxBase) {
            throw new Error(`base has to be within 0 to ${maxBase} 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 模块很相似,但是没有改变原来模块的对象:

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.AddFormMy.Application.Order.AddForm --- 两个类型的名字相同,但是命名空间不同。

然而对于模块来说,在一个模块中,没有理由两个对象拥有同一个名字。从模块实用角度来说,使用者会挑出它们用来引用模块的名字,所以也没有理由发生重名的情况。

需要注意的事情

以下均为模块结构上的危险信号。确保没有在对模块使用命名空间:

  • 文件的顶层声明是 export namespace Foo { ... } (删除 Foo 并把所有内容向上层移动一层)
  • 文件只有一个 export classexport function (考虑使用 export default
  • 多个文件的顶层具有同样的 export namespace Foo { (不要以为这些会合并到一个 Foo 中!)