使用jsdoc提升前端开发效率:像编写typescript一样轻松驾驭JavaScript

发表时间: 2023-02-03 12:13

众所周知的原因,由于JS的语言特性,任何开发工具都不能为JS提供足够好的智能提示,正因为此,微软创造的轮子:typescript,横空出世

那么,有没有一种不用typescript的解决方案呢?有,那就是今天的主角:jsdoc;这可能是一个大家很少使用的开发利器;它是一个可以使你像写typescript一样写JavaScript;没错,jsdoc主要就是用来给js添加类型信息的;

下面我们看一个简单的函数,这个函数接收一个参数modalElement,由于编辑器不知道它是什么类型,所以在调用它的querySelector方法的时候,无法获得编辑器的智能提示;同样地,编辑器也无法给出btnCloseclick方法的智能提示;

const closeModal = modalElement => {  if (modalElement) {    const btnClose = modalElement.querySelector('.el-dialog__close')    if (btnClose) {      btnClose.click()      return true    }  }}

这时候,就该jsdoc出场了;它的语法需要写在多行注释中,因为它不属于js语法的一部分;我们只需给modalElementbtnClose增加一个类型标注,编辑器就知道它们是什么类型,拥有什么能力了;

为函数参数指定类型,使用@param标记,语法:@param {类型} 参数名;为变量指定类型,使用@type标记,语法:@type {类型};代码如下:

/** @param {HTMLDivElement} modalElement */const closeModal = modalElement => {  if (modalElement) {    /** @type {HTMLLinkElement} */    const btnClose = modalElement.querySelector('.el-dialog__close')    if (btnClose) {      btnClose.click()      return true    }  }}

现在,当我们把指针移入modalElement的时候,就不是一个简单的any类型了,编辑器可以根据文档注释确定它是HTMLDivElement类型,我们在调用它的querySelector方法的时候就能得到编辑器的智能提示;当我们把指针移入click的时候,编辑器告诉我们,这个方法是从HTMLElement继承来的;

@returns标注用于指定函数返回值数据类型;语法:@returns {类型};如下函数返回由HTMLDivElement构成的数组;

/** @returns {HTMLDivElement[]} */const getAllModals = () => {  return Array.from(    document.body.querySelectorAll('.el-dialog__wrapper, .el-drawer__wrapper')  ).filter(_ => {    return window.getComputedStyle(_, null).display !== 'none'  })}

如下图,当我们调用getAllModals函数返回值的reduce方法的时候,编辑器可以给出智能提示;

我们还可以使用@typedef标记自定义类型;语法:import('模块路径'),用于从模块中导入TS类型定义;&符号用于合并2个类型;如下例子定义了一个名叫RouteConfig的类型,该类型在import('vue-router').RouteConfig的基础上为meta字段增加了number类型的index字段;

/** * @typedef {import('vue-router').RouteConfig & {meta: { index: number }}} RouteConfig */

如下例子定义了一个EntAccountInfo类型,包含2个字段:数值类型的id和字符串类型的password

/** * @typedef {{id: number, password: string}} EntAccountInfo */

如果我们的字段比较多,可以使用@property标记定义每个字段;

/** * @typedef UserData 用户数据 * @property {any} entid 租户id * @property {string} name 姓名 * @property {string} workNo 工号 * @property {string} userId 用户id * @property {string} username 登录用户名 */

我们自定义的类型和内置类型用法完全一样;请看下面例子,包含内置类型和我们上面创建的自定义类型;我们在给对象字段指定类型的时候,可以有2种写法:写在字段名前面或上面;大家觉得哪种风格优雅?

	export const state = Vue.observable({  /** @type {string[]} */ keeps: [],  /** @type {RouteConfig[]} */ menus: [],  /** @type {EntAccountInfo} */ entInfo: {},  /** @type {UserData} */ userData: {},})
export const state = Vue.observable({  /** @type {string[]} */  keeps: [],  /** @type {RouteConfig[]} */  menus: [],  /** @type {EntAccountInfo} */  entInfo: {},  /** @type {UserData} */  userData: {}})

我们可以使用管道符为一个变量指定多个可能的类型,请看如下例子,当用户调用该函数的时候,编辑器会提示该函数期望接收一个类型为日期或字符串或数值的参数time

/** @param {Date | string | number} time */export const getHalfYearAfterTime = time => {  const date = new Date(time)  date.setMonth(date.getMonth() + 6)  date.setDate(date.getDate() - 1)  return date}

如果我们的函数有不限个数的参数,可以使用语法:@param {...类型} 参数名,指定参数类型;请看如下例子:

/** @param  {...string} paths */export const getApiUrl = (...paths) => joinPath(API_BASE, ...paths)

不限参数个数的函数,还有更高级的类型标注写法;请看如下代码,formRequest是一个axios的实例,我们想每次发起post请求的时候少写10来个字符,定义了一个post函数,直接返回了对formRequestpost方法调用;

export const post = (...args) => formRequest.post(...args)

通过编辑器的提示,我们得知axiospost方法有3个不同类型的参数;而我们为了省事,使用了展开运算符,不管传入多少个参数,全部仍给axios实例的post方法;那么,参数类型该如何标注呢?

我们可以使用中括号语法为每个参数指定字段名和类型,例子如下:

/** @param  {[url: string, data?: any, config?: RequestConfig]} args */export const post = (...args) => formRequest.post(...args)

下图是当指针移入post函数上时,编辑器给出的提示;是不是很酷?

如下是RequestConfig的类型定义,我们扩展了AxiosRequestConfig,为其增加了2个布尔类型字段;现在当我们调用post函数的时候,编辑器就会知道我们的第3个参数config包含这2个布尔类型字段;

/** * @typedef {import('axios').AxiosRequestConfig & { needAuth: boolean, saveToken: boolean }} RequestConfig */

jsdoc还可以定义泛型类型,语法:@template 泛型名;请看如下例子,TreeNode是一个泛型类型,我们唯一能确定的是它有一个children字段;它具体还包含哪些字段,由泛型T决定;

/** * @template T * @typedef {T & {children: TreeNode[]}} TreeNode*/

我们还可以在函数类型标注中使用泛型,请看如下例子,我们定义了一个泛型T,参数data为泛型T数组,返回值为泛型类型TreeNode<T>构成的数组;

/** * @template T * @param {T[]} data * @param {{key: string, parentId: string}} config * @returns {TreeNode<T>[]} */export const toTree = (data, config) => {  const { key = 'id', parentId: pId = 'parentId' } = config || {}  const ids = data.map(_ => _[key])  /** @type {TreeNode<T>[]} */  const result = []  // ... ...  return result}

以上就是我工作中最常用的jsdoc用法,还有很多用法没有涉及到;篇幅有限,大家可以去官网查看文档;希望该文章能助大家的JS技术更上一层楼,感谢阅读!