Vue框架在前端開發中應用普遍,當一個多人開發的Vue項目通過長期維護以後每每會沉澱出不少的公共組件,這個時候常常會出現一我的 開發了一個組件而其餘維護者或新接手的人殊不知道這個組件是作什麼的、該怎麼用,還必須得再去翻看源碼,或者壓根就沒注意到這個組件 的存在致使重複開發。這個時候就很是須要維護對應的組件文檔來保障不一樣開發者之間良好的協做關係了。前端
可是傳統的手動維護文檔又會帶來新問題:vue
而理想中的文檔維護方式則是:java
爲了能實現上述理想效果,我搜索並研究了一下社區中的解決方案,目前Vue官方提供了Vue-press能夠用於快速搭建Vue項目文檔, 並且也已經有了能夠自動從Vue組件中提取信息的庫了。node
可是已有的第三方庫並不能徹底知足需求,主要存在如下兩個問題:json
信息不全面,一些重要內容沒法獲取例如不能處理v-model,不能解析屬性的修飾符sync,不能獲取methods中函數入參的詳細信息等。babel
好比下面的例子,value屬性與input事件能夠合起來構成一個v-model屬性,可是這個信息在生成的文檔中沒有體現出來,要文檔讀者自行理解判斷。並且生成的文檔中沒有展現是否支持sync。框架
有較多的自定義標識,並且標識的命名過於個性化,對原有的代碼侵入仍是比較大的。例以下圖中的代碼,爲了標記註釋,須要在原有的 業務代碼中額外添加"@vuese" "@arg"等標識,使得業務代碼多出了一些業務無關內容。
針對以上文中提到的問題以及社區方案的不足,咱們團隊內沉澱出了一個小工具專門用於Vue組件信息獲取並輸出組件文檔,大體效果以下:dom
上圖中左邊是一個常見的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,下一步咱們就能夠從中獲取咱們想要的信息了。
根據是否須要約定,信息能夠分爲兩種:
一種是能夠直接從Vue組件中獲取,例如props、events等。
另外一種是須要額外約定格式的,例如:組件的說明註釋,props的屬性說明等,這部分能夠放到註釋裏,經過對註釋進行解析獲取。
爲了方便的從ast中讀取信息,這裏先簡單介紹一個工具@babel/traverse,這個庫是babel官方提供的專門用於遍歷js AST的。使用方式以下;
import traverse from '@babel/traverse' traverse(jsAst, options);
經過在options中配置對應內容的回調函數,能夠得到想要的ast節點。具體的使用能夠參考官方文檔
能夠從代碼中直接獲取的信息能夠有效的解決信息同步問題,不管代碼怎麼變更,文檔的關鍵信息均可以自動同步,省去了人工校對的麻煩。
能夠直接獲取的信息有:
一、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。
爲何除了可直接獲取的組件信息以外,還會須要額外的約定一部份內容呢?其一是由於可直接獲取的信息內容比較單薄,還不足以支撐起一個相對完善的組件文檔;其二是咱們平常開發組件時自己就會寫不少的註釋,若是能直接將部分註釋提取出來放到文檔中,能夠大大下降文檔維護的工做量;
整理一下能夠約定的內容有如下幾條:
接下來咱們着重講解如何將提取註釋和註釋與被註釋的內容是如何對應起來的。
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