代码库结构

    针对代码库的每种主要的组织模式,在模版一节都有对应的文件。 你可以利用它们帮助你快速上手。

    首先,我们先看一下 TypeScript 声明文件能够表示的库的类型。 这里会简单展示每种类型的代码库的使用方式,以及如何去书写,还有一些真实案例。

    识别代码库的类型是书写声明文件的第一步。 我们将会给出一些提示,关于怎样通过代码库的使用方法及其源码来识别库的类型。 根据库的文档及组织结构的不同,在这两种方式中可能一个会比另外的一个简单一些。 我们推荐你使用任意你喜欢的方式。

    在为代码库编写声明文件时,你需要问自己以下几个问题。

    1. 如何获取代码库?

      比如,是否只能够从 npm 或 CDN 获取。

    2. 如何导入代码库?

      它是否添加了某个全局对象?它是否使用了或import/export语句?

    几乎所有的 Node.js 代码库都属于这一类。 这类代码库只能工作在有模块加载器的环境下。 比如,express只能在 Node.js 里工作,所以必须使用 CommonJS 的require函数加载。

    ECMAScript 2015(也就是 ES2015,ECMAScript 6 或 ES6),CommonJS 和 RequireJS 具有相似的导入一个模块的写法。 例如,对于 JavaScript CommonJS (Node.js),写法如下:

    对于 TypeScript 或 ES6,import关键字也具有相同的作用:

    1. import * as fs from 'fs';

    你通常会在模块化代码库的文档里看到如下说明:

    1. var someLib = require('someLib');

    1. });

    与全局模块一样,你也可能会在 模块的文档里看到这些例子,因此要仔细查看源码和文档。

    从代码上识别模块化代码库

    模块化代码库至少会包含以下代表性条目之一:

    • 无条件的调用requiredefine
    • import * as a from 'b';export c;这样的声明
    • 赋值给exportsmodule.exports

    它们极少包含:

    • windowglobal的赋值

    模块化代码库的模版

    有以下四个模版可用:

    你应该先阅读module.d.ts以便从整体上了解它们的工作方式。

    然后,若一个模块可以当作函数调用,则使用。

    1. const x = require('foo');
    2. // Note: calling 'x' as a function
    3. const y = x(42);
    1. var x = require('bar');
    2. // Note: using 'new' operator on the imported variable
    3. var y = new x('hello');

    如果一个模块在导入后会更改其它的模块,则使用module-plugin.d.ts

    1. const jest = require('jest');
    2. require('jest-matchers-files');

    全局代码库

    全局代码库可以通过全局作用域来访问(例如,不使用任何形式的import语句)。 许多代码库只是简单地导出一个或多个供使用的全局变量。 比如,如果你使用jQuery,那么可以使用$变量来引用它。

    你通常能够在文档里看到如何在 HTML 的 script 标签里引用代码库:

    1. <script src="http://a.great.cdn.for/someLib.js"></script>

    目前,大多数流行的全局代码库都以 UMD 代码库发布。 UMD 代码库与全局代码库很难通过文档来识别。 在编写全局代码库的声明文件之前,确保代码库不是 UMD 代码库。

    从代码来识别全局代码库

    通常,全局代码库的代码十分简单。 一个全局的“Hello, world”代码库可以如下:

    1. return 'Hello, ' + s;
    2. }

    或者这样:

    1. window.createGreeting = function (s) {
    2. return 'Hello, ' + s;
    3. };

    在阅读全局代码库的代码时,你会看到:

    • 顶层的var语句或function声明
    • 一个或多个window.someName赋值语句
    • 假设 DOM 相关的原始值documentwindow存在

    你不会看到:

    • 检查或使用了模块加载器,如requiredefine
    • CommonJS/Node.js 风格的导入语句,如var fs = require("fs");
    • define(...)调用
    • 描述require或导入代码库的文档

    全局代码库的示例

    由于将全局代码库转换为 UMD 代码库十分容易,因此很少有代码库仍然使用全局代码库风格。 然而,小型的代码库以及需要使用 DOM 的代码库仍然可以是全局的。

    全局代码库的模版

    模版文件global.d.ts定义了myLib示例代码库。 请务必阅读。

    UMD

    一个 UMD 模块既可以用作 ES 模块(使用导入语句),也可以用作全局变量(在缺少模块加载器的环境中使用)。 许多流行的代码库,如,都是使用这模式发布的。 例如,在 Node.js 中或使用了 RequireJS 时,你可以这样使用:

    1. import moment = require('moment');
    2. console.log(moment.format());

    在纯浏览器环境中,你可以这样使用:

      识别 UMD 代码库

      会检查运行环境中是否存在模块加载器。 这是一种常见模式,示例如下:

      1. (function (root, factory) {
      2. if (typeof define === "function" && define.amd) {
      3. define(["libName"], factory);
      4. } else if (typeof module === "object" && module.exports) {
      5. module.exports = factory(require("libName"));
      6. } else {
      7. root.returnExports = factory(root.libName);
      8. }
      9. }(this, function (b) {

      如果你看到代码库中存在类如typeof definetypeof windowtypeof module的检测代码,尤其是在文件的顶端,那么它大概率是 UMD 代码库。

      在 UMD 模块的文档中经常会提供在 Node.js 中结合require使用的示例,以及在浏览器中结合<script>标签使用的示例。

      大多数流行的代码库均提供了 UMD 格式的包。 例如,jQuery,和lodash等。

      模版

      使用module-plugin.d.ts模版。

      全局插件

      一个全局插件是全局代码,它们会改变全局对象的结构。 对于全局修改的模块,在运行时存在冲突的可能。

      比如,一些库往Array.prototypeString.prototype里添加新的方法。

      识别全局插件

      你会看到像下面这样的例子:

      模版

      使用global-plugin.d.ts模版。

      全局修改的模块

      当一个全局修改的模块被导入的时候,它们会改变全局作用域里的值。 比如,存在一些库它们添加新的成员到当导入它们的时候。 这种模式很危险,因为可能造成运行时的冲突, 但是我们仍然可以为它们书写声明文件。

      识别全局修改的模块

      全局修改的模块通常可以很容易地从它们的文档识别出来。 通常来讲,它们与全局插件相似,但是需要require调用来激活它们的效果。

      你可能会看到像下面这样的文档:

      1. // 'require' call that doesn't use its return value
      2. var unused = require('magic-string-time');
      3. /* or */
      4. require('magic-string-time');
      5. var x = 'hello, world';
      6. // Creates new methods on built-in types
      7. console.log(x.startsWithHello());
      8. var y = [1, 2, 3];
      9. // Creates new methods on built-in types
      10. console.log(y.reverseAndSort());

      模版

      使用global-modifying-module.d.ts模版。

      你的代码库可能会有若干种依赖。 本节会介绍如何在声明文件中导入它们。

      对全局库的依赖

      如果你的代码库依赖于某个全局代码库,则使用/// <reference types="..." />指令:

      1. /// <reference types="someLib" />
      2. function getThing(): someLib.thing;

      如果你的代码库依赖于某个模块,则使用import语句:

      1. import * as moment from 'moment';
      2. function getThing(): moment;

      对 UMD 模块的依赖

      全局代码库

      如果你的全局代码库依赖于某个 UMD 模块,则使用/// <reference types指令:

      1. /// <reference types="moment" />
      2. function getThing(): moment;

      ES 模块或 UMD 模块代码库

      如果你的模块或 UMD 代码库依赖于某个 UMD 代码库,则使用import语句:

      1. import * as someLib from 'someLib';

      不要使用/// <reference指令来声明对 UMD 代码库的依赖。

      防止命名冲突

      注意,虽说可以在全局作用域内定义许多类型。 但我们强烈建议不要这样做,因为当一个工程中存在多个声明文件时,它可能会导致难以解决的命名冲突。

      可以遵循的一个简单规则是使用代码库提供的某个全局变量来声明拥有命名空间的类型。 例如,如果代码库提供了全局变量cats,那么可以这样写:

      1. declare namespace cats {
      2. interface KittySettings {}
      3. }

      而不是:

      这样做会保证代码库可以被转换成 UMD 模块,且不会影响声明文件的使用者。

      ES6 对模块插件的影响

      一些插件会对已有模块的顶层导出进行添加或修改。 这在 CommonJS 以及其它模块加载器里是合法的,但 ES6 模块是不可改变的,因此该模式是不可行的。 因为,TypeScript 是模块加载器无关的,所以在编译时不会对该行为加以限制,但是开发者若想要转换到 ES6 模块加载器则需要注意这一点。

      许多代码库,如 Express,将自身导出为可调用的函数。 例如,Express 的典型用法如下:

      1. var app = exp();

      在 ES6 模块加载器中,顶层对象(此例中就exp)只能拥有属性; 顶层的模块对象永远不能够被调用。