一、概述(Overview)
总体上说,声明文件的结构,取决于所使用库的类型。JavaScript 中,库可以有有很多种提供形式(结构),写声明文件的时候,需要与库的提供形式匹配。本文主旨在于如何识别库的提供形式,以及为对应形式编写声明文件的方法。
本身中后续提及提供形式、库结构、库类型指的是一回事,基本上可以认为是库的模块化形式以及使用方式,如:全局、模块等。
二、识别库类型(Identifying Kinds of Libraries)
首先,看下一 TypeScript 提供的库声明文件类型,简单展示一下每种类型如何使用、如何编写,以及从实际场景中,列举出一些样例。
为库编写声明文件的第一步,是识别库的结构。我们给出的识别库结构的方法,都是取决于使用方式和库的代码。这两种方法中,使用方式要看库文档中的说明,而代码就看其中具体的代码组织形式了。推荐使用自己觉得舒适的方式。
1、全局库(Global Libraries)
全局库指的是可以在全局作用域使用的库,比如:不需要任何形式的 import
。很多库简单暴露出几个全局变量使用。比如:jQuery 中的 $
变量。
$(() => { console.log('hello!'); } );
经常会看到这种全局库的文档中,给出的如何在 HTML 标签中的使用方法:
如今大多数提供“全局操作形式”的库,实际上都使用的是 UMD 形式。全局库与 UMD 通过文档还是难以区分的,在为全局库编写声明文件之前,先确保其不是 UMD 的。
(1)通过代码识别全局库
全局库代码使用非常简单,一个 “Hello, world” 的全局库,看起来是这样的:
function createGreeting(s) {
return "Hello, " + s;
}
或者这样:
window.createGreeting = function(s) {
return "Hello, " + s;
}
观察全局库代码,通常可以看到:
- 顶层的
var
声明,或者function
声明 - 一个或多个给
window
下属性的赋值,如:window.someName
- 假设
window
和document
这样的 DOM 基本类型存在
不会看到:
- 模块加载器的侦测或使用,如:
require
或define
- CommonJS/Node.js 风格的引入形式,如:
var fs = require("fs");
- 调用
define(...)
- 文档描述怎样
require
或impot
库
(2)全局库
样例
由于将全局库转换为 UMD 非常容易,所以流行库很少再以全局库的形式提供。但是一些小型库并且需要 DOM 操作的,或者不需要其他依赖的库,可能仍然以“全局”形式提供。
(3)全局库
模板
global.d.ts 模板定义了一个 myLib
库。请确保阅读 避免命名冲突 的说明。
2、模块库(Modular Libraries)
一些库只在具备模块加载器的环境下才能使用。例如:由于 express
库只在 Node.js
环境下工作,并且使用 CommonJS 的 require
功能。
ECMAScript 2015(也称为 ES2015、ECMAScript 6 和 ES6)、CommonJS 和 RequireJS 对于引入模块具备相似的概念。
以下列举一些书写方式:
- CommonJS(Node.js)
var fs = require("fs");
- TypeScript、ES6 中,
import
关键字提供相同的功能
import fs = require("fs");
在模块库的文档中,可以看到一些代表性的字样,如:
var someLib = require('someLib');
或
define(..., ['someLib'], function(someLib) {
});
对于 UMD 库中,虽然也是提供全局功能,但是一样会在其中看到上述的样例,所以要确保查看代码或文档。
(1)通过代码识别模块库
模块库至少具备以下特点中的一些:
- 非条件地调用
require
或define
,这一点区别于 UMD - 类似
import * as a from 'b';
或export c;
这样的声明 - 给
exports
或module.exports
赋值
很少具备:
- 对于
window
或global
的属性的赋值
(2)模块库
样例
很多流行的 Node.js
库是模块库,如:express
、gulp
和 request
。
3、UMD
UMD 模块,既可以作为模块使用(通过引入),也可以全局(在没有模块加载器的环境)使用。很多流行库,如:Moment.js,就是以 UMD 的形式编写的。
在 Node.js 或者 RequireJS 中,可以这样写:
import moment = require("moment");
console.log(moment.format());
而在普通浏览器中,可以这样写:
console.log(moment.format());
(1)识别 UMD
库
UMD 模块检查模块加载器环境是否存在。
(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 define
、typeof window
或 typeof module
的测试,尤其是在文件顶部,基本上就是 UMD 库了。
UMD 库的文档也经常会演示 “Using in Node.js” 样例,展示如何使用 require
,以及 “Using in the browser” 样例展示如何使用 标签加载脚本。
(2)UMD 库
样例
现在大部分流行库都支持 UMD 包。例如:jQuery、Moment.js 和 lodash,等等。
(3)UMD 库
模板
有三种模板:
-
module-function.d.ts
如果模块可以像function
一样调用时使用
var x = require("foo");
// Note: calling 'x' as a function
var y = x(42);
确保阅读 footnote “The Impact of ES6 on Module Call Signatures”
-
module-class.d.ts
如果模块可以通过new
创建时使用
var x = require("bar");
// Note: using 'new' operator on the imported variable
var y = new x("hello");
确保阅读 footnote “The Impact of ES6 on Module Call Signatures”
-
module.d.ts
如果库不能像函数一样使用,也不能通过new
来构建实例,使用这种方式。
4、Module Plugin
和 UMD Plugin
Module Plugin
可以修改另一个模块(包括 UMD 和其他类型模块)的原型。比如:Moment.js 中,moment-range
为 moment
对象添加了新的 range
方法。
对于编写声明文件而言,普通模块与 UMD 的编写方式是相同的。
Module Plugin
和 UMD Plugin
使用的模板
module-plugin.d.ts
5、Global Plugin
Global Plugin
是全局代码,用于修改全局内容的原型。与 global-modifying modules
(后面会详细介绍)一样,会增加运行时冲突的可能性。
例如:一些库为 Array.prototype
或 String.prototype
增加新方法。
(1)识别 Global Plugin
Global Plugin
通常容易通过文档分辨。会看到类似这样的例子:
var x = "hello, world";
// Creates new methods on built-in types
console.log(x.startsWithHello());
var y = [1, 2, 3];
// Creates new methods on built-in types
console.log(y.reverseAndSort());
(2)Global Plugin
使用的模板
global-modifying-module.d.ts
6、Global-modifying Modules
Global-modifying Modules
在被引入时,修改全局存在的值。例如:一个库在被引入时,为 String.prototype
增加新成员。由于这种模式存在引发运行时冲突的可能性,所以存在一定风险,但还是可以为其编写声明文件。
(1)识别 Global-modifying Modules
Global-modifying modules
通常易于通过其文档进行分辨。通常情况,与 Global Plugin
相似,但是需要调用 require
来激活其效果。
可能会看到这样的文档:
// 'require' call that doesn't use its return value
var unused = require("magic-string-time");
/* or */
require("magic-string-time");
var x = "hello, world";
// Creates new methods on built-in types
console.log(x.startsWithHello());
var y = [1, 2, 3];
// Creates new methods on built-in types
console.log(y.reverseAndSort());
(2)Global-modifying Modules
使用的模板
global-modifying-module.d.ts
三、依赖(Consuming Dependencies)
1、全局库依赖(Dependencies on Global Libraries)
如果你的库依赖于一个全局库,使用 ///
指示符:
///
function getThing(): someLib.thing;
2、模块依赖(Dependencies on Modules)
如果你的库依赖于一个模块,使用 import
声明:
import * as moment from "moment";
function getThing(): moment;
3、UMD 库依赖(Dependencies on UMD libraries)
(1)全局库依赖 UMD(From a Global Library)
如果你的全局库依赖于一个 UMD 模块,使用 ///
指示符:
///
function getThing(): moment;
(2)模块或 UMD 库依赖 UMD 库(From a Module or UMD Library)
如果你的模块或者 UMD 库依赖于一个 UMD 库,使用 import
声明:
import * as someLib from 'someLib';
不要使用 ///
四、补充说明(Footnotes)
1、避免命名冲突(Preventing Name Conflicts)
我们注意到,在编写全局声明文件时,可以在其中定义很多全局作用域的类型。当工程中存在多个声明文件的时候,会导致无法解决的明明冲突。
遵循一个简单的规则:只声明库定义的全局变量的命名空间的类型。比如:如果库定义了全局值 cats
,可以这样写:
declare namespace cats {
interface KittySettings { }
}
而不是
// at top-level
interface CatsKittySettings { }
这种方式也能确保库可以过渡到 UMD,而不影响声明文件的使用。
2、ES6 对于 Module Plugins
的影响(The Impact of ES6 on Module Plugins)
一些插件增加或修改已存在模块的 top-level exports,这对于 CommonJS 和其他加载器是合法的。但是 ES6 模块被认为是不可修改的,并且这种模式是不可能的。由于 TypeScript 是加载器不定(loader-agnostic)的,所以这种策略不会在编译时强制执行,但是尝试转换到 ES6 模块加载器的开发者需要意识到这一点。
3、ES6 对于模块调用签名的影响(The Impact of ES6 on Module Call Signatures)
很多流行库,比如 Express,在被引入时,将自己暴露为一个可调用函数。例如:Express 的典型用法是这样的:
import exp = require("express");
var app = exp();
ES6 的模块加载器中,顶层对象(top-level object,这里被引入为 exp
)只能具备属性,顶层模块对象(top-level module object)不可被调用。最常用的解决方案,是为可调用对象/可构建对象定义一个 default
输出。一些模块加载器 shims 会自动检测这个情况,并且使用 default
输出来替换顶层对象。
五、参考资料
译自 TypeScript Declaration Files - Library Structures
(完)