UI组件库从1到N开发心得-声明文件篇

5,053 阅读3分钟

本篇主要介绍如何让你的组件库在Typescript项目中支持使用,并且还会详细介绍声明文件的作用、使用方法以及如何编写声明文件. 希望阅读完本篇的内容可以帮助你掌握编写声明文件的技能.

如果你所编写的组件库或者插件,没有添加声明文件的话,那么当它被Typescript项目中引入时,如下图所示:

找不到 owl-ui 模块的声明文件,请尝试执行 npm install @types/owl-ui 添加声明文件,或者创建一个包含 declare module 'owl-ui'.d.ts 文件. 编译器为开发者提供了两种解决办法,这两种办法在后面会介绍给大家,在这之前我简单介绍一下声明文件.

什么是声明文件

根据上面错误提示分析,声明文件就是为Javascript库提供类型声明,声明文件必须以 .d.ts 为后缀. 如果在Typescript项目中引入第三方库时,TS编译器会检查其声明文件内容,若没有找到则提示错误.

声明文件放在哪

在介绍声明文件如何编写之前,列举一般声明文件存放的方式.

  1. 目录 src/@types/,在 src 目录新建 @types 目录,在其中编写 .d.ts 声明文件,声明文件会自动被识别,可以在此为一些没有声明文件的模块编写自己的声明文件,实际上在 tsconfig.jsoninclude 字段包含的范围内编写 .d.ts,都将被自动识别;
  2. 与被声明的 js 文件同级目录内,创建相同名称的 .d.ts 文件,这样也会被自动识别;
  3. 设置 package.json 中的 typings 属性值,如 ./index.d.ts. 这样系统会识别该地址的声明文件. 同样当我们把自己的js库发布到 npm 上时,按照该方法绑定声明文件;
  4. 同过 npm 模块安装,如 @type/react ,它存放在 node_modules/@types/ 路径下.

一般来讲,TS编译器会解析项目中所有的 .ts 文件,其中也包括 .d.ts 文件,所以如果你在自己的项目中为第三方插件编写声明文件的话,文件放在哪里都会被解析到.

如何发布声明文件

如果我们自己实现了一个js库,如何来写声明文件呢?目前有两种方式用来发布声明文件到 npm 上:

  1. 与你的 npm 包同时捆绑在一起;
  2. 发布到 npm 上的 @types organization

npm包含声明文件

package.json 中,你的需要指定 npm 包的主 js 文件,那么你还需要指定主声明文件. 如下:

{
  "name": "owl-ui",
  "version": "0.2.6",
  "description": "Owl UI",
  "main": "./lib/owl-ui.common.js",
  "typings": "types/index.d.ts",
}

有的 npm 包设置的 types 属性,它和 typings 具有相同意义. 如果你的 npm 包需要依赖于其他包,需要将依赖放在 dependencies 中.

发布到@types

@types 下面的包是从 DefinitelyTyped 里自动发布的,通过 types-publisher 工具. 如果想让你的包发布为@types包,提交一个pull request到 github.com/DefinitelyT…. 在这里查看详细信息 contribution guidelines page.

如何编写声明文件

目前大致分为三种类型的类库,分别为全局类库模块类库UMD类库. 接下来,我会带大家分析 ts 引入各自类库的用法和区别.

接下来介绍的大部分内容为代码段,可以点击查看源码同时阅读.

引入jquery

jquery 是 UMD 类库,它可以全局引用,同时也可以使用模块化方式引用.

import $ from 'jquery'

// Error: Try `npm install @types/jquery` if it exists or add a new declaration (.d.ts) file containing `declare module 'jquery';`

ts 文件中引入 js 文件时,会提示上述错误,原因在于缺少声明文件.

我们在使用非 ts 编写的类库时,必须为这个类库编写一个声明文件,对外暴露它的 API,有时候这些类库的声明文件是单独提供的,需要额外安装. 上述例子中就提示需要安装 @types/jquery 插件.

npm install @types/jquery -D

安装完之后,就可以正常在 ts 文件中使用 jquery 了.

  1. 可在 http://microsoft.github.io/TypeSearch/ 中查询类库是否有声明文件
  2. 可在 http://definitelytyped.org/ 中了解如何发布声明文件

全局类库

编写 js 文件,如下所示:

function globalLib (options) {
  console.log(options)
}

globalLib.version = '1.0.0'

globalLib.doSomething = function () {
  console.log('global lib do something')
}

上述代码中,定义了一个函数,为函数添加了两个元素. 接下来我们用 script 标签引入该文件,让该函数作用在全局.

我们在 ts 中调用该函数,如下所示:

globalLib({ a: 1 }) // Error: Cannot find name 'globalLib'.

提示未找到该函数.

解决办法为它添加一个声明文件,在同级目录下创建一个同名 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 模块编写的文件:

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 文件中使用.

import module from './module-lib/index.js'
// Error: Could not find a declaration file for module './module-lib/index.js'. 

提示未找到该模块,同样我们需要为它编写文件声明.

declare function moduleLib (options: Options): void

