JSDoc 快乐入门

程序员最讨厌的四件事,写注释、写文档、别人不写注释、别人不写文档。

JSDoc 是什么,为什么我们需要它

为什么我们要写注释

JSDoc 首先是文档注释,文档注释是注释。我们写注释的目的,基本上是以下两点

让别人理解你的代码,和让自己理解自己的代码。

第一点容易理解,每个人写代码风格千差万别,我们不能保证他人能接受我们的自己的编程风格(比如我就会用大量的map, reduce, filter等高阶函数辅以老牌函数式编程工具库lodash)。注释至少能让他人明白我们代码的意图,知晓意图后,理解代码具体逻辑就会容易一些。另外,各种业务代码中的 Hack,如果不写注释,常常会让后面的维护者疑惑。

第二点,让自己理解自己的代码。在书写新代码时,如果业务复杂,不可避免会导致代码长度增加。注释能让我们帮助我们在脑内对我们写的东西,有个大概的框架,减少写出缺乏大局观的代码的可能。另一种情况,我们自己要在过去自己所负责的那片代码上进行业务迭代。这些注释能帮助我们回忆起之前业务代码上的一些细节。

从上面两方面看来,写注释最大的好处是,减少他人与自己理解代码的成本,并且更容易进入工作状态,更好更快地完成项目迭代,更快地下班,也有更多的时间跑崽。

写注释是以极少量的成本,却表达了对同事和自己的爱意

为什么选择 JSDoc

JSDoc 是一种统一的注释规范,它以/**或者/***开头,以*/结尾。比起 js 中基本的两种注释///* */,它具有以下优点

  1. 统一且和谐的注释风格,降低团队内理解文档的成本。
  2. 具有一套比较完善的类型系统,提高代码可维护性。
  3. 主流的编辑器和 IDE 会对类型注释等提供较好的支持,比如开发工具会提供 JSDoc 的代码片段,通过类型注释提供相应的自动补全提示等等。
  4. 无需任何配置,只要学会就能立即在项目中使用并获得收益。

当然,它也有这样的缺点。

  1. 类型系统对泛型的支持较羸弱,某些场景下只能退而求其次。
  2. JSDoc 具有一定的学习成本,对于未接触过静态类型语言的初学者可能会不好理解。

不过,瑕不掩瑜。对于上面的四点收益,下面的两点缺点也是可以克服和接受的。

类型系统

类型,会紧随在 JSDoc 的标签后,,被含在{}中。下面是一些例子。

下面的类型系统部分与 TypeScript 中的类型系统有大量相通之处,并且在主流编辑器中,混用他们大部分时候是没有问题的。

/**
 * 标注类型
 * @type {number}
 */
/**
 * 自定义一个类型
 * @typedef SomeType
 * @property {string}
 */
/**
 * 标注返回值
 * @returns {Function}
 */
/**
 * 标注参数
 * @param {boolean} isVisible
 */

基本类型

此处全当复习 JS 的类型系统,大家都明白的

类型含义
number数字
boolean布尔值, true or false
string字符串
nullNull 类型
undefinedundefined 类型
object代表一切非基本类型
any(或者*)任意类型

类型组合

数组

数组的表示方法极其简单,既可以在类型后面加方括号,形如T[],也可以Array<T>这样写(这里的T泛指一切类型)

/**
 * @type {number}
 */
const scores = fetchScore();

字符、数字字面量类型

|将每个字面量隔开

联合类型

我们传入的参数可能是多种类型中的某个,便是联合类型的使用场合。它的语法与上面的字面量类型相同。也用|隔开。

/**
 * 一个例子
 * @param {number|string} value 具有多种类型的值
 * @param {number} padding 内边距
 */
function padLeft(value, padding) {
  if (typeof padding === 'number') {
    return Array(padding + 1).join(' ') + value;
  }
  if (typeof padding === 'string') {
    return padding + value;
  }
  throw new Error(`Expected string or number, got '${padding}'.`);
}

交错类型

交叉类型可以叠加多个类型形成新的类型,这个在 React 组件中给组件的 props 注解类型极为有用。 下面的例子中,Vscode 会自动补全 Ant design 的 Form 属性中的各种相关方法

import * as React from 'react';
import { Form, Input } from 'antd';
import { FormComponentProps } from 'antd/lib/form';

const { Item: FormItem } = Form;

/**
 * @type {React.FC<FormComponentProps & { otherProps: Function }>}
*/
const UserForm:  = ({ form }) => {

  const { getFieldDecorator } = form;
  return (
    <Form>
      <FormItem>{getFieldDecorator('name')(<Input />)}</FormItem>
    </Form>
  );
};

索引类型

很多时候我们使用 object 注解类型还不够详细,这个时候,索引类型就出马了。 它的类型是这样写的。TV为对象值的类型,可替换为任何类型,TK为键类型,必须为number或者string或者它们的子集(比如用上面提到的字面量类型或者keyof关键字)

/**
 * @type {{[key: TK]: TV}}
 */

自定义一个类型

自定义类型使用@typedef标签定义。 下面的这个例子包含了常用的情况

// 第一种情况,我真的想定义一个新的类型
/**
 * @typedef TableRow
 * @property {number} age 年龄
 * @property {string} name 姓名
 * @property {boolean} isChecked 是否选中
 * @property {string?} ID 身份ID,为空时未登记
 */

// 第二种情况,我想为某个类型定义一个别名
/**
 * @typedef {1|2|3|4|5|6|7|8|9|10|11|12|13|14|15|16|17|18|19|20|21|22|23|24} Hour
 */

