ts-learnings/14_namespaces.md
2022-05-26 11:05:02 +08:00

11 KiB
Raw Blame History

命名空间

关于术语的一点说明在TypeScript 1.5中需要注意的是其中的命名法发生了改变。为了与ECMAScript 2015的命名法保持一致"内部模块"以及被命名为“命名空间”。“外部模块”已被简化为“模块”,(名以上module X {与现在所指的namespace X{是等价的。it's important to note that in TypeScript 1.5, the nomenclature has changed. "Internal modules" are now "namespace". "External modules" are now simply "modules", as to align with ECMAScript 2015's terminology, (namely that module X { is equivalent to the now-preferred namespace X{))。

简介

本文指出了TypeScript中使用命名空间也就是先前的“内部模块”来组织代码的不同方法。正如在有关命名法的注释中所暗示的现已使用“命名空间”来指代“内部模块”了。此外在将module关键字用于声明某个内部模块时,都可以且应当使用namespace关键字。这样做可避免由相似命名的用词带来的负担,而令到新用户迷惑。

第一步First Steps

这里以一个将贯穿本章节的示例开始。因为可能要对用户在web页中表单上的输入或对某个外部提供的数据文件的格式进行检查前面在模块章节曾编写了一个小的简化字符串验证器合集。

单个文件中的验证器

interface StringValidator {
    isAcceptable (s: string): boolean;
}

let lettersRegexp = /^[A-Za-z]+$/;
let numberRegexp = /^[0-9]+$/;

class LettersOnlyValidator implements StringValidator {
    isAcceptable (s: string) {
        return lettersRegexp.test(s);
    }
}

class ZipCodeValidator implements StringValidator {
    isAcceptable(s: string) {
        return s.length === 5 && numberRegexp.test(s);
    }
}

// 一些尝试样本
let strings = ["Hello", "98052", "101"];


// 要使用的验证器
let validators: { [s: string]: StringValidator; } = {};

validators["ZIP code"] = new ZipCodeValidator();
validators["Letters only"] = new LettersOnlyValidator();


// 展示各个字符串是否通过各个验证器验证
for (let s of strings) {
    for (let name in validators) {
        let isMatch = validators[name].isAcceptable(s);
        console.log(`"${ s }" ${ isMatch ? "matches" : "does not match" } "${ name }".`);
    }
}

命名空间化Namespacing

随着更多验证器的加入就将想有着某种组织方案从而可对类型加以跟踪并不必担心与其它对象的名称冲突。可将这些对象封装到命名空间以取代将很多不同名称放到全局命名空间的落后方式As we add more validators, we're going to want to have some kind of organization scheme so that we can keep track of our types and not worry about name collisions with other objects. Instead of putting lots of different names into the global namespace, let's wrap up our objects into a namespace

在本例中,将把所有验证器相关的实体,移入到一个叫做Validation的命名空间中。因为想要这里的接口与类对外部可见,所以要使用export来为它们建立索引。反过来,变量lettersRegexpnumberRegexp则是实现细节,因此它们会保持非导出状态,且对命名空间外部不可见。在该文件底部的测试代码中,在命名空间外部使用这些类型时,就需要对这些类型的名称进行修饰了,比如Validation.LettersOnlyValidator

已命名空间化的验证器

namespace Validation {
    export interface StringValidator {
        isAcceptable (s: string): boolean;
    }

    const lettersRegexp = /^[A-Za-z]+$/;
    const numberRegexp = /^[0-9]+$/;

    export class LettersOnlyValidator implements StringValidator {
        isAcceptable (s: string) {
            return lettersRegexp.test(s);
        }
    }

    export class ZipCodeValidator implements StringValidator {
        isAcceptable(s: string) {
            return s.length === 5 && numberRegexp.test(s);
        }
    }
}

// 一些尝试样本
let strings = ["Hello", "98052", "101"];

// 要使用的验证器
let validators: { [s: string]: Validation.StringValidator; } = {};

validators["ZIP code"] = new Validation.ZipCodeValidator();
validators["Letters only"] = new Validation.LettersOnlyValidator();


// 展示各个字符串是否通过各个验证器验证
for (let s of strings) {
    for (let name in validators) {
        let isMatch = validators[name].isAcceptable(s);
        console.log(`"${ s }" ${ isMatch ? "matches" : "does not match" } "${ name }".`);
    }
}

拆分到多个文件Splitting Across Files

随着应用日益增长,就有将代码拆分到多个文件的想法,目的是令代码易于维护。

多文件命名空间Multi-file namespace

下面将把上面的命名空间Validation拆分到许多文件Here, we'll split our Validation namespace across many files。尽管这些文件是分立的它们却都能贡献到同一命名空间且可以像是定义在一处那样被消费。因为文件之间存在依赖所以将添加 参考标志reference tags来告诉编译器文件之间的关系。此外测试代码并没有改动。

Validation.ts

namespace Validation {
    export interface StringValidator {
        isAcceptable (s: string): boolean;
    }
}

LettersOnlyValidator.ts

/// <reference path="Validation.ts">
namespace Validation {
    const lettersRegexp = /^[A-Za-z]+$/;

    export class LettersOnlyValidator implements StringValidator {
        isAcceptable (s: string) {
            return lettersRegexp.test(s);
        }
    }
}

ZipCodeValidator.ts

/// <reference path="Validation.ts">
namespace Validation {
    const numberRegexp = /^[0-9]+$/;

    export class ZipCodeValidator implements StringValidator {
        isAcceptable(s: string) {
            return s.length === 5 && numberRegexp.test(s);
        }
    }
}

Test.ts

/// <reference path="Validation.ts">
/// <reference path="LettersOnlyValidator.ts">
/// <reference path="ZipCodeValidator.ts">

// 一些尝试样本
let strings = ["Hello", "98052", "101"];

// 要使用的验证器
let validators: { [s: string]: Validation.StringValidator; } = {};

validators["ZIP code"] = new Validation.ZipCodeValidator();
validators["Letters only"] = new Validation.LettersOnlyValidator();


// 展示各个字符串是否通过各个验证器验证
for (let s of strings) {
    for (let name in validators) {
        let isMatch = validators[name].isAcceptable(s);
        console.log(`"${ s }" ${ isMatch ? "matches" : "does not match" } "${ name }".`);
    }
}

在涉及到多个文件时,就需要确保所有已编译的代码得到加载。而确保所有已编译代码得到加载的方式,有两种。

第一种可使用级联输出concatenated output。就是使用--outFile编译选项,来将所有输入文件,编译到一个单独的输出文件中。

tsc --outFile sample.js Test.ts

基于这些文件中所出现的参考标志,编译器将自动对输出文件进行排序。还可以对各个文件进行单独指定:

tsc --outFile sample.js Validation.ts LettersOnlyValidator.ts ZipCodeValidator.ts Test.ts

第二种方式就是各个文件的单独编译这是默认的做法从而为每个输入文件都生成一个JavaScript文件。在产生了多个JS文件后就需要在网页上使用<script>标签,来将各个生成的文件以适当顺序进行加载,比如:

MyTestPage.html片段

<script src="Validation.js" type="text/javascript" />
<script src="LettersOnlyValidarot.js" type="text/javascript" />
<script src="ZipCodeValidator.js" type="text/javascript" />
<script src="Test.js" type="text/javascript" />

别名Alias

另一种可简化命名空间使用的方式,就是使用import q = x.y.z来为一些常用对象创建较短名称Another way that you can simplify working with of namespaces is to use import q = x.y.z to create shorter names for commonly-used objects。请将此种语法不要与用于加载模块的import x = require("name")语法搞混此种语法只是简单地创建一个指定符号的别名。对于任何种类的标识符包括模块导入项所建立的对象都可以使用这类的导入通常被称作别名You can use these sorts of imports(commonly referred to as aliases) for any kind of identifier, including objects created from module imports

namespace Shapes {
    export namespace Polygons {
        export class Triangle {}
        export class Square {}
    }
}

import polygons = Shapes.Polygons;
let sq = new polygons.Square(); // 与 `new Shapes.Polygons.Square()` 效果一样

注意这里没有使用require关键字;而是直接从所导入的符号的合格名称进行赋值。这就与使用var关键字类似,但也在所导入的符号的类型与命名空间涵义上有效。重要的是,对于数值来说,import则相对于原始符号,是不同的引用,因此对别名化的var的修改将不会反映到原始变量instead we assign directly from the qualified name of the symbol we're importing. This is similar to using var, but also works on the type and namespace meanings of the imported symbol. Importantly, for values, import is a distinct reference from the original symbol, so changes to an aliased var will not be reflected in the original variable

与其它JavaScript库的交互Working with Other JavaScript Libraries

为对那些不是以TypeScript编写的库的外形进行描述需要声明该库所暴露出的API。因为大多数的JavaScript库都仅会暴露少数几个的顶级对象所以命名空间是一种对这些库进行表示的好方式To describe the shape of libraries not written in TypeScript, we need to declare the API that the library exposes. Because most JavaScript libraries expose only a few top-level objects, namespaces are a good way to represent them

对于那些没有对实现进行定义的声明,这里将其成为“外围”声明。外围声明通常都是定义在.d.ts文件中的。如对C/C++较为熟悉,那么可将这些.d.ts文件,看作是.h文件。下面是一些示例。

外围命名空间Ambient Namespaces

流行库D3就是在叫做d3的全局对象中定义其功能的。因为该库是通过<script>标签而不是模块加载器加载的所以其声明使用了命名空间来定义其外形。要让TypeScript的编译器看到该外形就要使用外围命名空间声明an ambient namespace declaration。比如可像下面这样开始编写该外围命名空间声明

D3.d.ts (简化摘抄)

declare namespace D3 {
    export interface Selectors {
        select: {
            (selector: string): Selection;
            (element: EventTarget): Selection;
        }
    }

    export interface Event {
        x: number;
        y: number;
    }

    export interface Base extends Selectors {
        event: Event;
    }
}

declare var d3: D3.Base;