Skip to content
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待

库结构

总的来说,你构建声明文件的方式取决于库的使用方式。 在 JavaScript 中,有多种提供库以供消费的方式,你需要编写与之匹配的声明文件。 本指南涵盖如何识别常见的库模式,以及如何编写与该模式对应的声明文件。

每种主要的库结构模式在模板部分都有对应的文件。 你可以从这些模板入手,帮助你更快地开始。

识别库的类型

首先,我们将回顾 TypeScript 声明文件可以表示的库类型。 我们将简要说明每种库的使用方式、编写方式,并列举一些现实世界中的示例库。

识别库的结构是编写其声明文件的第一步。 我们将根据库的使用方式和代码给出识别结构的提示。 根据库的文档和组织方式,其中一种可能比另一种更容易。 我们建议使用你觉得更舒服的方式。

你应该寻找什么?

在查看你试图为其编写类型的库时,要问自己几个问题。

  1. 你如何获取该库?

    例如,你只能通过 npm 获取它,还是只能从 CDN 获取?

  2. 你如何导入它?

    它是添加一个全局对象吗?它使用 requireimport/export 语句吗?

不同类型库的简要示例

模块库

几乎所有现代的 Node.js 库都属于模块家族。 这类库只能在具有模块加载器的 JS 环境中工作。 例如,express 只能在 Node.js 中工作,并且必须使用 CommonJS require 函数加载。

ECMAScript 2015(也称为 ES2015、ECMAScript 6 和 ES6)、CommonJS 和 RequireJS 在导入一个模块方面有相似的概念。 例如,在 JavaScript CommonJS(Node.js)中,你会写:

js
var fs = require("fs");

在 TypeScript 或 ES6 中,import 关键字服务于相同的目的:

ts
import * as fs from "fs";

你通常会在模块库的文档中看到以下行之一:

js
var someLib = require("someLib");

或者

js
define(..., ['someLib'], function(someLib) {

});

与全局模块一样,你可能会在 UMD 模块的文档中看到这些示例,因此请务必检查代码或文档。

从代码识别模块库

模块库通常至少具有以下特征之一:

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

它们很少会有:

  • windowglobal 属性的赋值

模块的模板

有四个可用于模块的模板, module.d.tsmodule-class.d.tsmodule-function.d.tsmodule-plugin.d.ts

你应该首先阅读 module.d.ts,以了解它们的工作原理。

如果你的模块可以像函数一样调用,则使用模板 module-function.d.ts

js
const x = require("foo");
// 注意:将 'x' 作为函数调用
const y = x(42);

如果你的模块可以使用 new 构造,则使用模板 module-class.d.ts

js
const x = require("bar");
// 注意:在导入的变量上使用 'new' 运算符
const y = new x("hello");

如果你有一个模块,当导入时会对其他模块进行更改,则使用模板 module-plugin.d.ts

js
const jest = require("jest");
require("jest-matchers-files");

全局库

全局库是指可以从全局作用域访问的库(即无需使用任何形式的 import)。 许多库只是暴露一个或多个全局变量供使用。 例如,如果你使用 jQuery,可以直接通过引用 $ 变量来使用它:

ts
$(() => {
  console.log("hello!");
});

你通常会在全局库的文档中看到如何在 HTML script 标签中使用该库的说明:

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

如今,大多数流行的全局可访问库实际上都是作为 UMD 库编写的(见下文)。 UMD 库的文档很难与全局库的文档区分开来。 在编写全局声明文件之前,请确保该库实际上不是 UMD。

从代码识别全局库

全局库代码通常极其简单。 一个全局的“Hello, world”库可能看起来像这样:

js
function createGreeting(s) {
  return "Hello, " + s;
}

或者像这样:

js
// Web
window.createGreeting = function (s) {
  return "Hello, " + s;
};

// Node
global.createGreeting = function (s) {
  return "Hello, " + s;
};

// 可能是任何运行时
globalThis.createGreeting = function (s) {
  return "Hello, " + s;
};

