一、現狀
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