模块

namespace-and-module
https://www.tslang.cn/docs/handbook/modules.html

关于术语的一点说明: 请务必注意一点,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的文件都被当成一个模块。相反地,如果一个文件不带有顶级的import或者export声明,那么它的内容被视为全局可见的(因此对模块也是可见的)。

全局模块

  • 在默认情况下,当你开始在一个新的 ts 文件中写下代码时,它处于全局命名空间中。
  • 使用全局变量命名空间是危险的,因为它会与文件内的代码命名冲突。推荐使用下边的文件模块。

foo.ts

  1. const foo = 1;

bar.ts

  1. const bar = foo;

文件模块

  • 文件模块也被称为外部模块。如果在 ts 文件的根级别位置含有 import 或者 export,那么它会在这个文件中创建一个本地的作用域。
  • 模块是 ts 中外部模块的简称,侧重于代码和复用
  • 模块在其自身的作用域里执行,而不是在全局作用域里
  • 一个模块里的变量、函数、类等在外部是不可见的,除非你把它导出
  • 如果想要使用一个模块里导出的变量,则需要导入

2.ts

  1. export const a = 1;
  2. export const b = 2;
  3. export default 'jack';

1.ts

  1. import name, {a, b} from './2';
  2. console.log(name, a, b); //jack 1 2

导出

导出声明

任何声明(比如变量,函数,类,类型别名或接口)都能够通过添加export关键字来导出。

  1. //2.ts
  2. export interface Person {
  3. name:string;
  4. age:number;
  5. }
  6. export const sex = 'male';
  7. export default 'jack';
  8. //1.ts
  9. import name, { sex, Person } from './2'
  10. let p:Person = {name: 'jack', age: 10};
  11. console.log(name, sex, p); //jack male { name: 'jack', age: 10 }

导出重命名

对导出的部分重命名。

  1. //2.ts
  2. export interface Person {
  3. name:string;
  4. age:number;
  5. }
  6. export { Person as Person2}
  7. //1.ts
  8. import { Person, Person2 } from './2'
  9. let p:Person = {name: 'jack', age: 10};
  10. let p2:Person2 = {name: 'jack2', age: 20};
  11. console.log(p, p2); //{ name: 'jack', age: 10 } { name: 'jack2', age: 20 }

导入导出结合

我们经常会去扩展其它模块,并且只导出那个模块的部分内容。 重新导出功能并不会在当前模块导入那个模块或定义一个新的局部变量。

  1. //3.ts
  2. export const aa = 'male';
  3. //2.ts
  4. export interface Person {
  5. name:string;
  6. age:number;
  7. }
  8. // 先导入,重命名后,再导出
  9. export { aa as sex} from './3'
  10. //1.ts
  11. import { sex, Person } from './2'
  12. let p:Person = {name: 'jack', age: 10};
  13. console.log(sex, p); //male { name: 'jack', age: 10 }

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

  1. //3.ts
  2. export const aa = 'male';
  3. export const bb = 'female';
  4. //2.ts
  5. //export * from './4';
  6. export * from './3';
  7. export interface Person {
  8. name:string;
  9. age:number;
  10. }
  11. //1.ts
  12. import { Person, aa, bb } from './2'
  13. let p:Person = {name: 'jack', age: 10};
  14. console.log(p, aa, bb); //{ name: 'jack', age: 10 } male female

导入

  1. import { Person, aa } from './2'
  2. import two, { Person, aa } from './2'
  3. import two, { Person, aa as male } from './2'
  4. import * as two from './2'
  5. //具有副作用的导入模块
  6. //不推荐这么做,一些模块会设置一些全局状态供其它模块使用。
  7. //这些模块可能没有任何的导出或用户根本就不关注它的导出。
  8. import "./2.js";

默认导出

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

  1. //App.ts
  2. export default class App {}
  3. // 等同于:export default class {}
  4. // 类和函数声明可以直接被标记为默认导出。 标记为默认导出的类和函数的名字是可以省略的。
  5. //使用
  6. import App from './App

default导出十分便利。 比如,像JQuery这样的类库可能有一个默认导出 jQuery$,并且我们基本上也会使用同样的名字jQuery$导出JQuery。

  1. //jQuery.d.ts
  2. declare let $: JQuery;
  3. export default $;
  4. //App.ts
  5. import $ from "JQuery";
  6. $("button.continue").html( "Next Step..." );

export =import = require()

CommonJS和AMD的环境里都有一个exports变量,这个变量包含了一个模块的所有导出内容。

CommonJS和AMD的exports都可以被赋值为一个对象, 这种情况下其作用就类似于 es6 语法里的默认导出,即 export default语法了。虽然作用相似,但是 export default 语法并不能兼容CommonJS和AMD的exports

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