在查看全局库的代码时,你通常会看到:

  • 顶层 var 语句或 function 声明
  • window.someName 的一次或多次赋值
  • 假定存在 documentwindow 等 DOM 原语

不会看到:

  • 检查或使用模块加载器(如 requiredefine
  • 形式为 var fs = require("fs"); 的 CommonJS/Node.js 风格导入
  • 调用 define(...)
  • 描述如何 require 或导入库的文档

全局库示例

由于将全局库转换为 UMD 库通常很容易,因此很少有流行的库仍然采用全局风格编写。 然而,那些较小且依赖 DOM(或没有依赖项)的库可能仍然是全局的。

全局库模板

模板文件 global.d.ts 定义了一个示例库 myLib。 请务必阅读“防止名称冲突”脚注

UMD

UMD 模块是指既可以作为模块(通过导入)使用,也可以作为全局变量(在没有模块加载器的环境中运行时)使用的模块。 许多流行的库,如 Moment.js,都是以这种方式编写的。 例如,在 Node.js 或使用 RequireJS 时,你会写:

ts
import moment = require("moment");
console.log(moment.format());

而在纯浏览器环境中,你会写:

js
console.log(moment.format());

识别 UMD 库

UMD 模块会检查是否存在模块加载器环境。 这是一种容易识别的模式,看起来像这样:

js
(function (root, factory) {
    if (typeof define === "function" && define.amd) {
        define(["libName"], factory);
    } else if (typeof module === "object" && module.exports) {
        module.exports = factory(require("libName"));
    } else {
        root.returnExports = factory(root.libName);
    }
}(this, function (b) {

如果在库的代码中看到对 typeof definetypeof windowtypeof module 的检查,尤其是在文件顶部,那么它几乎总是一个 UMD 库。

UMD 库的文档也通常会展示一个“在 Node.js 中使用”的示例,显示 require, 以及一个“在浏览器中使用”的示例,显示使用 <script> 标签加载脚本。

UMD 库示例

大多数流行的库现在都以 UMD 包的形式提供。 示例包括 jQueryMoment.jslodash 等等。

模板

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

使用依赖项

你的库可能有几种依赖项。 本节介绍如何将它们导入声明文件。

对全局库的依赖

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

ts
/// <reference types="someLib" />

function getThing(): someLib.thing;

对模块的依赖

如果你的库依赖于一个模块,请使用 import 语句:

ts
import * as moment from "moment";

function getThing(): moment;

对 UMD 库的依赖

从全局库

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

ts
/// <reference types="moment" />

function getThing(): moment;

从模块或 UMD 库

如果你的模块或 UMD 库依赖于一个 UMD 库,请使用 import 语句:

ts
import * as someLib from "someLib";

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

脚注

防止名称冲突

请注意,在编写全局声明文件时,可以在全局作用域中定义许多类型。 我们强烈反对这样做,因为当项目中存在多个声明文件时,这可能导致无法解决的名称冲突。

一个简单的规则是,只声明命名空间在库定义的任何全局变量下的类型。 例如,如果库定义了全局值 'cats',你应该写:

ts
declare namespace cats {
  interface KittySettings {}
}

不要写:

ts
// 在顶层
interface CatsKittySettings {}

这一指导也确保了库可以过渡到 UMD 而不会破坏声明文件的使用者。

ES6 对模块调用签名的影响

许多流行的库,如 Express,在导入时将自己暴露为可调用的函数。 例如,典型的 Express 用法如下:

ts
import exp = require("express");
var app = exp();

在符合 ES6 的模块加载器中,顶层对象(此处导入为 exp)只能具有属性; 顶层模块对象永远不能是可调用的。

这里最常见的解决方案是为可调用/可构造的对象定义一个 default 导出; 模块加载器通常会自动检测这种情况,并将顶层对象替换为 default 导出。 如果你的 tsconfig.json 中设置了 "esModuleInterop": true,TypeScript 可以为你处理这一点。