阿里低代码框架 lowcode-engine 低代码表单实战

发布于:2024-04-26 ⋅ 阅读:(16) ⋅ 点赞:(0)

前沿

lowcode-engine功能比较强大,最近这段时间做了个低代码表单的实战,在过程中遇到一些问题,在这里做下介绍和总结。

功能演示

gif1.gif

前台功能

主要介绍一下前台功能的基本实现和一些问题。

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属性,这个属性可以有两个方法setValuegetValue.

  1. 在meta上设置
// 更改其它选项,在meta上设置
extraProps: OptionsSetterExtraProps,

export const OptionsSetterExtraProps = {
  setValue: (target: any, value: any) => {},
  getValue:(target: any, value: any) => {}
};
  • 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();
    }}
    ...
  />
);
  1. 在默认值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修改,使用上面服务的地址,修改内容如下

image.png

修改在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低代码实战内容,后续我们介绍一下引擎和后台之间的交互,可以让大家实现一个完整的案例。

如果你觉得该文章不错,不妨

1、点赞,让更多的人也能看到这篇内容

2、关注我,让我们成为长期关系

3、关注公众号「前端有话说」,里面已有多篇原创文章,和开发工具,欢迎各位的关注,第一时间阅读我的文章


网站公告

今日签到

点亮在社区的每一天
去签到