深入理解TypeScript——文档篇之模块

386 阅读6分钟

一、前提

ts1.5术语变化,“内部模块”现在称做“命名空间”。 “外部模块”现在则简称为“模块”。

二、介绍

  1. 模块在其自身的作用域里执行,而不是在全局作用域里。这意味着定义在一个模块里的变量,函数,类等等在模块外部是不可见的,除非你明确地使用export形式之一导出它们。 相反,如果想使用其它模块导出的变量,函数,类,接口等的时候,你必须要导入它们,可以使用 import形式之一。
  2. 模块是自声明的;两个模块之间的关系是通过在文件级别上使用imports和exports建立的。
  3. 模块使用模块加载器去导入其它的模块。 在运行时,模块加载器的作用是在执行此模块代码前去查找并执行这个模块的所有依赖。 大家最熟知的JavaScript模块加载器是服务于Node.js的 CommonJS和服务于Web应用的Require.js。
  4. TypeScript与ECMAScript 2015一样,任何包含顶级import或者export的文件都被当成一个模块。相反地,如果一个文件不带有顶级的import或者export声明,那么它的内容被视为全局可见的(因此对模块也是可见的)。

三、导出

  1. 导出声明 任何声明(比如变量,函数,类,类型别名或接口)都能够通过添加export关键字来导出。
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);
    }
}
  1. 导出语句(重命名)
class ZipCodeValidator implements StringValidator {
    isAcceptable(s: string) {
        return s.length === 5 && numberRegexp.test(s);
    }
}
export { ZipCodeValidator };
export { ZipCodeValidator as mainValidator };
  1. 重新导出 扩展其它模块,并且只导出那个模块的部分内容。 重新导出功能并不会在当前模块导入那个模块或定义一个新的局部变量。
export class ParseIntBasedZipCodeValidator {
    isAcceptable(s: string) {
        return s.length === 5 && parseInt(s).toString() === s;
    }
}

// 导出原先的验证器但做了重命名
export {ZipCodeValidator as RegExpBasedZipCodeValidator} from "./ZipCodeValidator";

一个模块可以包裹多个模块,并把他们导出的内容联合在一起通过语法:export * from "module"。

export * from "./StringValidator"; // exports interface StringValidator
export * from "./LettersOnlyValidator"; // exports class LettersOnlyValidator
export * from "./ZipCodeValidator";  // exports class ZipCodeValidator

四、导入

模块的导入操作与导出一样简单。 可以使用以下 import形式之一来导入其它模块中的导出内容。

  1. 导入一个模块中的某个导出内容
import { ZipCodeValidator } from "./ZipCodeValidator";

let myValidator = new ZipCodeValidator();
  1. 重命名
import { ZipCodeValidator as ZCV } from "./ZipCodeValidator";
let myValidator = new ZCV();
  1. 将整个模块导入到一个变量,并通过它来访问模块的导出部分
import * as validator from "./ZipCodeValidator";
let myValidator = new validator.ZipCodeValidator();
  1. 具有副作用的导入模块 不推荐这么做,一些模块会设置一些全局状态供其它模块使用。 这些模块可能没有任何的导出或用户根本就不关注它的导出。 使用下面的方法来导入这类模块。
import "./my-module.js";

五、默认导出

每个模块都可以有一个default导出。 默认导出使用 default关键字标记;并且一个模块只能够有一个default导出。 需要使用一种特殊的导入形式来导入 default导出。

默认导出:

// export
declare let $: JQuery;

export default $;

默认导入:

// import and use
import $ from "JQuery";

$("button.continue").html( "Next Step..." );

类和函数声明可以直接被标记为默认导出。 标记为默认导出的类和函数的名字是可以省略的。

默认导出类:

export default class ZipCodeValidator {
    static numberRegexp = /^[0-9]+$/;
    isAcceptable(s: string) {
        return s.length === 5 && ZipCodeValidator.numberRegexp.test(s);
    }
}


// 省略默认导出名
const numberRegexp = /^[0-9]+$/;

