一個巧合,我把文檔寫進了代碼里

  • 2021 年 2 月 22 日
  • 筆記

最近因為公司業務的調整,項目需要開發大量的業務組件、高復用邏輯提供給客戶使用。當各類組件、代碼多了以後,加上團隊內幾個成員書寫習慣、開發思想的不同,出現了好多問題。尤其兩個問題最嚴重:

  1. 大量的業務組件/業務邏輯需要通過查源代碼的方式,或者問寫組件的人,才能知道組件是否有自己需要的屬性/鉤子方法
  2. 有些組件因為產品需求 + 口頭溝通 + 需求妥協,只能應用於某一個特定的情況下,其他人看設計圖或者邏輯差不多相似就直接拿過來用,結果發現用不了/各種問題

為了解決這兩個問題,就開始要求組員在開發業務組件的同時,必須寫對應的開發文檔/代碼注釋。一開始還好,中後期開發文檔的更新明顯跟不上組件的迭代,逐漸地又回到了靠嘴問的情況,第2個問題也是隨着時間推移又回到了起點。

某天通過VS Code調試代碼的時候忽然發現,用鼠標在原生語法和react的方法上懸浮幾秒鐘,就會出現一個提示框,裏面有一些節點/組件/方法的簡單介紹,參數等。

對,這就是我想要的效果!

原生語法 (如document.getElementById):
document.getElementById

react的方法(如useState):
useState

通過ctrl + 鼠標左鍵點開類型定義,發現提示框里的內容其實是相關代碼上方的注釋。
類型定義
按照類型定義裏面的注釋,我在代碼里輸入/**的時候出現了如下圖的提示。
JSDOC提示

拿着關鍵詞我去VS Code的官網搜索了一番,在官網搜到了答案(點擊此處)。

VS Code understands many standard JSDoc annotations, and uses these annotations to provide rich IntelliSense.

VS Code 可以理解標準的JSDoc代碼注釋,並使用這些注釋提供豐富的智能感知(如智能代碼完成,懸停信息和簽名信息)

JSDoc的語法也非常簡單,只需要保證注釋的開頭是/**即可,其他與多行注釋沒有什麼差別。(更多語法:點擊此處

/** 這樣便創建了一個代碼提醒 */
function remind() {}

上手寫個組件試試效果!

import React, { useEffect, useState } from 'react'

interface KeywordInterface {
  /**
   * 關鍵詞
   */
  keyword?: string;
  /**
   * 高亮顯示的顏色,支持hex、hsl、rgba、keywords
   */
  color?: string;
  children?: string;
}

/**
 * 關鍵詞高亮組件
 * 
 * @example <LightKeyword keyword="hello">Hello World</LightKeyword>
 * 
 * @param { string } keyword - 關鍵詞
 * @param { string } color - 高亮顯示的顏色
 */
const LightKeyword: React.FC<KeywordInterface> = ({
  color = '',
  keyword = '',
  children = ''
}) => {

  const [ context, setContext ] = useState('')

  useEffect(() => {
    // 當關鍵詞為空時,無需對內容做高亮顯示
    if( !keyword ) { 
      return setContext(children)
    }

    const pattern = new RegExp(keyword, 'gi')
    // 通過正則把關鍵詞過濾出來並增加HTML節點
    const allword = (children as string).replace(pattern, (word) => `<i class="light-keyword-item" ${ color && `style="color: ${ color }"` }>${ word }</i>`)

    setContext(allword)
  }, [ keyword, color, children ])

  return (
    <span className="light-keyword" dangerouslySetInnerHTML={{ __html: context }}></span>
  )
}

export default LightKeyword

效果展示:

當鼠標懸浮在組件上時:
當鼠標懸浮在組件上時

當數據懸浮在組件屬性上時:
當數據懸浮在組件屬性上時

完美!這樣只要按格式寫好注釋,就可以不用那麼麻煩地去查文檔了。(前提是得寫)

那如果是業務邏輯呢?因此我寫了一段基於業務封裝的異步請求代碼。



import qs from 'qs'
import { message } from 'antd'
import axios, { AxiosRequestConfig } from 'axios'