如何在各个场景下使用 JSDoc

下面我会用各种例子来说明,如何在代码中实践 Jsdoc 并获得收益

注解方法(公共方法,类方法等)

/**
 * @author Iron<lujianwei@duiba.com.cn>
 */
import sortBy from 'lodash/sortBy';
import last from 'lodash/last';

/**
 * 对数组分组。数组中位置相邻且为连续的数字会被分到一个数组
 * @param {number[]} list 待分组数组
 * @returns {number[][]}
 * @example
 * ``` js
 * groupByConsequent([1, 2, 3, 4, 6, 8, 9, 10, 11, 12, 13, 15]);
 * // 返回值为[[1, 2, 3, 4], [6], [8, 9, 10, 11, 12, 13], [15]]
 * ```
 */
function groupByConsequent(list) {
  return sortBy(list).reduce((prev, current) => {
    const prevGroup = last(prev);
    const prevHour = last(prevGroup);
    if (!prevGroup || (prevHour !== current - 1 && prevHour !== current)) {
      return [...prev, [current]];
    } else {
      return [...prev.slice(0, -1), [...prevGroup, current]];
    }
  }, []);
}

export default groupByConsequent;

注解 React 组件

函数式组件

在 React 中,无状态的函数式组件的泛型类为React.FunctionComponent,或者为React.FC,或者为React.SFC,如果你的 React 版本 <= 16.8,建议使用SFC,否则可用FC。而FunctionComponentFC的全称,任何版本都可用。SFC为 Stateless Function Component。由于 React16.8 更新了 React Hooks,使得函数式组件可以带上状态,因此称为 SFC 是不合适的。

第一个例子,一个业务代码中的 Modal,接收visibleonCloseIndexDetailModal两个参数。他们的类型分别为 boolean 和 Function

/**
 * @typedef ComparableChartIndexModalProps
 * @property {boolean} visible 弹窗可见性
 * @property {Function} onCloseIndexDetailModal 关闭弹窗时的回调
 */

/**
 * 可比较属性点击后的带折线图的弹层
 * @type {React.FunctionComponent<ComparableChartIndexModalProps>}
 */
const ComparableChartIndexModal = ({ visible, onCloseIndexDetailModal }) => {
  return (
    <Modal
      visible={visible}
      width={724}
      footer={null}
      onCancel={onCloseIndexDetailModal}
    >
      <ChartOptionForm />
      <ComparableIndexChart />
    </Modal>
  );
};

类组件

就把上面的 Modal 改为类组件。我们需要用@extend注解,告诉编辑器和自己,下面的组件继承自React.Component<ComparableChartIndexModalProps, ComparableChartIndexModalProps>。其中ComparableChartIndexModalProps也是我们上面的属性。ComparableChartIndexModalState为组件 state 的类型。之后,我们就能从this.statethis.props中得到其中的各个带类型的属性。

/**
 * @typedef ComparableChartIndexModalProps
 * @property {boolean} visible 弹窗可见性
 * @property {Function} onCloseIndexDetailModal 关闭弹窗时的回调
 */

/**
 * @typedef ComparableChartIndexModalState
 * @property {number} openCount Modal打开的次数
 */

/**
 * 可比较属性点击后的带折线图的弹层
 * @extends {React.Component<ComparableChartIndexModalProps, ComparableChartIndexModalState>}
 */
class ComparableChartIndexModal extends React.Component {
  render() {
    const { visible, onCloseIndexDetailModal } = this.props;

    return (
      <Modal
        visible={visible}
        width={724}
        footer={null}
        onCancel={onCloseIndexDetailModal}
      >
        <ChartOptionForm />
        <ComparableIndexChart />
      </Modal>
    );
  }
}

注解变量类型

使用@type标签注解类型

/**
 * @typedef TableRow
 * @property {number} age 年龄
 * @property {string} name 姓名
 * @property {boolean} isChecked 是否选中
 * @property {string?} ID 身份ID,为空时未登记
 */

/**
 * @type {TableRow}
 */
const row = getCertainRow();

从外部(.d.ts 文件等)引入类型

// 可以使用直接通过import语句导入的类型
// 如果触发了eslint的unused var规则,可以用第二种方法
import React from 'react';

/**
 * @type {React.FC<{}>}
 *
 */

// 第二种,使用typedef + import引入
/**
 * @typedef {import('antd/lib/form').FormComponentProps} FormComponentProps
 */

何时使用 JSDoc 注解组件、方法、类型。

对于公共组件和公共方法,通过 JSdoc 注解类型并且写上提示性的问题收益是极大的。这样会使得调用你写的公共组件、方法的同事使用它们的难度,增进同事之间的友情 (和基情)

对于业务组件,强烈建议书写。业务组件的 props 可能会很长,对 props 类型和含义的注释,能显著减少他人的维护成本。

至于类方法中的各种回调,个人认为有时间可以写一下,让别人更容易明白你的代码。

最后关于变量的类型注释,请酌情考虑自动补全带来的收益与写类型注解的成本。比如 React 中有个React.CSSProperties属性,可以告诉编辑器,这个变量是会成为 React 组件的 Style 的,可以考虑使用。

最后一些想法

这篇文章讲 JSDoc 与 React,仅仅只是入门用,许多想讲的地方因为篇幅原因未能完善。大家可以移步JSDoc 官网TypeScript 官网。特别是类型系统和泛型的使用。 最后,个人对 JSDoc 的看法是

JSDoc is the gateway to TypeScript!😂