如何在 TypeScript 中引入外部类库?

7,148 阅读3分钟

识别库的类型

目前大致分为三种类型的类库:全局类库、模块类库、UMD 类库。

全局类库

全局库是指能在全局命名空间下访问的(例如:不需要使用任何形式的 import)。 许多库都是简单的暴露出一个或多个全局变量。

在全局库的指南文档上经常会看到如何在 HTML 里用脚本标签引用库:

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

目前,大多数流行的全局访问型库实际上都以 UMD 库的形式进行书写。在书写全局声明文件前,一定要确认一下库是否真的不是 UMD

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

  • 顶级的 var 语句或 function 声明
  • 一个或多个赋值语句到 window.someName
  • 假设 DOM 原始值像 documentwindow 是存在的
function createGreeting(s) {
  return "Hello, " + s;
}
window.someName = function () {};

模块库

一些库只能在模块加载器的环境下工作。比如,Express 只能在 Node.js 里工作,所以就需要使用 CommonJSrequire 函数加载。

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

var someLib = require("someLib");

define(..., ['someLib'], function(someLib) {});

从源码上看:

  • 模块库至少会包含下列具有代表性的条目之一:

    • 无条件的调用 requiredefine
    • import as a from 'b'; or export c;这样的声明
    • 赋值给 exportsmodule.exports
  • 它们极少包含:

    • windowglobal 的赋值

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 definetypeof 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,有些源码包含了声明文件,有些则需要单独安装。

在社区的努力下,大部分类库的声明文件,都已经编写好了。

  1. 可以在 这个网站 自行搜索。
  2. 如果遇到还没有编写声明文件的,就是你贡献社区的 大好机会
// `jquery` 的声明文件
npm i @types/jquery --save-dev

如何编写声明文件?

声明文件放在哪?

  • 目录 src/@types/,在 src 目录新建 @types 目录,在其中编写 .d.ts 声明文件,声明文件会自动被识别,可以在此为一些没有声明文件的模块编写自己的声明文件,实际上在 tsconfig.jsontypeRoots 字段包含的范围内编写 .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 而不是 varlet

  • 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 可重用类型接口或别名 除了全局变量之外,可能有一些类型我们也希望能暴露出来。在类型声明文件中,我们可以直接使用 interfacetype 来声明一个全局的接口或类型。

    代码:

    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;

TypeScript 工程系列