大纲
- 遇到的问题场景及解决方案对比
- 什么是 babel?
- 解决过程
- 目前遗留的问题
- 目前实现功能 API
- 参考
遇到的问题场景及解决方案对比
我们目前采用的是 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 功能
Understanding ASTs by Building Your Own Babel Plugin
如上提供了 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
等。此时即完成了变量赋值的过程。
当然这只是很简单的语法转换,如果大家想学习更多关于转换及类型的知识,可参考如下两个官方链接:
- babel-types
- ast 转换工具
解决过程
首先定义目录结构:
.
├── 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/a… |
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 的值,则开启简单 / 复杂筛选切换 |
参考
- react-antd-admin
- AST 语法转换器
- babel-types-api