名字空间和模块

状态: 初稿

对命名的说明:
有必要说明, 在 TypeScript 1.5 中, 一些术语发生了变化.
“内部模块” 现在叫做 “名字空间”.
“外部模块” 简称 “模块”, 这是出于同 ECMAScript 2015 的命名保持一致的考虑, (module X { 等同于现在提出的 namespace X {).

介绍

本章概述在 TypeScript 中结合名字空间和模块组织你程序的若干方法.
我们将复习一些使用名字空间和模块的高级主题, 学会如何应对实际操练中的常见陷阱.

阅读模块获得更多关于模块的信息.
阅读名字空间获得更多关于名字空间的信息.

使用名字空间

事实上, 名字空间只是 JavaScript 全局空间中的命名对象.
这也是它易用的直接原因.
名字空间可以跨越多个文件, 最后利用 --outFile 选项合并.
名字空间是组织你 Web 应用程序的优良选择, 所有依赖都在 HTML 页面用 <script> 标签包含.

它也有全局空间污染的问题, 特别是对于大型应用程序, 组件依赖关系有时很难确定.

使用模块

同样作为组织程序的方法, 模块也能容纳声明和实现.
不同的是模块声明自己的依赖.

模块还依赖模块加载器(比如 CommonJs/Require.js).
它或许不是小 JS 应用程序的最佳选择, 但对于大型应用程序, 这点代价换来的是长期保持模块化和可维护的优势.
模块能带来更好的代码重用机会, 更强的隔离性, 更完善的打包工具支持.

同样值得一提的是, 针对 Node.js 程序, 模块是默认以及被推荐的组织你程序的方式.

从 ECMAScript 2015 起, 模块成为一项语言特性, 应当被所有兼容引擎实现支持.
因此, 模块应被推荐作为新项目的代码组织机制.

名字空间及模块中的陷阱

在这节, 我们列举多种使用名字空间或模块的常见陷阱, 以及规避它们的方法.

/// <reference> 引用一个模块

一个常见错误是试图用 /// <reference ... /> 语法引用一个模块文件, 而不是 import 语句.
为厘清两者差异, 首先我们要理解编译器是如何根据 import 路径 (即 import x from "...";, import x = require("..."); 等语句的 ... 部分.) 定位一个模块的类型信息的.

编译器试图以适当路径查找 .ts, .tsx, 然后是 .d.ts 文件.
如果编译器无法找到这个文件, 它随即查找外部模块定义.
回忆一下, 这些信息在 .d.ts 文件声明.

  • myModules.d.ts

    1
    2
    3
    4
    // In a .d.ts file or .ts file that is not a module:
    declare module "SomeModule" {
    export function fn(): string;
    }
  • myOtherModule.ts

    1
    2
    /// <reference path="myModules.d.ts" />
    import * as m from "SomeModule";

这里的 reference 标签允许我们定位包含外部模块声明的声明文件.
这也是许多 TypeScript 示例使用 node.d.ts 的格式.

滥用名字空间

当你把程序从名字空间转移到模块, 很容易产生这样的文件:

  • shapes.ts

    1
    2
    3
    4
    export namespace Shapes {
    export class Triangle { /* ... */ }
    export class Square { /* ... */ }
    }

TriangleSquare 放进顶层模块 Shapes 是没必要的.
你模块的用户会为此头晕, 厌烦:

  • shapeConsumer.ts

    1
    2
    import * as shapes from "./shapes";
    let t = new shapes.Shapes.Triangle(); // shapes.Shapes?

TypeScript 模块的一个关键特征是两个不同模块绝不会往同一个空间贡献标识符.
因为模块用户最终会为模块分配一个名字, 没必要提前把所有导出标识符放进额外的名字空间.

再次强调, 名字空间要解决的问题是提供逻辑分组, 和避免命名冲突.
而模块文件本身就是一个逻辑分组, 它顶层名字由导入它的代码定义, 额外地为导出项创建一个模块层是没必要的.

修改后的示例如下:

  • shapes.ts

    1
    2
    export class Triangle { /* ... */ }
    export class Square { /* ... */ }
  • shapeConsumer.ts

    1
    2
    import * as shapes from "./shapes";
    let t = new shapes.Triangle();

模块的问题

正如 JS 文件和模块是一对一关系, TypeScript 模块源文件与编译后的 JS 文件同样是一对一关系.
一个问题是取决于你面向的模块系统, 有时不可能合并多个模块源文件.
例如, 面向 commonjsumd 时, outFile 选项将不可用, 但对 TypeScript 1.8 或更高版本, 你可以面向 amdsystem 合并文件.

如果这篇文章对您有用,可以考虑打赏:)