interface configInterface {
  /**
   * 請求地址
   */
  url: string;
  /**
   * 請求方式
   */
  method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
  /**
   * 請求參數
   */
  data?: any;
  /**
   * 其他配置參數
   * 
   * @param { Object }  headers 請求頭配置
   * @param { boolean } errorMessage 是否啟用錯誤提醒
   * @param { string }  responseType 請求類型,默認為json
   * @param { boolean } withCredentials 是否攜帶跨域憑證
   */
  options?: {
    /**
     * 請求頭配置
     */
    headers?: any;
    /**
     * 是否啟用錯誤提醒
     */
    errorMessage?: boolean;
    /**
     * 請求類型,默認為json
     */
    responseType?: 'json' | 'arraybuffer' | 'blob' | 'document' | 'text' | 'stream';
    /**
     * 是否攜帶跨域憑證
     */
    withCredentials?: boolean
  }
}

// axios全局配置
const $axios = axios.create({

  // 請求接口地址
  baseURL: '//demo.com',
  // 超時時間
  timeout: 60 * 1000
})

/**
 * 異步請求
 * 
 * @description 基於現有業務封裝,自動處理GET請求序列化/錯誤碼處理反饋/跨域配置等操作
 * @example useRequest<T>({ url: 'api/weather', method: 'GET', data: { date: '2021-02-30' }, options: {} })
 * @typedef requestConfig 請求參數
 * @param   { string } requestConfig.url 請求地址
 * @param   { string } requestConfig.method 請求方式
 * @param   { any }    requestConfig.data 請求參數
 * @param   { object } requestConfig.options 其他配置參數
 */
const useRequest = async <T>(requestConfig: configInterface): Promise<T> => {

  const requestOptions = requestConfig.options || {}

  const axiosConfig: AxiosRequestConfig = {

    url: requestConfig.url,
    method: requestConfig.method || 'GET',
    headers: requestOptions.headers || {},
    responseType: requestOptions.responseType || 'json',
    withCredentials: requestOptions.withCredentials !== false
  }

  // 請求方式為GET時,對參數進行序列化處理
  if( axiosConfig.method === 'GET' ) {

    axiosConfig.params = requestConfig.data || {}
    axiosConfig.paramsSerializer = (params) => qs.stringify(params, { arrayFormat: 'brackets' })

  } else {

    axiosConfig.data = requestConfig.data || {}
  }

  try {

    const { data: response } = await $axios(axiosConfig)

    // 如後端返回錯誤碼,將錯誤推入catch句柄執行
    if( response.code !== 0 ) {

      // 錯誤提醒
      if( requestOptions.errorMessage !== false ) {

        message.error(response.message || '未知錯誤')
      }
      
      return Promise.reject(response)
    }

    return Promise.resolve(response)

  } catch(e) {

    // 錯誤提醒
    if( requestOptions.errorMessage !== false ) {

      message.error('請求錯誤,請稍後重試')
    }

    return Promise.reject(e)
  }
}

export default useRequest

實際效果:
懸停提示
(基本用法及參數提醒)
額外配置提醒
(額外配置提醒)

配合Typescript,幾乎就是把文檔寫進了代碼里!!!

然而當我興緻勃勃地搭建vue 3的開發環境,想嘗試一下vue的智能提示。經過多輪測試,JSDoc的智能提示只支持在js/ts/tsx這幾類的文件,並不支持.vue格式的文件。
vue文件不支持jsdoc的智能提示
vue文件不支持jsdoc的智能提示)

如果希望在vue文件中也有類似的智能提示,可以通過VS Code安裝vetur插件,然後在項目根目錄下創建名為vetur的文件夾,並新建tags.jsonattributes.json兩個文件,然後在package.json中引入兩者的路徑 。

// package.json
{
  "name": "demo",
  "version": "0.1.0",
  "vetur": {
    "tags": "./vetur/tags.json",
    "attributes": "./vetur/attributes.json"
  }
}

// vetur/tags.json
{
  "light-keyword": {
    "attributes": ["keyword", "content", "color"],
    "description": "關鍵詞高亮組件"
  }
}

// vetur/attributes.json
{
  "color": {
    "type": "string",
    "description": "高亮顯示的顏色,支持hex、hsl、rgba、keywords"
  },
  "content": {
    "type": "string",
    "description": "文本內容"
  },
  "keyword": {
    "type": "string",
    "description": "關鍵詞"
  }
}

最後的實現效果
組件智能提示
(組件智能提示)
組件描述
(組件描述)
屬性描述
(屬性描述)

好處是不受vue版本的限制,2和3都可以用;壞處是json文件的限制,沒有辦法像JSDoc一樣顯示豐富的格式和代碼片段,希望vetur能夠加強這方面的優化吧。