Node.js躬行記(18)——半吊子的可視化搭建系統

  我們組維護的管理後臺會接到很多開發需求,每次新開頁面,就會到處複製黏貼相關代碼。

  並且還會經常性的翻閱文檔,先在書籤或地址欄輸入WIKI地址,然後找到那一份說明文檔,再定位到要看的組件位置。

  雖然單人損耗的時間並不是非常多,但還是會打斷思路,影響開發的流暢性,當把所有人的時間累加起來,那損耗的時間也很可觀。

  爲了能提升團隊成員的開發效率,就開始構思一套可視化搭建系統。理想狀態下,拖動組件,配置交互和樣式,頁面生成,直接可用。

  但是要完成這套功能,開發成本比較大,現在我想先解決當前的痛點,減少代碼複製的頻率和快速讀取組件文檔。

  爲此,在構思了好多天後,打算搞一個半吊子的可視化搭建系統。

  所謂半吊子是指搭建完後,點擊生成,會在後臺創建視圖和數據兩個腳本文件、自動添加權限、新增菜單欄,不過後續我們還得繼續做開發,完善頁面功能。

一、界面

  界面分成左右兩部分,左邊是配置區域,右邊空白處是組件的預覽區域。

  

1)組件區域

  組件區域的第一個下拉框可以選擇Ant Design和部分模板組件,選中後,會替換組件地址的鏈接,點擊就能跳轉到組件的說明文檔。

  第二個下拉框能選擇頁面中需要的組件,例如圖中的提示組件,點擊添加後會在右邊顯示,並且還會提供一個刪除圖標,目前暫不支持拖動效果。

  

2)配置區域

  在配置區域中,可以輸入菜單名稱、路由、文件目錄和權限等信息。

  原先的話,還得手動的在路由和權限兩個文件中新增配置項,現在都能自動化了。

  原理就是先用Node分別讀取這兩份文件,得到一個數組,然後將配置內容塞到此數組中,再將數組序列化寫入文件內。

  注意,需求在引入模塊(調用require()函數)前刪除模塊緩存,否則讀到的將是之前的文件內容。

//權限文件的絕對路徑
const absAuthorityPath = pathObj.resolve(__dirname, 'src/utils/authority.ts');  
delete require.cache[absAuthorityPath];              //刪除模塊緩存
const authorities = require(absAuthorityPath);
const obj = {
  id: authority,
  pid: parent,
  name: menu,
  desc: '',
  routers: currentPath,
};
authorities.push(obj);       //添加權限
//寫入文件
fs.writeFileSync(absAuthorityPath, `module.exports = ${JSON.stringify(authorities, null, 2)}`);

  fs.writeFileSync()用於同步寫入文件。module.exports是Node的模塊語法,而export default是ES6語法,Node原生並不支持,好在webpack對於這些模塊化語法都支持。

  一旦點擊生成文件按鈕,在項目重新構建後,左邊菜單列表就能出現剛剛配置的菜單名稱(例如名稱叫菜單測試),並且能夠跳轉,權限也加好了。

  

  視圖和數據文件也是用Node創建的,在Node項目中寫好一份模板字符串(下面是生成視圖模板的函數),將可變部分作爲參數傳入。

export function setPageTemplate({name, antd, namespace, code='', props, component}) {
  return `import { connect, Dispatch, ${namespace}ModelState } from "umi";
import { setColumn } from '@/utils/tools';
import { TEMPLATE_MODEL } from '@/utils/constants';
${antd}
// 頁面參數類型
interface ${name}Props {
  dispatch: Dispatch;
  state: ${namespace}ModelState;
}
// 全局聲明
${code}
const ${name} = ({ dispatch, state }: ${name}Props) => {
  // dispatch({ type: "xx/xx", payload: {} });
  // 狀態
  // const { } = state;
  // 通用組件配置
  ${props}
  return <>
    ${component}
  </>;
};
export default connect((data: {${namespace}: ${namespace}ModelState}) => ({ state: data.${namespace} }))(${name});`;
}

二、配置

  配置是本系統的核心,構思了很久,原先考慮了系統的靈活性,就想直接提供腳本編輯框,自定義邏輯。

  不過出現個問題,那就是我這邊目前是用TypeScript語言開發的,那麼我在自定義腳本邏輯時,也需要使用TypeScript語法。

  瀏覽器提供的 eval() 函數並不支持TypeScript語法,需要先做轉譯,網上搜索後,得到了解決方案,下載了TypeScript庫後。

  但是卻一直報錯,在網上也查到了些解決方案(方案一方案二),不過並不適用於我目前的項目環境。

./node_modules/typescript/lib/typescript.js
Critical dependency: the request of a dependency is an expression

./node_modules/typescript/lib/typescript.js
Critical dependency: the request of a dependency is an expression

