大綱
遇到的問題場景及解決方案對比
我們目前採用的是antd + react(umi)的框架做業務開發。在業務開發過程中會有較多頻繁出現並且相似度很高的場景,比如基於一個table的基礎的增刪改查,這個相信大家都非常熟悉。在接到一個新的業務需求的時候,相信有不少人會選擇copy一份功能類似的程式碼然後基於這份程式碼去改造以滿足當前業務,當然我目前也是這樣做的~
其實想把這塊功能提取成一個公共組建的想法由來已久,最近開始做基礎元件,便拿這個下手了。經過一週左右的時間完成了基礎元件的編寫。
檢視基礎支援的功能點API。
基本的思路是通過json生成一些抽象配置,然後通過解析json的抽象配置+渲染器最終生成頁面。json配置涵蓋了目前80%的業務場景的基本需求,但是可擴充套件性很低。比如一些複雜的業務場景:表單的關聯校驗
、資料關聯顯示
、多級列表下鑽
等等功能。雖然通過一些較為複雜的處理可以把這些功能融入進來,但最終元件將會異常龐大難以維護。
所以,我能不能通過這些json配置通過某種工具生成對應的程式碼?這樣一來以上提到的問題就完全不存在了,因為這和我們自己寫的程式碼完全一樣,工具只是幫我們完成初始化的過程。所以後來想了很多辦法,最初採用template string
的方式,這種方式較為簡單粗暴,無非通過string中巢狀變數的判斷來輸出code。但是在實際寫的時候發現很多問題,比如
function
的輸出(JSON.stringify會將function忽略)- 多層函式巢狀之後怎麼獲取最終渲染的節點code
- 嵌入變數怎麼實現、umi-models-effects/reducer中額外的字典查詢怎麼生成等等..
最終學習了一些生成程式碼的工具比如angular-cli以及一些關於js生成程式碼的文章,主要是通過知乎上的這篇討論瞭解到了大家是怎麼處理這種問題的。最終決定採用babel的生態鏈來解決上述遇到的問題。
我們目前採用的方式是基於antd+react(umi)編寫通用的CRUD模板,然後通過程式碼生成器解析json中的配置生成對應的程式碼,大致的流程是:
React --> JavaScript AST ---> Code Generator --> Compiler --> Page
目前功能只是完成了初步版本,待應用在專案中使用一段時間穩定之後將會開源~
什麼是babel?
Babel是一個工具鏈,主要用於編譯ECMAScript 2015+程式碼轉換為向後相容的可執行在各種瀏覽器上的JavaScript。主要功能:
- 語法轉換
- 環境中缺少的Polyfill功能
- 原始碼轉換
- 檢視更多Babel功能
如上提供了babel基本的流程及一篇介紹AST的文章。
我的理解中比如一段string型別code,首先通過babel.transform會將code轉為一個包含AST(Abstract Syntax Tree)的Object,同樣可以使用@babel/generator將AST轉為code完成逆向過程。 例如一段變數宣告程式碼:
const a = 1;
複製程式碼
在解析之後的結構為:
{
"type": "Program",
"start": 0,
"end": 191,
"body": [
{
"type": "VariableDeclaration",
"start": 179,
"end": 191,
"declarations": [
{
"type": "VariableDeclarator",
"start": 185,
"end": 190,
"id": {
"type": "Identifier",
"start": 185,
"end": 186,
"name": "a"
},
"init": {
"type": "Literal",
"start": 189,
"end": 190,
"value": 1,
"raw": "1"
}
}
],
"kind": "const"
}
],
"sourceType": "module"
}
複製程式碼
首先型別為VariableDeclaration
,首先他的型別是const
,可以通過點選檢視api其它還有let
、var
的值。其次是宣告declarations
部分,這裡值為陣列,因為我們可以同時定義多個變數。陣列中值的型別為VariableDeclarator
,包含id
和init
兩個引數,分別為變數名稱以及變數值。id
的型別為Identifier
,譯為修飾符即是變數名稱。init
型別為Literal
,即是常量,一般常用的有stringLiteral
、numericliteral
、booleanliteral
等。此時即完成了變數賦值的過程。
當然這只是很簡單的語法轉換,如果大家想學習更多關於轉換及型別的知識,可參考如下兩個官方連結:
解決過程
首先定義目錄結構:
.
├── genCode // 程式碼生成器
| ├── genDetail // 需要新頁面開啟時單獨的detail目錄
| └── genIndex // 首頁
| └── genModels // umi models
| └── genServices // umi services
| └── genTableFilter // table篩選區域
| └── genTableForm // 非新頁面模式,新增/更新模態框
| └── genUpsert // 新頁面模式下,新增/更新頁面
| └── genUtils // 生成工具類
├── schema // 模型定義檔案
| ├── table // 當前要生成的模型
| └── ├──config.js // 基礎配置
| └── └──dataSchema.js // 列表、新增、更新配置
| └── └──querySchema.js // 篩選項配置
├── scripts // 生成指令碼
| ├── generateCode.js // 生成主檔案
| └── index.js // 入口
| └── utils.js // 工具類
├── toCopyFiles // 生成時需要拷貝的檔案,比如less
└── index.js // 主入口
複製程式碼
主體流程為:
- 指定要生成程式碼的路徑。
- 根據schema中當前json配置路徑,依次呼叫genCode目錄中各個模組的程式碼生成方法獲取對應code。
- 在指定的路徑下寫入對應的檔案。
- 執行
eslint ${filePath} --fix
格式化生成的程式碼。 - 根據配置對應複製toCopyFiles資料夾中依賴的less等檔案到對應的資料夾。
其中主要模組為genCode資料夾中根據json配置生成程式碼的過程。
以genModels為例,首先提取可以使用template string
完成的部分,減少程式碼解析的工作量。
module.exports = (tableConfig) => {
return `
import { message } from 'antd';
import { routerRedux } from 'dva/router'
import { parse } from 'qs'
${dynamicImport(dicArray, namespace)}
export default {
namespace: '${namespace}',
state: {
...
},
effects: {
*fetch({ payload }, { call, put }) {
const response = yield call(queryData, payload);
if (response && response.errorCode === 0) {
yield put({
type: 'save',
payload: response.data,
});
} else {
message.error(response && response.errorMessage || '請求失敗')
}
},
...,
${dynamicYieldFunction(dicArray)}
},
reducers: {
save(state, action) {
return {
...state,
data: action.payload,
};
},
...,
${dynamicReducerFunction(dicArray)}
},
};
`
}
複製程式碼
因為列表資料可能有字典項從後臺獲取值來對應顯示,所以import
、effects
、reducers
模組均有需根據配置動態生成的程式碼。
以dynamecImport為例:
function dynamicImport (dicArray, namespace) {
// 基礎api import
let baseImport = [
'queryData', 'removeData', 'addData', 'updateData', 'findById'
]
// 判斷json資料中是否有需從後臺載入項
if (dicArray && dicArray.length) {
baseImport = baseImport.concat(dicArray.map(key => getInjectVariableKey(key)))
}
// 遍歷生成依賴項
const _importDeclarationArray = map(specifier => (
_importDeclarationArray.push(t.importSpecifier(t.identifier(specifier), t.identifier(specifier)))
))
// 定義importDeclaration
const ast = t.importDeclaration(
_importDeclarationArray,
t.stringLiteral(`../services/${namespace}`)
)
// 通過@babel/generator 將ast生成code
const { code } = generate(ast)
return code
}
複製程式碼
其它程式碼生成邏輯類似,有不確定如何生成的部分可參考上方提供的連結完成程式碼轉換再去生成。
若有通過babel轉換無法生成的程式碼,可通過正則來完成。
例如以下umi-models
程式碼:
*__dicData({ payload }, { call, put }) {
const response = yield call(__dicData, payload);
if (response && response.errorCode === 0) {
yield put({
type: 'updateDic',
payload: response.data,
});
} else {
message.error(response && response.errorMessage || '請求失敗')
}
}
複製程式碼
基礎程式碼可通過yieldExpression
生成,但是轉換之後無function之後的*
符號,反覆查了文件之後沒有解決辦法,最後只能將生成完的code利用正則替換來解決。
如果大家有遇到類似的問題歡迎討論~
問題
- 目前使用的編輯器元件為braft-editor,但是結合antd使用initialValue不生效,必須使用setFieldsValue。但是使用useEffects時會預設新增props.form作為依賴並且props.form會不斷變化而觸發死迴圈,目前無奈只有禁用eslint react-hooks/exhaustive-deps。
useEffect(() => {
props.form.setFieldsValue({
editorArea: BraftEditor.createEditorState(current.editorArea),
editorArea2: BraftEditor.createEditorState(current.editorArea2)
});
}, [current.editorArea, current.editorArea2]);
複製程式碼
- 生成的程式碼怎麼刪除未使用的依賴?使用eslint --fix不會刪除未使用的變數定義。
- 初始化之後的程式碼要修改怎麼辦?因當前方法只會完成程式碼初始化過程,以後修改的過程暫無思路解決。
功能API
引數規範參考react-antd-admin 功能配置包含三個基礎配置檔案:
config.json
配置基本屬性dataSchema.json
配置列表及新增修改欄位querySchema.json
配置篩選區域欄位
config.json
配置列表
引數 | 必填 | 型別 | 預設值 | 說明 |
---|---|---|---|---|
namespace | true | string | null | 名稱空間 |
showExport | false | boolean | true | 是否顯示匯出 |
showCreate | false | boolean | true | 是否顯示建立 |
showDetail | false | boolean | true | 是否顯示檢視 |
showUpdate | false | boolean | true | 是否顯示修改 |
showDelete | false | boolean | true | 是否顯示刪除 |
newRouterMode | false | boolean | false | 在新的頁面新增/編輯/檢視詳情。若包含富文字編輯器,建議此值設為true,富文字在模態框展示不是非常美觀。 |
showBatchDelete | false | boolean | true | 是否顯示批量刪除,需multiSelection為 true |
multiSelection | false | boolean | true | 是否支援多選 |
defaultDateFormat | false | string | 'YYYY-MM-DD' | 日期格式 |
upload | false | object | null | 上傳相關配置,上傳圖片和上傳普通檔案分別配置。 詳見下方upload屬性 |
pagination | false | object | null | 分頁相關配置, 詳見下方pagination屬性 |
dictionary | false | array | null | 需要請求的字典項,用於下拉框或treeSelect的值為從後端獲取的情況,可在dataSchema 和querySchema中使用, 詳見下方dictionary屬性 |
upload
引數 | 必填 | 型別 | 預設值 | 說明 |
---|---|---|---|---|
uploadUrl | false | string | null | 預設的上傳介面.優先順序image/fileApiUrl > uploadUrl > Global.apiPath |
imageApiUrl | false | string | null | 預設的圖片上傳介面 |
fileApiUrl | false | string | null | 預設的檔案上傳介面 |
image | false | string | '/uploadImage' | 預設的上傳圖片介面 |
imageSizeLimit | false | number | 1500 | 預設的圖片大小限制, 單位KB |
file | false | string | '/uploadFile' | 預設的上傳檔案介面 |
fileSizeLimit | false | number | 10240 | 預設的檔案大小限制, 單位KB |
pagination
引數 | 必填 | 型別 | 預設值 | 說明 |
---|---|---|---|---|
pageSize | false | number | 10 | 每頁顯示數量 |
showSizeChanger | false | boolean | false | 是否可以改變pageSize |
pageSizeOptions | false | array | ['10', '20', '50', '100'] | 指定每頁可以顯示多少條 |
showQuickJumper | false | boolean | false | 是否可以快速跳轉至某頁 |
showTotal | false | boolean | true | 是否顯示總數 |
dictionary
引數 | 必填 | 型別 | 預設值 | 說明 |
---|---|---|---|---|
key | true | string | null | 變數標識 |
url | true | string | null | 請求資料地址 |
dataSchema.json
配置列表
引數 | 必填 | 型別 | 預設值 | 說明 |
---|---|---|---|---|
key | true | string | null | 唯一識別符號 |
title | true | string | null | 顯示名稱 |
primary | false | boolean | false | 主鍵 如果不指定主鍵, 不能update/delete, 但可以insert; 如果指定了主鍵, insert/update時不能填寫主鍵的值; |
showType | false | string | input | 顯示型別 input/textarea/inputNumber/datePicker/rangePicker/radio/select/checkbox/multiSelect/image/file/cascader/editor |
disabled | false | boolean | false | 表單中這一列是否禁止編輯 |
addonBefore | false | string/ReactNode | null | showType 為input可以設定前標籤 |
addonAfter | false | string/ReactNode | null | showType 為input可以設定後標籤 |
placeholder | false | string | null | 預設提示文字 |
format | false | string | null | 日期型別的格式 |
showInTable | false | boolean | true | 這一列是否要在table中展示 |
showInForm | false | boolean | true | 是否在新增或編輯的表單中顯示 |
validator | false | boolean | null | 設定校驗規則, 參考https://github.com/yiminghe/async-validator#rules |
width | false | string/number | null | 列寬度 |
options | false | array | null | format:[{ key: '', value: '' }]或string。showType為cascader時,此欄位暫不支援Array,資料只能通過非同步獲取。 |
min | false | number | null | 數字輸入的最小值 |
max | false | number | null | 數字輸入的最大值 |
accept | false | string | null | 上傳檔案格式限制 |
sizeLimit | false | number | 20480 | 上傳檔案格式限制 |
url | false | string | null | 上傳圖片url。圖片的上傳介面, 可以針對每個上傳元件單獨配置, 如果不單獨配置就使用config.js中的預設值;如果這個url是http開頭的, 就直接使用這個介面; 否則會根據config.js中的配置判斷是否加上host |
sorter | false | boolean | false | 是否排序 |
actions | false | array | null | 操作 |
actions
引數 | 必填 | 型別 | 預設值 | 說明 |
---|---|---|---|---|
keys | false | array | null | 允許更新哪些欄位, 如果不設定keys, 就允許更所有欄位 |
name | true | string | null | 展示標題 |
type | false | string | null | update/delete/newLine/component |
querySchema.json
配置列表
引數 | 必填 | 型別 | 預設值 | 說明 |
---|---|---|---|---|
key | true | string | null | 唯一識別符號 |
title | true | string | null | 顯示名稱 |
placeholder | false | string | null | 提示語 |
showType | false | string | input | 顯示型別, 一些可列舉的欄位, 比如type, 可以被顯示為單選框或下拉框 input, 就是一個普通的輸入框, 這時可以省略showType欄位 目前可用的showType: input/inputNumber/datePicker/rangePicker/select/radio/checkbox/multiSelect/cascader |
addonBefore | false | string/ReactNode | null | showType 為input可以設定前標籤 |
addonAfter | false | string/ReactNode | null | showType 為input可以設定後標籤 |
defaultValue | false | string/array/number | null | 多選的defaultValue是個陣列 |
min | false | number | null | showType為 inputNumber 時可設定最小值 |
max | false | number | null | showType為 inputNumber 時可設定最大值 |
options | false | array | null | options的key要求必須是string, 否則會有warning normal-format: [{"key": "", "value": ""}] cascader-format: [{"value": "", "label": "", children: ["value": "", "label": "", children: []]}] 如果值為string,代表非同步獲取的資料,則獲取當前名稱空間下該key對應的值 |
defaultValueBegin | false | string | null | showType為 rangePicker 時可設定預設開始值 |
defaultValueEnd | false | string | null | showType為 rangePicker 時可設定預設結束值 |
placeholderBegin | false | string | 開始日期 | showType為 rangePicker 時可設定預設開始提示語 |
placeholderEnd | false | string | 結束日期 | showType為 rangePicker 時可設定預設結束提示語 |
format | false | string | null | 日期篩選格式 |
showInSimpleMode | false | boolean | false | 在簡單查詢方式下展示,若資料中有一項包含此欄位且為true的值,則開啟簡單/複雜篩選切換 |