解放生产力,自动化生成vue组件文档

1、现状

Vue框架在前端开发中应用普遍,当一个多人开发的Vue项目通过长期维护以后每每会沉淀出不少的公共组件,这个时候常常会出现一我的 开发了一个组件而其余维护者或新接手的人殊不知道这个组件是作什么的、该怎么用,还必须得再去翻看源码,或者压根就没注意到这个组件 的存在致使重复开发。这个时候就很是须要维护对应的组件文档来保障不一样开发者之间良好的协做关系了。前端

可是传统的手动维护文档又会带来新问题:vue

  • 效率低,写文档是个费时费力的体力活,好不容易抽时间把组件开发完了回头还要写文档,想一想都头大。
  • 易出错,文档内容容易出现差错,可能与实际组件内容不一致。
  • 不智能,组件更新迭代的同时,须要手动将变动同步到文档中,消耗时间还容易遗漏。

而理想中的文档维护方式则是:java

  • 工做量小,可以结合Vue组件自动获取相关信息,减小从头开始写文档的工做量。
  • 信息准确,组件的关键信息与组件内容一致,不出错。
  • 智能同步,Vue组件迭代升级时,文档内容能够自动的同步更新,无需人工校验信息是否一致。

2、社区解决方案

2.1 业务梳理

为了能实现上述理想效果,我搜索并研究了一下社区中的解决方案,目前Vue官方提供了Vue-press能够用于快速搭建Vue项目文档, 并且也已经有了能够自动从Vue组件中提取信息的库了。node

可是已有的第三方库并不能彻底知足需求,主要存在如下两个问题:json

信息不全面,一些重要内容没法获取例如不能处理v-model,不能解析属性的修饰符sync,不能获取methods中函数入参的详细信息等。babel

好比下面的例子,value属性与input事件能够合起来构成一个v-model属性,可是这个信息在生成的文档中没有体现出来,要文档读者自行理解判断。并且生成的文档中没有展现是否支持sync。框架

有较多的自定义标识,并且标识的命名过于个性化,对原有的代码侵入仍是比较大的。例以下图中的代码,为了标记注释,须要在原有的 业务代码中额外添加"@vuese" "@arg"等标识,使得业务代码多出了一些业务无关内容。

3、技术方案

针对以上文中提到的问题以及社区方案的不足,咱们团队内沉淀出了一个小工具专门用于Vue组件信息获取并输出组件文档,大体效果以下:dom

上图中左边是一个常见的Vue单文件组件,右边是生成的文档。咱们能够看到咱们从组件中成功的提取到了如下一些信息:编辑器

  • 组件的名称。
  • 组件的说明。
  • props,slot,event,methods等。
  • 组件的注释内容。

接下来咱们将详细的讲解如何从组件中提取这些信息。函数

3.1 Vue文件解析

既然是要从Vue组件中提取信息,那么首先的问题就是如何解析Vue组件。Vue官方开发了Vue-template-compiler库专门用于Vue解析, 这里咱们也能够用一样的方式来处理。经过查阅文档可知Vue-template-compiler提供了一个parseComponent方法能够对原始的Vue文件进行处理。

import { parseComponent } from 'Vue-template-compiler'
const result = parseComponent(VueFileContent, [options])

处理后的结果以下,其中template和script分别对应Vue文件中的template和script的文本内容。

export interface SFCDescriptor {
  template: SFCBlock | undefined;
  script: SFCBlock | undefined;
  styles: SFCBlock[];
  customBlocks: SFCBlock[];
}

固然仅仅是获得文本是不够的,还须要对文本进行更进一步的处理来获取更多的信息。获得script后,咱们能够用babel把js编译成js的AST(抽象语法树),这个AST是一个普通的js对象,能够经过js进行遍历和读取 有了Ast以后咱们就能够从中获取到咱们想到详细的组件信息了。

import { parse } from '@babel/parser';
const jsAst = parse(script, [options]);

接着咱们来看template,继续查找Vue-template-compiler的文档咱们找到compile方法,compile是专门用于将template编译成AST的, 正好能够知足需求。

import { compile } from 'Vue-template-compiler'
const templateAst = compile(template, [options]);

获得结果中的ast则为template的编译结果。

export interface CompiledResult {
  ast: ASTElement,
  render: string,
  staticRenderFns: Array<string>,
  errors: Array<string>
}

经过第一步的文件解析工做,咱们成功获取到了Vue的模板ast和script中的js的AST,下一步咱们就能够从中获取咱们想要的信息了。

3.2 信息提取

根据是否须要约定,信息能够分为两种:

一种是能够直接从Vue组件中获取,例如props、events等。

另外一种是须要额外约定格式的,例如:组件的说明注释,props的属性说明等,这部分能够放到注释里,经过对注释进行解析获取。

为了方便的从ast中读取信息,这里先简单介绍一个工具@babel/traverse,这个库是babel官方提供的专门用于遍历js AST的。使用方式以下;

import traverse from '@babel/traverse'

traverse(jsAst, options);

经过在options中配置对应内容的回调函数,能够得到想要的ast节点。具体的使用能够参考官方文档

3.2.1 可直接获取的信息

能够从代码中直接获取的信息能够有效的解决信息同步问题,不管代码怎么变更,文档的关键信息均可以自动同步,省去了人工校对的麻烦。

能够直接获取的信息有:

  • 组件属性props
  • 提供外部调用的方法methods
  • 事件events
  • 插槽slots

一、2均可以利用traverse在js AST上直接遍历名称为props和methods的对象节点获取。

