Skip to content

随着前端技术日趋复杂化,越来越多的项目都会选择使用TypeScript语言。TypeScript语言最显著的特点是提供了类型系统的支持。 类型系统有助于增加大型项目的可维护性。利用类型检查机制在编译期阶段可以发现更多的错误。从开发理论上讲,越早发现错误,解决错误的成本就会越低。这样的话就会使编写效率明显提高。这样的话也就可以更早下班,陪家人。

当然使用Typescript也不是只有好处。最大的问题是需要付出相应的学习成本,和多余的工作量用于定义类型系统。,因为对于使用Typescript 多了一套类型定义的心智负担。简单的类型还没什么也许只是给变量设置 string 或 number。其实也不跟你没什么卵用。

复杂的类型系统包含复杂的类型推导,泛型等内容,以下是 Vue 源码中 ractive 的返回对象。给大家一个截图大家可以感受一下。

img

很多小伙伴都在过度迷恋强类型语言,其实大可不必。弱类型语言晚于强类型语言出现的。也就是说弱类型语言是强类型语言的进化版。就是为了增加开发效率才会引入弱类型。我的观点是,对于业务逻辑型开发并不太适合使用Typescript。

实际上,未必使用Typescript编写逻辑才能体现类型检查机制的好处。即使你使用 Javascript 语言开发,使用可 Typescript 编写的库也可以同样享受类型的收益。连接两者的的桥梁就是类型定义文件 d.ts。好了下面就到我们这节课的主题了为组件库添加类型定义。

前置知识

类型定义文件的作用

下面我们用一个简单的例子来演示类型定义的作用。

创建一个文件 utils.ts

首先使用 Typescript 创建一个函数。

function add(a: number, b: number): number {
    return a + b
}

interface Person {
    name: string,
    age?: number
}

export { add, Person }

如果在 index.ts 引用这个库,可以得到明确的类型提示。

import { Person, add } from './utils'

const a: Person = {
    name: 'abc'
}

add(1, 2)

并且可以正常的进行类型检查的。

img

但是如果将 utils.ts 编译为 utils.js 文件。这个时候类型定义就会丢失。

img

那调用的时候自然也不会有类型检查。

img

可以看到调用 add 方法的时候,类型是 any。

如果想要保留类型定义,就需要生成类型定义文件,将类型保留到类型定义文件中 d.ts。这个时候编辑器就可以依据类型定义文件进行类型检查了。

img

utils.ts

declare function add(a: number, b: number): number;
interface Person {
    name: string;
    age?: number;
}
export { add, Person };

这个时候,在使用index.js的时候就可以看到类型定义了。

img

最后的结论,就是想得到类型定义的恩惠,未必一定要使用Typescript作为开发语言。比如: 使用 Javascript 语言开发,只要使用了Typescript 开发的库,一样可以享受到类型系统提示和检查。

Typescript导出类型定义

一个标准的 Typescript 项目导出类型定义,只需要在 tsconfig.json 中添加declaration 选项就可以实现。

img

这个时候使用 tsc 编译的时候就可以导出类型定义。

用户故事(UserStory)

为组件库添加类型定义,使组件具备类型提示功能

任务分解(Task)

  • 配置 vite-plugin-dts 插件
  • 生成软件包的类型定义入口
  • 注册全局组件
  • 编写模版脚本
  • 测试类型系统

配置vite-plugin-dts插件

想让组件库具有类型定义,第一步必须要将组件库源码中的类型定义导出。虽然可以使用 tsc 导出类型定义。但是组件涉及 .vue 文件。所以需要 vite-plugin-dts 插件来完成。

在 vite.config.ts 中增加插件

pnpm i vite-plugin-dts
import dts from "vite-plugin-dts";
export const config = {
  plugins: [
    // ...
    dts({
      outputDir: "./dist/types",
      insertTypesEntry: false, // 插入TS 入口
      copyDtsFiles: true, // 是否将源码里的 .d.ts 文件复制到 outputDir
    }),
  ],
  }

这里面有三个配置

  • outputDir: 是为了设置类型定义的位置
  • insertTypesEntry: 这个选项是为了生成入口,由于默认入口还不能完全满足要求所以选择 false。不接受导出。 后续会通过自定义脚本生成。
  • copyDtsFiles: 目的是可以自动复制源码中的类型定义,这个需要有。

    • 增加插件定义后,重新执行build后的效果
    • pnpm build
    • img

定义类型定义入口

类型定义入口文件是靠编写编译脚本实现的。如何编写这个脚本在后面介绍。现在大家先弄清生成后的样子。对于一个软件包来讲,类型定义文件的位置通过 package.json 的 types 属性确定。