export default class {
	static numberRegexp = /^[0-9]+$/;
	isAcceptable(s: string) {
			return s.length === 5 && numberRegexp.test(s);
	}
}

默认导入类使用:

import validator from "./ZipCodeValidator"; // default

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导出也可以是一个值。

默认导出值:

export default "123";

默认导入值:

import num from "./OneTwoThree";

console.log(num); // "123"

六、export = 和 import = require()

为了支持CommonJS和AMD的exports, TypeScript提供了export =语法。

export =语法定义一个模块的导出对象。这里的对象一词指的是类,接口,命名空间,函数或枚举。
==若使用export =导出一个模块,则必须使用TypeScript的特定语法import module = 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" }`);
});

七、生成模块代码

导入导出语句里使用的名字是怎么转换为相应的模块加载器代码?

ts:

import m = require("mod");
export let t = m.something + 1;

AMD / RequireJS:

define(["require", "exports", "./mod"], function (require, exports, mod_1) {
    exports.t = mod_1.something + 1;
});

CommonJS:

let mod_1 = require("./mod");
exports.t = mod_1.something + 1;

Native ECMAScript 2015 modules:

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语句编译相应的文件。

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

编译器会检测是否每个模块都会在生成的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("...")) { /* ... */ }
    });
}

十、使用其它的JavaScript库

目的:要想描述非TypeScript编写的类库的类型,我们需要声明类库所暴露出的API。
我们叫它声明因为它不是“外部程序”的具体实现。 它们通常是在 .d.ts文件里定义的。

  1. 外部模块
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;
}

现在我们可以/// node.d.ts并且使用import url = require("url");或import * as URL from "url"加载模块。

/// <reference path="node.d.ts"/>
import * as URL from "url";
let myUrl = URL.parse("http://www.typescriptlang.org");
  1. 外部模块简写(推荐) 假如你不想在使用一个新模块之前花时间去编写声明,你可以采用声明的简写形式以便能够快速使用它。
declare module "hot-new-module";

简写模块里所有导出的类型将是any。

import x, {y} from "hot-new-module";
x(y);
  1. 模块声明通配符
    某些模块加载器如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;
}

使用:

import fileContent from "./xyz.txt!text";
import data from "json!http://example.com/data.json";
console.log(data, fileContent);
  1. UMD模块 有些模块被设计成兼容多个模块加载器,或者不使用模块加载器(全局变量)。 它们以 UMD模块为代表。 这些库可以通过导入的形式或全局变量的形式访问。例如:
export function isPrime(x: number): boolean;
export as namespace mathLib;

使用:

import { isPrime } from "math-lib";
isPrime(2);
mathLib.isPrime(2); 

十一、创建模块结构指导

  1. 如果仅导出单个 class 或 function,使用 export default

导出:

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());
  1. 如果要导出多个对象,把它们放在顶层里导出

导出:

export class SomeType { /* ... */ }
export function someFunc() { /* ... */ }

导入:

import { SomeType, someFunc } from "./MyThings";
let x = new SomeType();
let y = someFunc();
  1. 使用命名空间导入模式当你要导出大量内容的时候

导出:

export class Dog { ... }
export class Cat { ... }
export class Tree { ... }
export class Flower { ... }

导入:

import * as myLargeModule from "./MyLargeModule.ts";
let x = new myLargeModule.Dog();
  1. 使用重新导出进行扩展
    你可能经常需要去扩展一个模块的功能。 JS里常用的一个模式是JQuery那样去扩展原对象。 如我们之前提到的,模块不会像全局命名空间对象那样去 合并。 推荐的方案是 不要去改变原来的对象,而是导出一个新的实体来提供新的功能。

  2. 模块里不要使用命名空间
    当初次进入基于模块的开发模式时,可能总会控制不住要将导出包裹在一个命名空间里。 模块具有其自己的作用域,并且只有导出的声明才会在模块外部可见。 记住这点,命名空间在使用模块时几乎没什么价值。

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

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