interface Options {
  [key: string]: any
}

declare namespace moduleLib {
  const version: string
  function doSomething(): void
}

export = moduleLib

上述 ts 与刚刚编写的全局类库声明文件大致相同,唯一的区别这里需要 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')
    }
  }
}))

同样我们将它引入 ts 文件中使用,如果没有声明文件也会提示错误,我们直接看 ts 声明文件

declare namespace umdLib {
  const version: string
  function doSomething (): void
}

export as namespace umdLib

export = umdLib

我们声明了一个命名空间,命名空间内有两个成员 versiondoSomething,分别对应 umd 中的两个成员.

这里与其他类库不同的是,多添加了一条语句 export as namespace umdLib,如果为 umd 库声明,这条语句必不可少.

umd 同样可以使用全局方式引用.

插件

有时候,我们想给一个第三方类库添加一些自定义的方法. 以下介绍如何在模块插件全局插件中添加自定义方法.

模块插件

我们使用 moment 插件,为它添加一个自定义方法. 关键字 declare module.

import m from 'moment'
declare module 'moment' {
  export function myFunction (): void
}
m.myFunction = () => { console.log('I am a Fn') }

全局插件

在上面我们有介绍全局类库,我们为它添加一个自定义方法. 关键字 declare global.

declare global {
  namespace globalLib {
    function doAnyting (): void
  }
}
globalLib.doAnyting = () => {}

UI组件库添加声明文件

在项目的根目录创建 types 文件. 目录结构如下:

.
├── component.d.ts
├── index.d.ts
├── owl-ui.d.ts
└── packages
    ├── button-group.d.ts
    ├── button.d.ts
    ├── date-picker.d.ts
    ├── dialog.d.ts
    ├── drawer.d.ts
    ├── input-group.d.ts
    ├── input.d.ts
    ├── picker.d.ts
    ├── range.d.ts
    ├── select-group.d.ts
    ├── select.d.ts
    ├── switch.d.ts
    ├── tabs.d.ts
    └── toast.d.ts

component.d.ts 存放声明组件需要的继承类:

import Vue from 'vue'

export declare class OwlUIComponent extends Vue {
  static install (vue: typeof Vue): void
}

export declare class OwlUIPopupComponent extends OwlUIComponent {
  visible: boolean
  zIndex: number
  maskStyle: object
  containerStyle: object
  show (): any
  hide (): any
}

packages 文件夹保存着每个组件的声明文件,如 button.d.ts

import { OwlUIComponent } from '../component'

export type ButtonType = 'default' | 'disabled' | 'light'

export declare class OwlButton extends OwlUIComponent {
  type: ButtonType
  inline: boolean
  outline: boolean
  btnStyle: object
}

owl-ui.d.ts 整合所有的声明文件,逐一输出:

import Vue from 'vue'

import { OwlUIComponent } from './component'

/**
 * component common definition
 */
export type Component = OwlUIComponent

export function install (vue: typeof Vue): void

import { OwlButton } from './packages/button'
import { OwlButtonGroup } from './packages/button-group'
import { OwlDatePicker } from './packages/date-picker'
import { OwlDialog } from './packages/dialog'
import { OwlDrawer } from './packages/drawer'
import { OwlInputGroup } from './packages/input-group'
import { OwlInput } from './packages/input'
import { OwlPicker } from './packages/picker'
import { OwlRange } from './packages/range'
import { OwlSelectGroup } from './packages/select-group'
import { OwlSelect } from './packages/select'
import { OwlSwitch } from './packages/switch'
import { OwlTabs } from './packages/tabs'
import { OwlToast } from './packages/toast'

export class Button extends OwlButton {}
export class ButtonGroup extends OwlButtonGroup {}
export class DatePicker extends OwlDatePicker {}
export class Dialog extends OwlDialog {}
export class Drawer extends OwlDrawer {}
export class InputGroup extends OwlInputGroup {}
export class Input extends OwlInput {}
export class Picker extends OwlPicker {}
export class Range extends OwlRange {}
export class SelectGroup extends OwlSelectGroup {}
export class Select extends OwlSelect {}
export class Switch extends OwlSwitch {}
export class Tabs extends OwlTabs {}
export class Toast extends OwlToast {}

declare module 'vue/types/vue' {
  interface Vue {
    /** create Drawer instance */
    $drawer(options: object): Drawer
    /** create Dialog instance */
    $dialog(options: object): Dialog
    /** create DatePicker instance */
    $datePicker(options: object): DatePicker
    /** create Picker instance */
    $picker(options: object): Picker
    /** create Toast instance */
    $toast(options: object | string): Toast
  }
}

index.d.ts 为输入输出文件:

export * from './owl-ui'

import * as OwlUI from './owl-ui'

export default OwlUI

最后在 package.json 中添加 typings: "types/index.d.ts".

这样组件库就支持在Typescript项目中使用了.

结语

最近 Vue3.0 已经进入 Beta 阶段了,待正式版发布之后,我会继续更新迭代组件库并把开发心得分享给大家.

相关链接

OwlUI GitHub

Typescript 开发教程