解析如何自動化生成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的文檔,不僅有組件信息還有展示實例。如果一個組件庫維護的相對完善的話,一個組件應該會有對應的測試用例,那麼是否可以將組件的測試用例也提取出來, 實現組件文件中示例部分的自動提取呢?這也是值得研究的問題。

以上就是解析如何自動化生成vue組件文檔的詳細內容,更多關於自動化生成vue組件文檔的資料請關註WalkonNet其它相關文章!

推薦閱讀: