识别库的类型
目前大致分为三种类型的类库:全局类库、模块类库、UMD
类库。
全局类库
全局库是指能在全局命名空间下访问的(例如:不需要使用任何形式的 import
)。 许多库都是简单的暴露出一个或多个全局变量。
在全局库的指南文档上经常会看到如何在 HTML 里用脚本标签引用库:
<script src="http://a.great.cdn.for/someLib.js"></script>
目前,大多数流行的全局访问型库实际上都以 UMD 库的形式进行书写。在书写全局声明文件前,一定要确认一下库是否真的不是 UMD
。
查看全局库的源代码时,通常会看到:
- 顶级的
var
语句或function
声明 - 一个或多个赋值语句到
window.someName
- 假设
DOM
原始值像document
或window
是存在的
function createGreeting(s) {
return "Hello, " + s;
}
window.someName = function () {};
模块库
一些库只能在模块加载器的环境下工作。比如,Express
只能在 Node.js
里工作,所以就需要使用 CommonJS
的 require
函数加载。
通常会在模块化库的文档里看到如下说明:
var someLib = require("someLib");
或
define(..., ['someLib'], function(someLib) {});
从源码上看:
-
模块库至少会包含下列具有代表性的条目之一:
- 无条件的调用
require
或define
- 像
import as a from 'b';
orexport c;
这样的声明 - 赋值给
exports
或module.exports
- 无条件的调用
-
它们极少包含:
- 对
window
或global
的赋值
- 对
UMD 类库
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
库的文档里经常会包含通过 require
“在 Node.js
里使用”例子, 和“在浏览器里使用”的例子,展示如何使用 <script>
标签去加载脚本。
如何引入外部类库?
在开发过程中,不可避免要引用第三方 JavaScript
类库。通过直接引用可以调用库的类和方法,但是却无法通过 TypeScript
的严格类型检查机制。以jquery
为例:
npm i jquery
import $ from "jquery";
//Error! Could not find a declaration file for module 'jquery'.
无法找到 jquery
模版的声明文件,我们在使用非 ts
的类库时,必须为它编写一个声明文件,对外暴露它的 API,有些源码包含了声明文件,有些则需要单独安装。
在社区的努力下,大部分类库的声明文件,都已经编写好了。
// `jquery` 的声明文件
npm i @types/jquery --save-dev
如何编写声明文件?
声明文件放在哪?
- 目录
src/@types/
,在src
目录新建@types
目录,在其中编写.d.ts
声明文件,声明文件会自动被识别,可以在此为一些没有声明文件的模块编写自己的声明文件,实际上在tsconfig.json
中typeRoots
字段包含的范围内编写.d.ts
,都将被自动识别; - 与被声明的
js
文件同级目录内,创建相同名称的.d.ts
文件,这样也会被自动识别; - 设置
package.json
中的types
属性值,如./index.d.ts
. 这样系统会识别该地址的声明文件。同样当我们把自己的js
库发布到npm
上时,按照该方法绑定声明文件。 - 如果
npm
模块安装,如@type/react
,它存放在node_modules/@types/
路径下。
声明语法
-
declare var
声明全局变量声明:
declare const foo: number;
一般来说,全局变量都是禁止修改的常量,所以大部分情况都应该使用
const
而不是var
或let
。 -
declare function
声明全局函数代码:
greet("hello");
声明:
declare function greet(greeting: string): void;
支持函数重载
declare function greet(greeting: number): void; declare function greet(greeting: string): void;
-
declare namespace
声明带属性的对象描述用点表示法访问的类型或值。
代码:
let result = myLib.makeGreeting("hello, world"); let count = myLib.numberOfGreetings;
声明:
declare namespace myLib { function makeGreeting(greeting: string): void; let numberOfGreetings: number; }
-
interface type
可重用类型接口或别名 除了全局变量之外,可能有一些类型我们也希望能暴露出来。在类型声明文件中,我们可以直接使用interface
或type
来声明一个全局的接口或类型。代码:
greet({ greeting: "hello world", duration: 4000, });
声明:
interface GreetingSettings { greeting: string; duration?: number; color?: string; } declare function greet(greeting: GreetingSettings): void;
-
组织类型
代码:
const g = new Greeter("Hello"); g.log({ verbose: true }); g.alert({ modal: false, title: "Current Greeting" });
使用命名空间组织类型:
declare namespace Greeter { interface LogOptions { verbose?: boolean; } interface AlertOptions { modal: boolean; title?: string; color?: string; } }
也可以在一个声明中创建嵌套的命名空间:
declare namespace Greeter.Options { // Refer to via Greeter.Options.Log interface Log { verbose?: boolean; } interface Alert { modal: boolean; title?: string; color?: string; } }
-
declare class
声明全局类代码:
const myGreeter = new Greeter("hello, world"); myGreeter.greeting = "howdy"; myGreeter.showGreeting(); class SpecialGreeter extends Greeter { constructor() { super("Very special greetings"); } }
声明:
declare class Greeter { constructor(greeting: string); greeting: string; showGreeting(): void; }
声明文件
全局类库
- js 文件
```js
function globalLib(options) {
console.log(options);
}
globalLib.version = "1.0.0";
globalLib.doSomething = function () {
console.log("global lib do something");
};
```
在 `ts` 中调用该函数
```ts
globalLib({ a: 1 }); // Error: Cannot find name 'globalLib'.
```
报错:未找到该函数。解决办法为它添加一个声明文件:
```ts
// .d.ts
declare function globalLib(options: globalLib.Options): void;
declare namespace globalLib {
const version: string;
function doSomething(): void;
interface Options {
[key: string]: any;
}
}
```
`declare` 关键字可以为外部变量提供声明。命名空间和函数的声明合并,为函数添加一些默认属性。接口参数指定为可索引类型,接受任意属性。
模块类库
以下为 `CommonJS` 模块编写的文件:
```js
const version = "1.0.0";
function doSomething() {
console.log("moduleLib do something");
}
function moduleLib(options) {
console.log(options);
}
moduleLib.version = version;
moduleLib.doSomething = doSomething;
module.exports = moduleLib;
```
同样我们将它引入 `ts` 文件中使用。
```ts
import module from "./module-lib/index.js";
// Error: Could not find a declaration file for module './module-lib/index.js'.
```
提示未找到该模块,同样我们需要为它编写文件声明。
```ts
declare function moduleLib(options: Options): void;
interface Options {
[key: string]: any;
}
declare namespace moduleLib {
const version: string;
function doSomething(): void;
}
// export = 更通用
export = moduleLib;
```
与全局类库声明文件唯一的区别这里需要 `export` 输出。
UMD 类库
(function (root, factory) {
if (typeof define === "function" && define.amd) {
define(factory);
} else if (typeof module === "object" && module.exports) {
module.exports = factory();
} else {
root.umdLib = factory();
}
})(this, function () {
return {
version: "1.0.0",
doSomething() {
console.log("umd lib do something");
},
};
});
声明文件:
declare namespace umdLib {
const version: string;
function doSomething(): void;
}
export as namespace umdLib;
export = umdLib;
这里与其他类库不同的是,多添加了一条语句 export as namespace umdLib
,如果为 umd 库声明,这条语句必不可少。
umd
同样可以使用全局方式引用。
<script src="src/libs/umd-lib.js"></script>
umdLib.doSomething();
// Error! 'umdLib' refers to a UMD global, but the current file is a module. Consider adding an import instead.
错误提示:umdLib
是一个 UMD
库,建议 import
导入。
我们可以通过配置项 "allowUmdGlobalAccess": true
,来关闭这个提示。
模块
有时候,我们想给一个第三方类库添加一些自定义的方法。
- 模块插件:
declare module
import m from "moment"; declare module "moment" { export function myFunc(): void; } m.myFunc = () => {};
- 全局插件:
declare global
这会造成全局污染,所以不建议。declare global { namespace globalLib { export function doAnything(): void; } } globalLib.doAnything = () => { console.log("globalLib do anything"); };
声明依赖
当一个类库很大时,声明文件也会根据模块进行划分,这时声明文件之间就存在了依赖关系。以jquery
为例:
/**** 模块依赖 ****/
/// <reference types="sizzle" />
/**** 路径依赖 ****/
/// <reference path="JQueryStatic.d.ts" />
/// <reference path="JQuery.d.ts" />
/// <reference path="misc.d.ts" />
/// <reference path="legacy.d.ts" />
export = jQuery;