前沿
lowcode-engine功能比較強(qiáng)大,最近這段時(shí)間做了個(gè)低代碼表單的實(shí)戰(zhàn),在過程中遇到一些問題,在這里做下介紹和總結(jié)。
功能演示
前臺(tái)功能
主要介紹一下前臺(tái)功能的基本實(shí)現(xiàn)和一些問題。
FormContainer容器組件
我們的默認(rèn)容器不是頁面,而是需要自定義容器。例如,在常見的低代碼平臺(tái)中默認(rèn)容器是表單容器,通過表單容器類提供布局能力。這塊之前有一篇文章詳情介紹,可以查看FormContainer容器。
那篇文章介紹了怎么實(shí)現(xiàn)自定義容器,我們打開詳情頁面,看到所有的表單項(xiàng)都是只讀的,我們在容器中做一個(gè)全局狀態(tài)管理,在這里用context去實(shí)現(xiàn)。
- 定義 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組件內(nèi)獲取數(shù)據(jù),這塊簡單做了封裝處理。
// 從conext獲取更改只讀的方法const [changeReadonly] = useFormContainerSelector((s) => [s.changeReadonly]);
- Form容器對(duì)外提供能力
我們提交保存操作沒有在容器內(nèi)實(shí)現(xiàn)對(duì)應(yīng)的物料,是在外部自定義的,這時(shí)候就需要我們對(duì)FormContainer綁定Ref,之后我們獲取實(shí)例可以拿到對(duì)應(yīng)的方法。
// 綁定refReact.useImperativeHandle( ref, () => { return { formRef: form, changeReadonly, // 更改只讀方法 }; }, []);
物料組件
我們對(duì)每個(gè)表單項(xiàng)開發(fā)對(duì)應(yīng)的物料,物料的開發(fā),官方提供腳手架快速創(chuàng)建項(xiàng)目,之前也寫過一遍文章,流程不清楚的請(qǐng)移步自定義物料篇。這里我們用日期物料做說明,還會(huì)介紹一下開發(fā)調(diào)試,之前文章說我們要把物料發(fā)布到npm上,這樣開發(fā)調(diào)試很不方便。
Filed Date 物料
- 定義Date物料類型
可以看到我們有個(gè)基礎(chǔ)的類型,是一些通用的屬性,columnConfig這個(gè)屬性是每個(gè)FormItem的config。
export interface IColumnEntity<T extends EFieldType = EFieldType> extends IBaseEntity { ... // 數(shù)據(jù)庫字段類型 fieldType: TFieldType; // 標(biāo)題 title?: string; // 擴(kuò)展參數(shù) extraParam?: Record<string, any>; // 列配置信息 columnConfig: T extends keyof TColumnConfigMap ? TColumnConfigMap[T] : TColumnConfig; // 校驗(yàn)信息 validateConfig: IColumnValidateConfig;}
- FieldData config
日期物料的config信息,有了具體的TS類型,在我們寫代碼的時(shí)候會(huì)事半功倍
/** * 日期 */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; /** * 默認(rèn)值類型 */ defaultValueType: string; /** * 默認(rèn)值 */ 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,我們可以使用這種方式來描述嵌套的屬性。
- 實(shí)現(xiàn)FieldData組件
這里相對(duì)來說也不復(fù)雜,需要注意的是porps中的內(nèi)容,有我們在meta文件中定義的props,還有FormItem中標(biāo)注的value,onChange屬性,還有一些屬性,大家可以打印下看看。有時(shí)候有些需求實(shí)現(xiàn)這上面的屬性會(huì)有幫助,
// FieldData 具體實(shí)現(xiàn)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
實(shí)現(xiàn)我們的需求,setter是一個(gè)比較重要的環(huán)節(jié),這里我們對(duì)setter做了重寫,全部使用了antd的組件。setter我們分為通用的setter和單個(gè)物料的自己的setter。
- setter定義
官方的案例Setter使用的是字符串,也就是在引擎注入的setter供我們使用。在項(xiàng)目中開發(fā),我們可以用一個(gè)setter組件,待setter穩(wěn)定后,考慮引擎注入。
- 每個(gè)setter對(duì)應(yīng)一個(gè)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組件會(huì)有問題,我們用類組件做一層包裹。
export const SetterHoc = (Component: any) => { return class SetterComponent extends React.Component { render() { return <Component {...this.props} />; } };};
- 獲取和設(shè)置其它props值
有的需求我們在setter中需要獲取其它組件的屬性,通過props?.field?.parent 可以獲取到,這里封裝了一個(gè)自定的hooks,來獲取和設(shè)置值
export const usePropsValue = (props: any) => { const getPropValue = useMemoizedFn((key: string) => { const propsField = props?.field?.parent; // 獲取同級(jí)其他屬性 showJump 的值 return propsField.getPropValue(key); }); const setPropsValue = useMemoizedFn((key: string, value: any) => { const propsField = props?.field?.parent; // 獲取同級(jí)其他屬性 showJump 的值 propsField.setPropValue(key, value); }); return { getPropValue, setPropsValue, };};
還有一種方法可以可以實(shí)現(xiàn)此效果,就是在setter上設(shè)置extraProps屬性,這個(gè)屬性可以有兩個(gè)方法setValue和getValue.
- 在meta上設(shè)置
// 更改其它選項(xiàng),在meta上設(shè)置extraProps: OptionsSetterExtraProps,
// 更改其它選項(xiàng),在meta上設(shè)置extraProps: OptionsSetterExtraProps,
- setter之間通信
在引擎中,通信需要通過事件的方式去做。在這里,通常我們有些setter的變更會(huì)影響其它setter,例如:日期的格式變化默認(rèn)值會(huì)做相應(yīng)的調(diào)整。在業(yè)務(wù)中,setter的變更,通知依賴的setter刷新,刷新的時(shí)候重新獲取屬性值,做業(yè)務(wù)調(diào)整。
在這里,封裝了reRender一個(gè)hooks,
export const useReRenderEvent = (props?: IUseReRenderEventProps) => { const { isBindEvent = true } = props || {}; const update = useUpdate(); // 強(qiáng)制觸發(fā)更新 const reRenderEvent = useMemoizedFn(() => { update(); }); /** * 發(fā)送重新渲染事件 */ const sendReRenderEvent = useMemoizedFn(() => { event.emit(EFiledEventName.ReRenderEmit); }); useEffect(() => { isBindEvent && event.on(EFiledEventName.ReRender, reRenderEvent); return () => { isBindEvent && event.off(EFiledEventName.ReRender, reRenderEvent); }; }, [isBindEvent]); return { sendReRenderEvent, };};
這個(gè)hooks,有兩個(gè)作用,一個(gè)是發(fā)送重新渲染事件,一個(gè)是監(jiān)聽渲染事件。在上面的案例當(dāng)中,
1.在格式的setter中引入該hooks,做事件發(fā)送。
const { sendReRenderEvent } = useReRenderEvent({ isBindEvent: false });return ( <Select ... onChange={(val) => { changeReRenderEvent && sendReRenderEvent(); }} ... />);
- 在默認(rèn)值setter中,做事件的監(jiān)聽。
// 監(jiān)聽格式的變化useReRenderEvent();// 獲取格式數(shù)據(jù)const { getPropValue } = usePropsValue(otherProps);const format = getPropValue('format');
渲染詳情頁
封裝FormContainerRnder組件,來做渲染。
- 引擎提供了ReactRender的能力,我們傳入對(duì)應(yīng)的scheam信息,就可以做到顯示。
<ReactRenderer className="lowcode-plugin-sample-preview-content" schema={schema} designMode="dialog" rendererName="LowCodeRenderer" components={components} onCompGetRef={onCompGetRef} appHelper={{ requestHandlersMap: { Fetch: createFetchHandler(), }, }}/>
- 獲取FormContainer組件Ref
在數(shù)據(jù)提交的時(shí)候,我們需要獲取組件的實(shí)力,在引擎中獲取Ref方法,要使用 onCompGetRef方法。
const onCompGetRef = (schema: any, ref: any) => { if ('FormContainer' === schema.componentName) { const { formRef, ...otherRef } = ref; formInstanceRef.current = ref.formRef; formOtherRef.current = otherRef; // 獲取到ref,執(zhí)行resolve promiseRef?.resolve(true); }};
在渲染的時(shí)候,我們有可能獲取不到實(shí)例,我們用個(gè)異步來處理。
// 此處異步是因?yàn)椴荒芰ⅠR獲取到form的實(shí)例const promiseRef = useCreation(() => { return createPromiseWrapper();}, []);
提供對(duì)外的數(shù)據(jù)能力
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); }, }; }, []);
- 初始化數(shù)據(jù)
打開編輯詳情頁的時(shí)候,需要把從接口獲取的數(shù)據(jù)給設(shè)置到表單上。有了FormContainer實(shí)例,我們可以很方便的做設(shè)置
useAsyncEffect(async () => { if (mode !== EMode.create && !itemData.loading && Object.keys(itemData.data).length > 0) { // 獲取實(shí)例 const formInstance = await formRef.current?.getFormInstance(); // 數(shù)據(jù)轉(zhuǎn)換 const formValues = convertItemDataToFormValues(itemData.data, table.data.columns); // 設(shè)置值 formInstance?.setFieldsValue?.(formValues); }}, [itemData.data, itemData.loading]);
提交數(shù)據(jù)
獲取表單數(shù)據(jù),做提交。這里通過FormContainer的時(shí)候,可以獲取所有的值,包括做一些前端的校驗(yàn)等。
- 獲取所有值,調(diào)用api,做數(shù)據(jù)提交
export const getFormValues = async (formRef: React.MutableRefObject<IPreviewRef>) => { const formInstance = await formRef?.current?.getFormInstance(); return formInstance?.getFieldsValue();};
開發(fā)調(diào)試
開發(fā)物料后,如果我們發(fā)布npm,整個(gè)流程會(huì)很繁瑣,效率低,物料腳手架也提供了調(diào)試,不過在我們實(shí)際業(yè)務(wù)開發(fā)中,會(huì)有一些業(yè)務(wù)數(shù)據(jù)和上下文的環(huán)節(jié)依賴,所有要能實(shí)時(shí)調(diào)試開發(fā)。接下來幾個(gè)步驟介紹一下
- 啟動(dòng)lowcode開發(fā)模式
"lowcode:dev": "build-scripts start --config ./build.lowcode.js",
會(huì)開啟一個(gè)實(shí)時(shí)的監(jiān)聽服務(wù)。
- 在我們引擎中的assets.json修改,使用上面服務(wù)的地址,修改內(nèi)容如下
修改在url中的內(nèi)容為本地地址,這時(shí)候我們開發(fā)后。刷新瀏覽器,會(huì)實(shí)時(shí)看到結(jié)果
- 做環(huán)境變量,動(dòng)態(tài)切換
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;};
總結(jié)
以上就是對(duì)lowcode-engine低代碼實(shí)戰(zhàn)內(nèi)容,后續(xù)我們介紹一下引擎和后臺(tái)之間的交互,可以讓大家實(shí)現(xiàn)一個(gè)完整的案例。
作者:Witty_Wizard
鏈接:https://juejin.cn/post/7346865556328808463
版權(quán)聲明:本文內(nèi)容由互聯(lián)網(wǎng)用戶自發(fā)貢獻(xiàn),該文觀點(diǎn)僅代表作者本人。本站僅提供信息存儲(chǔ)空間服務(wù),不擁有所有權(quán),不承擔(dān)相關(guān)法律責(zé)任。如發(fā)現(xiàn)本站有涉嫌抄襲侵權(quán)/違法違規(guī)的內(nèi)容, 請(qǐng)發(fā)送郵件至 舉報(bào),一經(jīng)查實(shí),本站將立刻刪除。