export =语法定义一个模块的导出对象。 这里的对象一词指的是类,接口,命名空间,函数或枚举。

若使用export =导出一个模块,则必须使用TypeScript的特定语法import module = require("module")来导入此模块。

  1. //2.ts
  2. class App {
  3. static a = 1;
  4. }
  5. export = App;
  6. //使用
  7. //import App = require('./2');
  8. import App from './2'; //如果想这样,需要开启 "esModuleInterop": true
  9. console.log(App.a);

生成模块-不同规范对比

  • AMD:不要使用它,它仅能在浏览器工作。
  • SystemJS:这一个好的实验,已经被ES模块替代。
  • ES模块:它并没有准备好。
  • 使用 module:commonJS 选项来提到这些模式,将会是一个好的主意
  1. import App = require('./2');
  2. export let t = App.a;
  3. export function run(){}

amd
  1. define(["require", "exports", "./2"], function (require, exports, App) {
  2. "use strict";
  3. Object.defineProperty(exports, "__esModule", { value: true });
  4. exports.run = exports.t = void 0;
  5. exports.t = App.a;
  6. function run() { }
  7. exports.run = run;
  8. });

commonjs
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. exports.run = exports.t = void 0;
  4. var App = require("./2");
  5. exports.t = App.a;
  6. function run() { }
  7. exports.run = run;

umd
  1. (function (factory) {
  2. if (typeof module === "object" && typeof module.exports === "object") {
  3. var v = factory(require, exports);
  4. if (v !== undefined) module.exports = v;
  5. }
  6. else if (typeof define === "function" && define.amd) {
  7. define(["require", "exports", "./2"], factory);
  8. }
  9. })(function (require, exports) {
  10. "use strict";
  11. Object.defineProperty(exports, "__esModule", { value: true });
  12. exports.run = exports.t = void 0;
  13. var App = require("./2");
  14. exports.t = App.a;
  15. function run() { }
  16. exports.run = run;
  17. });

system
  1. System.register(["./2"], function (exports_1, context_1) {
  2. "use strict";
  3. var App, t;
  4. var __moduleName = context_1 && context_1.id;
  5. function run() { }
  6. exports_1("run", run);
  7. return {
  8. setters: [
  9. function (App_1) {
  10. App = App_1;
  11. }
  12. ],
  13. execute: function () {
  14. exports_1("t", t = App.a);
  15. }
  16. };
  17. });

使用其它的 js 库

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

外部模块

在Node.js里大部分工作是通过加载一个或多个模块实现的。 我们可以使用顶级的 export声明来为每个模块都定义一个.d.ts文件,但最好还是写在一个大的.d.ts文件里。 我们使用与构造一个外部命名空间相似的方法,但是这里使用 module关键字并且把名字用引号括起来,方便之后import。 例如:

/typings/node.d.ts

  1. declare module "url" {
  2. export interface Person {
  3. name: string;
  4. age: number;
  5. }
  6. export function Parse(url:string):string;
  7. }
  8. declare module "path" {
  9. export type Props1 = string | number | boolean;
  10. export type Props2 = string | number;
  11. export type SetDifference<T,U> = T extends U ? never: T;
  12. }

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

  1. //reference 就是引入 node.d.ts 文件,ts的专属语法。这个语法是给ide用的
  2. /// <reference path="../typings/node.d.ts"/>
  3. import * as URL from "url";
  4. import { Props1, Props2, SetDifference } from 'path';
  5. let person:URL.Person = {name: 'jack', age: 18};
  6. console.log(person); //{ name: 'jack', age: 18 }
  7. let parse = (url:string) => url;
  8. console.log(parse('https://www.baidu.com')); //https://www.baidu.com
  9. type A = SetDifference<Props1, Props2>;
  10. // => type A = boolean;

模块声明通配符

某些模块加载器如SystemJSAMD支持导入非JavaScript内容。 它们通常会使用一个前缀或后缀来表示特殊的加载语法。 模块声明通配符可以用来表示这些情况。

  1. declare module "*!text" {
  2. const content: string;
  3. export default content;
  4. }
  5. // Some do it the other way around.
  6. declare module "json!*" {
  7. const value: any;
  8. export default value;
  9. }

现在你可以就导入匹配"*!text""json!*"的内容了。

  1. import fileContent from "./xyz.txt!text";
  2. import data from "json!http://example.com/data.json";
  3. console.log(data, fileContent);

创建模块结构指导

尽可能地在顶层导出

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

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

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

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

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

  1. // MyClass.ts
  2. export default class SomeType {
  3. constructor() { ... }
  4. }
  5. // MyFunc.ts
  6. export default function getThing() { return 'thing'; }
  7. // 使用
  8. // 可以随意命名导入模块的类型(本例为t)并且不需要多余的(.)来找到相关对象。
  9. import t from "./MyClass";
  10. import f from "./MyFunc";
  11. let x = new t();
  12. console.log(f());

