一、现状

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