前沿
lowcode-engine功能比较强大,最近这段时间做了个低代码表单的实战,在过程中遇到一些问题,在这里做下介绍和总结。
功能演示
前台功能
主要介绍一下前台功能的基本实现和一些问题。
FormContainer容器组件
我们的默认容器不是页面,而是需要自定义容器。例如,在常见的低代码平台中默认容器是表单容器,通过表单容器类提供布局能力。这块之前有一篇文章详情介绍,可以查看FormContainer容器。
那篇文章介绍了怎么实现自定义容器,我们打开详情页面,看到所有的表单项都是只读的,我们在容器中做一个全局状态管理,在这里用context去实现。
- 定义 Provider
// 定义FormContainerProvider
export 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,之后我们获取实例可以拿到对应的方法。
// 绑定ref
React.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