如果要导出多个对象,把它们放在顶层里导出
  1. export class SomeType { /* ... */ }
  2. export function someFunc() { /* ... */ }

明确地列出导入的名字
  1. import { SomeType, someFunc } from "./MyThings";
  2. let x = new SomeType();
  3. let y = someFunc();

使用命名空间导入模式当你要导出大量内容的时候
  1. // MyLargeModule.ts
  2. export class Dog { ... }
  3. export class Cat { ... }
  4. export class Tree { ... }
  5. export class Flower { ... }
  6. // 使用
  7. import * as myLargeModule from "./MyLargeModule.ts";
  8. let x = new myLargeModule.Dog();

使用重新导出进行扩展

你可能经常需要去扩展一个模块的功能。 JS里常用的一个模式是JQuery那样去扩展原对象。 如我们之前提到的,模块不会像全局命名空间对象那样去 合并。 推荐的方案是 不要去改变原来的对象,而是导出一个新的实体来提供新的功能。

假设Calculator.ts模块里定义了一个简单的计算器实现。 这个模块同样提供了一个辅助函数来测试计算器的功能,通过传入一系列输入的字符串并在最后给出结果。

Calculator.ts
  1. export class Calculator {
  2. private current = 0;
  3. private memory = 0;
  4. private operator: string;
  5. protected processDigit(digit: string, currentValue: number) {
  6. if (digit >= "0" && digit <= "9") {
  7. return currentValue * 10 + (digit.charCodeAt(0) - "0".charCodeAt(0));
  8. }
  9. }
  10. protected processOperator(operator: string) {
  11. if (["+", "-", "*", "/"].indexOf(operator) >= 0) {
  12. return operator;
  13. }
  14. }
  15. protected evaluateOperator(operator: string, left: number, right: number): number {
  16. switch (this.operator) {
  17. case "+": return left + right;
  18. case "-": return left - right;
  19. case "*": return left * right;
  20. case "/": return left / right;
  21. }
  22. }
  23. private evaluate() {
  24. if (this.operator) {
  25. this.memory = this.evaluateOperator(this.operator, this.memory, this.current);
  26. }
  27. else {
  28. this.memory = this.current;
  29. }
  30. this.current = 0;
  31. }
  32. public handleChar(char: string) {
  33. if (char === "=") {
  34. this.evaluate();
  35. return;
  36. }
  37. else {
  38. let value = this.processDigit(char, this.current);
  39. if (value !== undefined) {
  40. this.current = value;
  41. return;
  42. }
  43. else {
  44. let value = this.processOperator(char);
  45. if (value !== undefined) {
  46. this.evaluate();
  47. this.operator = value;
  48. return;
  49. }
  50. }
  51. }
  52. throw new Error(`Unsupported input: '${char}'`);
  53. }
  54. public getResult() {
  55. return this.memory;
  56. }
  57. }
  58. export function test(c: Calculator, input: string) {
  59. for (let i = 0; i < input.length; i++) {
  60. c.handleChar(input[i]);
  61. }
  62. console.log(`result of '${input}' is '${c.getResult()}'`);
  63. }

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

TestCalculator.ts
  1. import { Calculator, test } from "./Calculator";
  2. let c = new Calculator();
  3. test(c, "1+2*33/11="); // prints 9

现在扩展它,添加支持输入其它进制(十进制以外),让我们来创建ProgrammerCalculator.ts

ProgrammerCalculator.ts
  1. import { Calculator } from "./Calculator";
  2. class ProgrammerCalculator extends Calculator {
  3. static digits = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"];
  4. constructor(public base: number) {
  5. super();
  6. const maxBase = ProgrammerCalculator.digits.length;
  7. if (base <= 0 || base > maxBase) {
  8. throw new Error(`base has to be within 0 to ${maxBase} inclusive.`);
  9. }
  10. }
  11. protected processDigit(digit: string, currentValue: number) {
  12. if (ProgrammerCalculator.digits.indexOf(digit) >= 0) {
  13. return currentValue * this.base + ProgrammerCalculator.digits.indexOf(digit);
  14. }
  15. }
  16. }
  17. // Export the new extended calculator as Calculator
  18. export { ProgrammerCalculator as Calculator };
  19. // Also, export the helper function
  20. export { test } from "./Calculator";

新的ProgrammerCalculator模块导出的API与原先的Calculator模块很相似,但却没有改变原模块里的对象。 下面是测试ProgrammerCalculator类的代码:

TestProgrammerCalculator.ts
  1. import { Calculator, test } from "./ProgrammerCalculator";
  2. let c = new Calculator(2);
  3. 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中!)