阿里低代码框架 lowcode-engine 低代码表单实战(阿里低代码平台)
前沿
lowcode-engine功能比较强大,最近这段时间做了个低代码表单的实战,在过程中遇到一些问题,在这里做下介绍和总结。
功能演示
前台功能
主要介绍一下前台功能的基本实现和一些问题。
FormContainer容器组件
我们的默认容器不是页面,而是需要自定义容器。例如,在常见的低代码平台中默认容器是表单容器,通过表单容器类提供布局能力。这块之前有一篇文章详情介绍,可以查看FormContainer容器。
那篇文章介绍了怎么实现自定义容器,我们打开详情页面,看到所有的表单项都是只读的,我们在容器中做一个全局状态管理,在这里用context去实现。
- 定义 Provider
// 定义FormContainerProviderexport const FormContainerProvider: FC<IFormContainerProviderProps> = ({ children, isMobile }) => { const processorAction = useCreation(() => { return createFormContainerProcessor(); }, []); const { processor, getRoot, destroy } = processorAction || {}; useEffect(() => { processor.setMobile(isMobile); }, [isMobile]); useEffect(() => { return () => { destroy?.(); }; }, []); return <Context.Provider value={processor!}>{children}</Context.Provider>;};
- 之后我们就可以在容器组件和FormItem组件内获取数据,这块简单做了封装处理。
// 从conext获取更改只读的方法const [changeReadonly] = useFormContainerSelector((s) => [s.changeReadonly]);
- Form容器对外提供能力
我们提交保存操作没有在容器内实现对应的物料,是在外部自定义的,这时候就需要我们对FormContainer绑定Ref,之后我们获取实例可以拿到对应的方法。
// 绑定refReact.useImperativeHandle( ref, () => { return { formRef: form, changeReadonly, // 更改只读方法 }; }, []);
物料组件
我们对每个表单项开发对应的物料,物料的开发,官方提供脚手架快速创建项目,之前也写过一遍文章,流程不清楚的请移步自定义物料篇。这里我们用日期物料做说明,还会介绍一下开发调试,之前文章说我们要把物料发布到npm上,这样开发调试很不方便。
Filed Date 物料
- 定义Date物料类型
可以看到我们有个基础的类型,是一些通用的属性,columnConfig这个属性是每个FormItem的config。
export interface IColumnEntity<T extends EFieldType = EFieldType> extends IBaseEntity { ... // 数据库字段类型 fieldType: TFieldType; // 标题 title?: string; // 扩展参数 extraParam?: Record<string, any>; // 列配置信息 columnConfig: T extends keyof TColumnConfigMap ? TColumnConfigMap[T] : TColumnConfig; // 校验信息 validateConfig: IColumnValidateConfig;}
- FieldData config
日期物料的config信息,有了具体的TS类型,在我们写代码的时候会事半功倍
/** * 日期 */export interface IColumnDateConfig { /** * 描述 */ description: string; /** * 占位符 */ placeholder?: string; /** * 1. 普通 2禁用 3 只读 */ status: number; /** * 格式化类型 1. YY-MM 2. YYYY-MM-DD 3. YYYY-MM-DD HH:MM 4. YYYY-MM-DD HH:MM:SS */ format: number; /** * 默认值类型 */ defaultValueType: string; /** * 默认值 */ defaultValue: string;}
- meta.ts信息
这里主要描述物料组件信息, 我们简单介绍一下setter信息,其它的可以看官方文档。
configure: { props: [ { title: { label: '格式', }, name: 'columnConfig.format', supportVariable: false, setter: { componentName: SelectSetter, props: { options: DateFormatConstant, changeReRenderEvent: true, }, initialValue: 2, }, }, ]}
props中的name属性columnConfig.format,我们可以使用这种方式来描述嵌套的属性。
- 实现FieldData组件
这里相对来说也不复杂,需要注意的是porps中的内容,有我们在meta文件中定义的props,还有FormItem中标注的value,onChange属性,还有一些属性,大家可以打印下看看。有时候有些需求实现这上面的属性会有帮助,
// FieldData 具体实现export interface IFieldDateProps extends BaseWrapperProps<EFieldType.DATE> {}export const FieldDate: FC<IFieldDateProps> = (props) => { const { columnConfig, onChange, value, ...otherProps } = props; const [readonly] = useFormContainerSelector((s) => [s.readonly]); const format = columnConfig?.format; const currFormat = DateFormatConstant.find((f) => f.value == format); const onDateChange: DatePickerProps['onChange'] = (date, dateString) => { const currUnix = date?.valueOf(); onChange?.(currUnix); }; return ( <BaseWrapper {...props}> <DatePicker style={{ width: '100%' }} disabled={readonly || columnConfig?.status === EFieldStatus.disable} placeholder={columnConfig?.placeholder} showTime={currFormat?.showTime} format={currFormat?.label || 'YYYY-MM-DD'} value={value ? dayjs(value) : undefined} onChange={onDateChange} /> </BaseWrapper> );};
setter
实现我们的需求,setter是一个比较重要的环节,这里我们对setter做了重写,全部使用了antd的组件。setter我们分为通用的setter和单个物料的自己的setter。
- setter定义
官方的案例Setter使用的是字符串,也就是在引擎注入的setter供我们使用。在项目中开发,我们可以用一个setter组件,待setter稳定后,考虑引擎注入。
- 每个setter对应一个props属性
上面我们在meta文件中的columnConfig.format使用了SelectSetter,定义如下:
export const SelectSetterFun: FC<ISelectSetterProps> = (props) => { const { options = [{ label: '-', value: '' }], onChange, mode, value, showSearch, onChangeEvent, changeReRenderEvent, } = props; const dataSource = formateOptions(options); const { sendReRenderEvent } = useReRenderEvent({ isBindEvent: false }); return ( <Select style={{ width: '100%' }} value={value} size={'small'} options={dataSource} onChange={(val) => { onChange?.(val); onChangeEvent?.(val); changeReRenderEvent && sendReRenderEvent(); }} showSearch={showSearch} /> );};export const SelectSetter = SetterHoc(SelectSetterFun);
- 高阶组件 SetterHoc 在setter中直接使用hooks组件会有问题,我们用类组件做一层包裹。
export const SetterHoc = (Component: any) => { return class SetterComponent extends React.Component { render() { return <Component {...this.props} />; } };};
- 获取和设置其它props值
有的需求我们在setter中需要获取其它组件的属性,通过props?.field?.parent 可以获取到,这里封装了一个自定的hooks,来获取和设置值
export const usePropsValue = (props: any) => { const getPropValue = useMemoizedFn((key: string) => { const propsField = props?.field?.parent; // 获取同级其他属性 showJump 的值 return propsField.getPropValue(key); }); const setPropsValue = useMemoizedFn((key: string, value: any) => { const propsField = props?.field?.parent; // 获取同级其他属性 showJump 的值 propsField.setPropValue(key, value); }); return { getPropValue, setPropsValue, };};
还有一种方法可以可以实现此效果,就是在setter上设置extraProps属性,这个属性可以有两个方法setValue和getValue.
- 在meta上设置
// 更改其它选项,在meta上设置extraProps: OptionsSetterExtraProps,
// 更改其它选项,在meta上设置extraProps: OptionsSetterExtraProps,
- setter之间通信
在引擎中,通信需要通过事件的方式去做。在这里,通常我们有些setter的变更会影响其它setter,例如:日期的格式变化默认值会做相应的调整。在业务中,setter的变更,通知依赖的setter刷新,刷新的时候重新获取属性值,做业务调整。
在这里,封装了reRender一个hooks,
export const useReRenderEvent = (props?: IUseReRenderEventProps) => { const { isBindEvent = true } = props || {}; const update = useUpdate(); // 强制触发更新 const reRenderEvent = useMemoizedFn(() => { update(); }); /** * 发送重新渲染事件 */ const sendReRenderEvent = useMemoizedFn(() => { event.emit(EFiledEventName.ReRenderEmit); }); useEffect(() => { isBindEvent && event.on(EFiledEventName.ReRender, reRenderEvent); return () => { isBindEvent && event.off(EFiledEventName.ReRender, reRenderEvent); }; }, [isBindEvent]); return { sendReRenderEvent, };};
这个hooks,有两个作用,一个是发送重新渲染事件,一个是监听渲染事件。在上面的案例当中,
1.在格式的setter中引入该hooks,做事件发送。
const { sendReRenderEvent } = useReRenderEvent({ isBindEvent: false });return ( <Select ... onChange={(val) => { changeReRenderEvent && sendReRenderEvent(); }} ... />);
- 在默认值setter中,做事件的监听。
// 监听格式的变化useReRenderEvent();// 获取格式数据const { getPropValue } = usePropsValue(otherProps);const format = getPropValue('format');
渲染详情页
封装FormContainerRnder组件,来做渲染。
- 引擎提供了ReactRender的能力,我们传入对应的scheam信息,就可以做到显示。
<ReactRenderer className="lowcode-plugin-sample-preview-content" schema={schema} designMode="dialog" rendererName="LowCodeRenderer" components={components} onCompGetRef={onCompGetRef} appHelper={{ requestHandlersMap: { Fetch: createFetchHandler(), }, }}/>
- 获取FormContainer组件Ref
在数据提交的时候,我们需要获取组件的实力,在引擎中获取Ref方法,要使用 onCompGetRef方法。
const onCompGetRef = (schema: any, ref: any) => { if ('FormContainer' === schema.componentName) { const { formRef, ...otherRef } = ref; formInstanceRef.current = ref.formRef; formOtherRef.current = otherRef; // 获取到ref,执行resolve promiseRef?.resolve(true); }};
在渲染的时候,我们有可能获取不到实例,我们用个异步来处理。
// 此处异步是因为不能立马获取到form的实例const promiseRef = useCreation(() => { return createPromiseWrapper();}, []);
提供对外的数据能力
React.useImperativeHandle( ref, () => { return { getFormInstance: async () => { await promiseRef.promise; return formInstanceRef.current! as FormInstance; }, changeReadonly: async (disabled: boolean) => { await promiseRef.promise; formOtherRef.current?.changeReadonly?.(disabled); }, }; }, []);
- 初始化数据
打开编辑详情页的时候,需要把从接口获取的数据给设置到表单上。有了FormContainer实例,我们可以很方便的做设置
useAsyncEffect(async () => { if (mode !== EMode.create && !itemData.loading && Object.keys(itemData.data).length > 0) { // 获取实例 const formInstance = await formRef.current?.getFormInstance(); // 数据转换 const formValues = convertItemDataToFormValues(itemData.data, table.data.columns); // 设置值 formInstance?.setFieldsValue?.(formValues); }}, [itemData.data, itemData.loading]);
提交数据
获取表单数据,做提交。这里通过FormContainer的时候,可以获取所有的值,包括做一些前端的校验等。
- 获取所有值,调用api,做数据提交
export const getFormValues = async (formRef: React.MutableRefObject<IPreviewRef>) => { const formInstance = await formRef?.current?.getFormInstance(); return formInstance?.getFieldsValue();};
开发调试
开发物料后,如果我们发布npm,整个流程会很繁琐,效率低,物料脚手架也提供了调试,不过在我们实际业务开发中,会有一些业务数据和上下文的环节依赖,所有要能实时调试开发。接下来几个步骤介绍一下
- 启动lowcode开发模式
"lowcode:dev": "build-scripts start --config ./build.lowcode.js",
会开启一个实时的监听服务。
- 在我们引擎中的assets.json修改,使用上面服务的地址,修改内容如下
修改在url中的内容为本地地址,这时候我们开发后。刷新浏览器,会实时看到结果
- 做环境变量,动态切换
import assetsLocal from '../services/assets-local.json';import assets from '../services/assets.json';export const getAssetsJson = () => { // 用本地配置文件 if (process.env.LOCAL_UI_MATERIAL === 'true') { return assetsLocal; } return assets;};
总结
以上就是对lowcode-engine低代码实战内容,后续我们介绍一下引擎和后台之间的交互,可以让大家实现一个完整的案例。
作者:Witty_Wizard
链接:https://juejin.cn/post/7346865556328808463