img

入口文件 smarty-ui.d.ts 其实就是引用了 entry.d.ts 。

export * from './types/entry'
import SmartyUI from './types/entry'
export default SmartyUI

注册全局组件

什么是注册全局组件呢。举一个例子,在 Typescript 中一切变量都不能凭空捏造。比如在 node 全局作用域中需要某个全局变量的存在,也需要在全局作用域中注册。这样 Typescript 才认为他是合法的。对于组件库的组件,本来在全局进行注册,依然需要一个类型定义的声明。这样才可以,在 vue 文件中使用的时候可以找到对应的类型定义。这个声明的意思大概就是在所有的 vue 文件中存在某些全局组件。

以组件库为例,其中的 SButton 需要在 vue 文件中使用, 就需要注册为全局组件。实际上组件库中的所有组件都需要注册为全局组件。

img

具体写法为

img

这段描述,需要增加到类型定义中。

综合考虑前面需要生成类型入口,从实现上考虑这段代码比较适合写在入口文件中。这样便于脚本的编写。

编写模版脚本

上面搞清了类型定义的格式,就需要编写一个脚本自动生成这部分代码。这部分代码主要围绕着入口文件的生成展开,其实就是利用模版来生成代码,方法和 CLI 工具章节中的模版生成代码方法一致这里就不赘述。

第一步,要实现声明全局组件。

首先需要获取全局组件的列表。这个功能可以通过对 entry.ts 的反射遍历完成。

type.ts

/**
 * 获取组件列表
 * 通过解析entry.ts模块获取组件数据
 */
async function getComponents(input) {
    const entry = await import(input)
    return Object.keys(entry)
        .filter(k => k !== 'default')
        .map(k => ({
            name: entry[k].name,
            component: k
        }))
}

然后是编写一个入口代码模板,这个模版主要是需要遍历组件列表生成全局组件接口。

entry.d.ts.hbs

export * from './types/entry'
import SmartyUI from './types/entry'
export default SmartyUI

declare module 'vue' {
    export interface GlobalComponents {
        {{#each components}}
            {{name}}: typeof import("./types/entry").{{component}},
        {{/each}}
    }
}

下一步是编写脚本生成类型定义文件。

type.ts

/**
 * 生成类型定义文件 d.ts
 * @param components 
 */
export async function generateDTS(entryPath) {

    const template = resolve(__dirname, './entry.d.ts.hbs')
    const dts = resolve(__dirname, entryPath.replace('.esm.js', '.d.ts'))

    // 组件库数据
    const components = await getComponents(entryPath)
    // console.log('list', list)

    // 生成模版
    generateCode({
        components
    }, dts, template)
}

然后把完成的入口生成函数加入到 build.ts 文件中。顺便增加入口的位置定义。

build.ts

// ...
 packageJson.types = "smarty-ui.d.ts";
// ...
  // 生成配置DTS配置文件入口
  generateDTS(path.resolve(config.build.outDir, `smarty-ui.esm.js`),)

最后的效果

img

测试类型提示

Typescript的类型提示支持VsCode原生。但是如果让 Vue 单文件也得到类型提示,就需要安装相应的插件。比如使用的 Volar 插件。

这个插件目前我测试的结果仅支持 TS版本的Vue项目。这个大家一定要注意。 这是插件的限制并不是 Typescript 类型系统的限制。实际上是完全有可能实现 JS 项目中也具备 Vue 文件的类型提示。

img

首先需要把新版本发布上线。

然后可以使用前面章节创建的 create-smarty 创建一个模版项目。当然这里面还需要升级模版项目引用最新版的 smary-ui-vite。这些过程就不再赘述。

这时候在新创建的项目中就可以看到类型提示了。

img

复盘

这节课我们主要讲了如何给组件库添加类型定义。类型定义可以在使用组件库的时候获得类型提示。提高使用者的开发体验。从而充分的享受 Typescript 类型系统带来的恩惠。

当然这个章节还有遗憾,就是现有版本只能支持对全量组件引入的类型提示。并不支持在分包下的类型提示。这个地方然叔就留给读者去考虑一下如何去实现。实际上原理是一致的,难点在于如何合理用脚本实现。也欢迎同学们将这个答案 PR 到咱们的项目中。

https://github.com/smarty-team/smarty-admin

最后留一些思考题帮助大家复习,也欢迎在留言区讨论。

  • 类型定义文件的作用 ?
  • 如何确定软件包 package 中的类型定义入口?