程式設計師最討厭的兩件事情,第一種是寫文件,另一種是別人沒有寫文件。有沒有直接根據vue元件生成文件的呢?當然是有的的。但第三方使用起來不一定能和現有專案結合使用,往往需要額外的註釋用來標記提取資訊。使用第三方的一些比較常見問題
- 文件提取資訊不全面,可能有些資訊你需要提取但是它又不支援。這種情況下就只能修改三方的外掛原始碼了。
- 需要額為的註釋資訊來標記,例如 vuese 需要給方法 打 @vuese、@arg 等標記來提供方法資訊。
俗話說自己動手豐衣足食,打造自己的vue文件生成工具與自己專案結合使用。一個元件文件大致需要提供 元件名稱和描述(name)、元件屬性(props)、元件方法(methods)、元件事件(event)、插槽(slot) 這幾個部分,以及還需要這個幾個部分的註釋組成生成描述資訊。接下來一步步實現對著幾個部分的提取實現。
解析.vue 檔案
一般一個.vue檔案分三個部分 template、script、style、style部分的內容我們不需要,我們需要分別提取出 template 和 script 內容。Vue官方開發了 Vue-template-compiler 庫專門用於Vue解析,我們可以直接使用它來解析提取.vue檔案, Vue-template-compiler 提供了一個 parseComponent 方法可以對原始的Vue檔案進行處理。
const compiler = require('vue-template-compiler')
const result = compiler.parseComponent(vueStr, [options])
// parseComponent 返回 template、script、style內容,
export interface SFCDescriptor {
template: SFCBlock | undefined;
script: SFCBlock | undefined;
styles: SFCBlock[];
customBlocks: SFCBlock[];
}
拿到各個部分文字後,還需要將它轉成ast(抽象語法樹),template 部分內容可以直接使用 Vue-template-compiler 提供的 compile 方法直接生成ast, script部分需要藉助其他的生成ast了,這裡使用 babel 的模組來處理 js 文字。
const compiler = require('vue-template-compiler')
//vueStr .vue 檔案內容
const vue = compiler.parseComponent(vueStr)
//生成html部分的 ast
let template = compiler.compile(vue.template.content, {
preserveWhitespace: false,
comments: true // 生成註釋資訊
})
使用 @babel/parser(Babel解析器,是Babel中使用的JavaScript解析器)來處理js 文字內容。
const parse = require('@babel/parser');
//生成js部分的 ast
let jsAst = parse.parse(vue.script.content, {
allowImportExportEverywhere: true
})
提取文件資訊
通過上一步的檔案解析工作,我們成功獲取到了Vue的模板ast和script中的js的ast,下一步我們就可以從中獲取我們想要的資訊了。這裡需要使用到 @babel/traverse 這個工具,用來遍歷 js ast 的節點工具。可以在這裡檢視 ast 的生成內容,方便檢視各種節點資訊。
const traverse = require('@babel/traverse');
traverse.default(jsAst, {
enter(path){ // 開始
},
// 支援自定義節點 比如當節點型別 為 ExportDefaultDeclaration 時掉這個方法
ExportDefaultDeclaration(){
}
})
提取元件名稱、描述、props、methods、model
export default 生成的對應節點型別是 ExportDefaultDeclaration,declaration 屬性就是對應的元件的 options 了,遍歷 declaration 的屬性可以獲取到 name、props、methods、model 等節點資訊。
示例
let componentInfo = {}
traverse.default(jsAst, {
ExportDefaultDeclaration(path){
path.node.declaration.properties.forEach(item => {
switch (item.key.name) {
case 'props':
componentInfo.props = extractProps(item) // 提取 props
break;
case 'methods':
componentInfo.methods = extractMethods(item) // 提取 methods
break
case 'name':
componentInfo.name = item.value.value // 獲取元件名稱
break
case 'model':
componentInfo.model = extractModel(item) // 提取 model
break
default:
break;
}
});
}
})
提取描述
js中註釋分為單行和多行兩種,生成ast也會生成不同型別的,可以看下面例子。
/**
* 多行備註
* 用來上傳文件資訊
*/
// 單行備註
export default {
}
// 結尾註釋
可以看到會 CommentBlock、 CommentLine 兩種型別的節點,還有頭部的會放在 leadingComments 裡,底部的註釋在 trailingComments 裡。
一般會把元件描述註釋放在 export default 上面,簡單提取註釋資訊
// ExportDefaultDeclaration 插入如下程式碼
if (path.node.leadingComments) {
componentInfo.desc = path.node.leadingComments.map(item => {
if (item.type === 'CommentLine') {
return item.value.trim()
} else {
return item.value.split('\n').map(item => item.replace(/[\s\*]/g, '')).filter(Boolean)
}
}).toString()
}
提取 methods
因為 methods 中的註釋需要額外描述 出參、入參等資訊需要額外處理,jsdoc註釋規範使用還是比較大眾的,這裡根據需要自己定義提取規則,還需要提取 async 用來標識是否是非同步函式。
/**
* 方法描述
* @param {Bool} type 引數描述
* @returns 返回值描述
*/
提取 props
props 的提取需要區分下面幾種情況,default 和 validator 還是提取還是有點麻煩的,validator 校驗還可以通過註釋簡單描述來提取,但是 default 就不好處理了。
{
propA: Number, // 只有型別
propB: [String, Number], // 只有型別但是支援多種
propC: {
type: String,
required: true
},
propD: {
type: Number,
default: 100 // 帶有預設值
},
propE: {
type: Object,
default () { // 預設值 需要函式返回
return { message: 'hello' }
}
},
propF: {
default: function () { // 預設值 需要函式返回 和上面的 default 的 ast 節點型別是不同的
return { message: 'hello' }
}
validator: function (value) { // 校驗
return ['success', 'warning', 'danger'].indexOf(value) !== -1
}
}
}
我這裡對 default 處理是藉助 @babel/generator 將 default 轉換程式碼, 通過eval轉成函式呼叫返回會預設值。types 是 @babel/types 模組,用來判斷節點型別的。
// 獲取Props預設值
function getDefaultVal (node) {
if (types.isRegExpLiteral(node) || types.isBooleanLiteral(node) || types.isNumericLiteral(node) || types.isStringLiteral(node)) {
return node.value
} else if (types.isFunctionExpression(node) || types.isArrowFunctionExpression(node) || types.isObjectMethod(node)) {
try {
let code = generate.default(types.isObjectMethod(node) ? node.body : node).code
let fun = eval(**0,${types.isObjectMethod(node) ? 'function ()' : ''} ${code}**)
return JSON.stringify(fun())
} catch (error) {
}
}
}
提取 model
這個比較簡單,直接獲取就可以了。
提取元件Events
元件的事件沒法直接獲取到對應節點,只能通過 $emit 方法來定位事件位置,在 traverse 中可以使用 MemberExpress(複雜型別節點),然後通過節點上的屬性名是否是 $emit 判斷是否是事件。
可以看到事件名稱在 MemberExpress 父級上的 arguments 裡,而備註則在更上一層的裡。
const extractEvents = (path) => {
// 第一個元素是事件名稱
const eventName = path.parent.arguments[0];
let comments = path.parentPath.parent.leadingComments
return {
name: eventName.value,
desc: comments ? comments.map(item => item.value.trim()).toString() : '——'
}
}
MemberExpression (path) {
// 判斷是不是event
if (path.node.property.name === '$emit') {
let event = extractEvents(path)
!componentInfo.events && (componentInfo.events = {});
if (componentInfo.events[event.name]) {
componentInfo.events[event.name].desc = event.desc ? event.desc : componentInfo.events[event.name].desc
} else {
componentInfo.events[event.name] = event
}
}
}
在成功獲取到Events後,那麼結合Events、Props、Model,就可以進一步的判斷屬性是否支援 .sync 和 v-model。
提取元件Slots
首先需要寫一個對Vue模板的ast遍歷的函式,Vue-template-compiler 沒有提供類似於 @babel/traverse 用來 遍歷 ast 的。
簡單實現個遍歷模板抽象樹函式
const traverserTemplateAst = (ast, visitor = {}) => {
function traverseArray (array, parent) {
array.forEach(child => {
traverseNode(child, parent);
});
}
function traverseNode (node, parent) {
visitor.enter && visitor.enter(node, parent);
visitor[node.tag] && visitor[node.tag](node, parent);
node.children && traverseArray(node.children, node);
visitor.exit && visitor.exit(node, parent);
}
traverseNode(ast, null);
}
Vue模板的ast的結構還是比較清晰的,沒有js ast 那麼多的型別,只需要區分不同tag就可以了。註釋會單獨一個節點,所以在查詢 slot 節點時候,還需要去找它上一個相鄰節點,判斷是否是註釋。
traverserTemplateAst(template.ast, {
slot (node, parent) {
!componentInfo.slots && (componentInfo.slots = {})
// 獲取節點位置
let index = parent.children.findIndex(item => item === node)
let desc = '無描述', name = '-';
if (index > 0) {
let tag = parent.children[index - 1]
// isComment 判斷是否是 註釋
if (tag.isComment) {
desc = tag.text.trim()
}
}
if (node.slotName) name = node.attrsMap.name
componentInfo.slots[name] = {
name,
desc
}
}
})
結語
到這裡簡單的實現了自動化生成vue元件資訊了,當然還有幾種情況還沒有考慮進去,例如事件$emit 在 template 中,slot 在 render 函式中時候的情,不過提取這部分實現也是大同小異的了。可以在 這裡檢視 本文原始碼。