解放生產力,自動化生成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