事件的获取稍微麻烦一点,能够经过查找$emit函数来定位到事件的位置,而$emit函数能够在traverse中监听MemberExpress(复杂类型节点), 而后经过节点上的属性名是不是'$emit'判断是不是事件。若是是事件,那么在$emit父级中读取arguments字段, arguments的第一个元素就是事件名称,后面的元素为事件传参。

this.$emit('event', arg);
traverse(jsAst, {
 MemberExpression(Node) {
  // 判断是否是event
  if (Node.node.property.name === '$emit') {
  // 第一个元素是事件名称
    const eventName = Node.parent.arguments[0];
  }
 }
});

在成功获取到Events后,那么结合Events和props,就能够进一步的判断出props中的两个特殊属性:

是否存在v-model:查找props中是否存在value属性而且Events中是否存在input事件来肯定。

props的某个属性是否支持sync:判断Events的时间名中是否存在有update开头的事件,而且事件名称与属性名相同。

插槽slots的信息保存在上文的template的AST中,递归遍历template AST找到名为slots的节点,进而还能够在节点上查找到name。

3.2.2 须要约定的信息

为何除了可直接获取的组件信息以外,还会须要额外的约定一部份内容呢?其一是由于可直接获取的信息内容比较单薄,还不足以支撑起一个相对完善的组件文档;其二是咱们平常开发组件时自己就会写不少的注释,若是能直接将部分注释提取出来放到文档中,能够大大下降文档维护的工做量;

整理一下能够约定的内容有如下几条:

  • 组件名称。
  • 组件的总体介绍。
  • props、Events、methods、slots文字说明。
  • Methods标记和入参的详细说明。这些内容均可以放在注释中进行维护,之因此放在注释中进行维护是由于注释能够很容易从上文提到的js AST以及template AST中获取到, 在咱们解析Vue组件信息的同时就能够把这部分针对性的说明一块儿解析到。

接下来咱们着重讲解如何将提取注释和注释与被注释的内容是如何对应起来的。

js中的注释根据位置不一样能够分为头部注释(leadingComments)和尾部注释(trailingComments),不一样位置的注释会存放在对应的字段中, 代码展现以下:

// 头部注释export default {} // 尾部注释

解析结果

const exportNode = {
  type: "ExportDefaultDeclaration",
  leadingComments: [{
    type: 'CommentLine',
    value: '头部注释'
  }],
  trailingComments: [{
    type: 'CommentLine',
    value: '尾部注释'
  }]
}

在同一个位置上,根据注释格式的不一样又分为单行注释(CommentLine)和块级注释(CommentBlock),两种注释的区别会反应在注释节点的type字段中:

/**
 * 块级注释
 */
// 单行注释
export default {}

解析结果

const exportNode = {
  type: "ExportDefaultDeclaration",
  leadingComments: [
    {
      type: 'CommentBlock',
      value: '块级注释'
    },
    {
      type: 'CommentLine',
      value: '单行注释'
    }
  ]
}

另外,从上面的解析结果咱们也能够看到,注释节点是挂载在被注释的export节点里面的,这也解决咱们上面提到的另外一个问题:注释与被注释的关联关系怎么获取的--其实babel在编译代码的时候已经替咱们作好了。

template查找注释与被注释内容的方法不一样。template中注释节点与其余节点同样是做为dom节点存在的, 在遍历节点的时候经过判断isComment字段的值是否为true来肯定是不是注释节点。而被注释的内容的位置在兄弟节点的后一位:

<!--template的注释-->
<slot>被注释的节点</slot>

解析结果

const templateAst = [
  {
    isComment: true,
    text: "template的注释",
    type: 3
  },
  {
    tag: "slot",
    type: 1
  }
]

知道了如何处理注释内容,那么咱们还能够利用注释作更多的事情。例如能够经过在methods的方法的注释中约定一个标记@public来区分是私有方法仍是公共方法,若是更细节一点的话, 还能够参考另外一个专门用于解析js注释的库js-doc的格式,对方法的入参进行更进一步的说明,丰富文档的内容。

咱们只须要在获取到注释内容以后对文本进行切割读取便可,例如:

export default {
  methods: {
    /**
     * @public
     * @param {boolean} value 入参说明
     */
    show(value) {}
  }
}

固然了为了不对代码侵入过多,咱们仍是须要尽可能少的添加额外的标识。而入参说明采用了与js-doc相同的格式,主要仍是由于这套方案 使用比较广泛,并且代码编辑器都自动支持方便编辑。

4、总结

编写组件文档是一个能够很好的提高项目内各个前端开发成员之间协做的事情,一份维护良好的文档会极大的改善开发体验。而若是能进一步的使用工具把维护文档的过程自动化的话,那开发的幸福感还能获得再次提高。

通过一系列的摸索和尝试,咱们成功的找到了 自动化提取Vue组件信息的方案,大大减轻了维护Vue组件文档的工做量,提高了文档信息的准确度。具体实现上,先用vue-template-compiler对Vue文件进行处理,得到template的AST和js的AST,有了这两个AST后就能够去获取更加详细的信息了, 梳理一下到目前为止咱们生成的文档里能够获取到的内容及获取方式:

至于获取到内容以后是以Markdown的形式输出仍是json文件的形式输出,就取决于实际的开发状况了。

5、展望

这里咱们所讨论的是直接从单个Vue文件去获取信息并输出,可是像不少第三方组件库里例如elementUI的文档,不只有组件信息还有展现实例。若是一个组件库维护的相对完善的话,一个组件应该会有对应的测试用例,那么是否能够将组件的测试用例也提取出来, 实现组件文件中示例部分的自动提取呢?这也是值得研究的问题。

做者:vivo互联网前端团队-Feng Di
相关文章
相关标签/搜索