共计 5213 个字符,预计需要花费 14 分钟才能阅读完成。
一、现状
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