首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

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

一、现状

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

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

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

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

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

二、社区解决方案

2.1 业务梳理

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

但是已有的第三方库并不能完全满足需求,主要存在以下两个问题:

信息不全面,一些重要内容无法获取例如不能处理 v-model,不能解析属性的修饰符 sync,不能获取 methods 中函数入参的详细信息等。 比如下面的例子,value 属性与 input 事件可以合起来构成一个 v-model 属性,但是这个信息在生成的文档中没有体现出来,要文档读者自行理解判断。而且生成的文档中没有展示是否支持 sync。

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

三、技术方案

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

上图中左边是一个常见的 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

1、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 相同的格式,主要还是因为这套方案 使用比较普遍,而且代码编辑器都自动支持方便编辑。

四、总结

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

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

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

五、展望

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

作者:vivo 互联网前端团队-Feng Di

  • 发表于:
  • 本文为 InfoQ 中文站特供稿件
  • 首发地址https://www.infoq.cn/article/67f970a523f3c30d50c31a777
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券