【译】TypeScript 类型声明文件(.d.ts)编写之库结构(Library Structures)

一、概述(Overview)

总体上说,声明文件的结构,取决于所使用库的类型。JavaScript 中,可以有有很多种提供形式(结构),写声明文件的时候,需要与库的提供形式匹配。本文主旨在于如何识别库的提供形式,以及为对应形式编写声明文件的方法。
  本身中后续提及提供形式、库结构、库类型指的是一回事,基本上可以认为是库的模块化形式以及使用方式,如:全局、模块等。

二、识别库类型(Identifying Kinds of Libraries)

首先,看下一 TypeScript 提供的库声明文件类型,简单展示一下每种类型如何使用、如何编写,以及从实际场景中,列举出一些样例。
  为库编写声明文件的第一步,是识别库的结构。我们给出的识别库结构的方法,都是取决于使用方式和库的代码。这两种方法中,使用方式要看库文档中的说明,而代码就看其中具体的代码组织形式了。推荐使用自己觉得舒适的方式。

1、全局库(Global Libraries)

全局库指的是可以在全局作用域使用的库,比如:不需要任何形式的 import。很多库简单暴露出几个全局变量使用。比如:jQuery 中的 $ 变量。

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

经常会看到这种全局库的文档中,给出的如何在 HTML <script></script> 标签中的使用方法:

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

如今大多数提供“全局操作形式”的库,实际上都使用的是 UMD 形式。全局库UMD 通过文档还是难以区分的,在为全局库编写声明文件之前,先确保其不是 UMD 的。

(1)通过代码识别全局库

全局库代码使用非常简单,一个 “Hello, world” 的全局库,看起来是这样的:

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

或者这样:

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

观察全局库代码,通常可以看到:

  • 顶层的 var 声明,或者 function 声明
  • 一个或多个给 window 下属性的赋值,如:window.someName
  • 假设 windowdocument 这样的 DOM 基本类型存在

不会看到:

  • 模块加载器侦测使用,如:requiredefine
  • CommonJS/Node.js 风格的引入形式,如:var fs = require("fs");
  • 调用 define(...)
  • 文档描述怎样 requireimpot
(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)通过代码识别模块库

模块库至少具备以下特点中的一些:

  • 非条件地调用 requiredefine,这一点区别于 UMD
  • 类似 import * as a from 'b';export c; 这样的声明
  • exportsmodule.exports 赋值

很少具备:

  • 对于 windowglobal 的属性的赋值
(2)模块库样例

很多流行的 Node.js 库是模块库,如:expressgulprequest

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 definetypeof windowtypeof module 的测试,尤其是在文件顶部,基本上就是 UMD 库了。
  UMD 库的文档也经常会演示 “Using in Node.js” 样例,展示如何使用 require,以及 “Using in the browser” 样例展示如何使用 <script> 标签加载脚本。

(2)UMD 库样例

现在大部分流行库都支持 UMD 包。例如:jQueryMoment.jslodash,等等。

(3)UMD 库模板

有三种模板:

var x = require("foo");
// Note: calling 'x' as a function
var y = x(42);

确保阅读 footnote “The Impact of ES6 on Module Call Signatures”

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 PluginUMD Plugin

Module Plugin 可以修改另一个模块(包括 UMD 和其他类型模块)的原型。比如:Moment.js 中,moment-rangemoment 对象添加了新的 range 方法。
  对于编写声明文件而言,普通模块UMD 的编写方式是相同的

Module PluginUMD Plugin 使用的模板

module-plugin.d.ts

5、Global Plugin

Global Plugin 是全局代码,用于修改全局内容的原型。与 global-modifying modules(后面会详细介绍)一样,会增加运行时冲突的可能性。
  例如:一些库为 Array.prototypeString.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)

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

/// <reference types="someLib" />
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 模块,使用 /// <reference types="..." /> 指示符:

/// <reference types="moment" />
function getThing(): moment;
(2)模块或 UMD 库依赖 UMD 库(From a Module or UMD Library)

如果你的模块或者 UMD 库依赖于一个 UMD 库,使用 import 声明:

import * as someLib from 'someLib';

不要使用 /// <reference 指示符来声明一个对 UMD 库的依赖!!!

四、补充说明(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

(完)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,491评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,856评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,745评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,196评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,073评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,112评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,531评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,215评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,485评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,578评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,356评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,215评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,583评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,898评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,497评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,697评论 2 335

推荐阅读更多精彩内容