我們組維護的管理後臺會接到很多開發需求,每次新開頁面,就會到處複製黏貼相關代碼。
並且還會經常性的翻閱文檔,先在書籤或地址欄輸入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(',')}]`; },
在經過一系列的處理後,將一些字符串代碼傳遞給接口,接口最後拼接成兩個文件,輸出到指定目錄中。
不過生成的代碼,排版有點混亂,每次都還需要手動格式化一下。