./node_modules/typescript/lib/typescript.js
Module not found: Can't resolve 'perf_hooks' in 'C:\Users\User\node_modules\typescript\lib'

  最終決定暫時放棄自定義腳本邏輯,先解決當前痛點,儘快將系統上線。

  期間還遇到個比較隱蔽的bug,如下所示,數組會先調用 toString() 轉換成字符串,最終變爲 eval("(1, 2)"),所以得到的值是 2。

eval(`(${[1,2]})`);  //2

  還遇到個問題,那就是在用 JSON.stringify() 序列化對象時,若參數是函數,那麼就會被過濾掉。

JSON.stringify({func:() => {}});  //"{}"

1)物料庫

  物料庫中的組件分爲兩種,一種是自定義的後臺模板組件,另一種是第三方的Ant Design 3.X組件。

  爲了快速搭建頁面,選擇的組件是前者。這次順便用TypeScript,再次完善了組件代碼的類型聲明。

  

  後者只是用來文檔查詢和在模板字符串中拼接引入語句,如下所示。

`import { ${antds.join(',')} } from 'antd';`

2)自定義組件

  自定義組件的聲明採用JSON格式,TypeScript聲明的類型如下所示。

interface OptionsType {
  value: string;
  label: string;
  children: Array<{
    value: string;
    label: string;
    link: string;   //鏈接地址
    readonlyProps?: ObjectType;  //會影響組件的呈現,並且不能配置的屬性
    readonlyStrProps?: string;   //待拼接的字符串屬性
    handleProps?: (values:ObjectType) => ObjectType;    //在格式化表單數據後,再處理特定的組件屬性
    handleStrProps?: (values:ObjectType) => string; //拼接無法轉換成字符串的屬性
    props: Array<{
      label: string;
      name: string;
      params?: ObjectType;
      control: JSX.Element | ((index: number) => JSX.Element);
      type?: string;
      initControl?: (props:any) => JSX.Element;
    }>
  }>;
}

  鏈接地址就是說明文檔的地址,在組件的屬性中,有一部分是回調函數,而目前已經捨棄了自定義的回調邏輯。

  所以這部分屬性要特殊處理(聲明在 readonlyProps),不能在界面中輸入。

        readonlyProps: {
          initPanes: (record: ObjectType): TabPaneType[] => [
            {
              name: "示例",
              key: "demo",
              controls: [
                { label: '測試組件', control: <>內容</> }
              ]
            },
          ],
        },

  readonlyStrProps 就是 readonlyProps 對應的字符串格式,該屬性還會增加一些其它屬性,配上註釋,也相當於是份組件文檔了。

        readonlyStrProps: `,
        // 標籤欄內容回調函數,參數爲 record,當標籤欄只有一項時,將不顯示菜單
        "initPanes": (record: ObjectType): TabPaneType[] => [
          {
            name: "示例",
            key: "demo",
            controls: [
              { label: '測試組件', control: <>內容</> }
            ]
          },
        ],
        // useEffect鉤子中的回調函數,參數是 record
        "effectCallback": (record: ObjectType) => {}`,

  handleProps() 是一個回調函數,在表單接收到數據後,有些組件需要再做一次特殊的處理。

  例如加些特定屬性、數組元素合併成字符串等,從而才能順利的在預覽界面呈現。

        handleProps: (values:ObjectType) => { //對錶單中的值做處理
          // 對接口數組做特殊處理,從['api', 'get']轉換成api.get
          values.url && (values.url = values.url.join('.'));// 初始化表單需要的組件
          if(values.controls.length === 0) {
            values.controls = [
              {
                label: "示例",
                name: "demo",
                control: <>測試組件</>
              },
            ];
          }else {
            values.originControls = values.controls;    //備份組件名稱數組
            values.controls = values.controls.map((item:string) => getControls(item));
          }
          delete values.controlskeys; //刪除冗餘屬性
          return values;
        },

  handleStrProps() 是在輸出模板時使用,將那些特殊屬性寫成字符串形式。

        handleStrProps: (values:ObjectType):string => {
          if(values.controls.length === 0) {
            delete values.originControls; //刪除備份數組
            delete values.controls;   //刪除原始屬性
            return `,"controls": [
              {
                label: "示例",
                name: "demo",
                control: <>測試組件</>
              },
            ]`;
          }
          // 組件名稱數組處理
          const newControls = values.originControls.map((item:string) => getStrControls(item));
          delete values.originControls;
          delete values.controls;
          return `,"controls": [${newControls.join(',')}]`;
        },

  在經過一系列的處理後,將一些字符串代碼傳遞給接口,接口最後拼接成兩個文件,輸出到指定目錄中。

  不過生成的代碼,排版有點混亂,每次都還需要手動格式化一下。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章