CRF自定义表单

Aditya2022-01-24前端ReactFormily

iform

基于 formilyopen in new window 的可视化表单搭建系统。

这里引用官网的描述: Formily 是一个由阿里巴巴集团多部共建的面向中后台复杂场景的表单解决方案,它也是一个表单框架。

Formily实现了什么?

  • 在复杂联动场景下更加清晰简单的描述联动的方式
  • 在超多表单项场景下可以获得更好的表单操作性能
  • 在跨终端场景下实现通用表单解决方案

架构

iform

  • packages
    • iform-components-editors
    • iform-components
    • iform-controller
    • Iform-editor
    • iform-example
    • iform-renderer
    • iform-shared

Form Controller

一个默认实现的多表单控制器,包含数据字典、表单大纲、表单预览、表单切换以及新建表单等功能。同时 Form Controller 还负责表单数据的获取、草稿的保存以及表单的发布。

Form Editor

表单编辑器,负责表单组件的增、删、改、拖拽、排序等。同时向外暴露保存草稿、删除表单等事件,这些事件由 Form Controller 接收并处理。

Component Editor

表单组件编辑器,根据用户在界面上的输入,输出渲染表单组件时需要的 JsonSchema。组件编辑器与组件是一对多的关系,一个组件编辑器,根据不同的配置,可以输出不同组件的 JsonSchema。组件编辑器支持自定义扩展。

Component

表单组件,即用户在填写表单时,每个表单字段所对应的组件,它们负责满足具体的业务需求。表单组件支持自定义扩展。

考虑到表单有在多端(PC 端、移动端)展示的需求,需要针对每个端开发对应的组件。即同一份 JsonSchema,在不同的端,渲染不同的组件。

Form Renderer

表单渲染器,在 formily 的 SchemaForm 基础上,封装了自定义联动的处理能力。针对不同的端,有不同的表单渲染器。

Shared

定义一些公用的方法、组件、interface。

理解 JsonSchema

JsonSchema 是一套用于描述 JSON 文档的数协议,详见官方文档open in new window。formily 借助 JsonSchema 来描述表单配置数据,并在 JsonSchema 基础做了一些扩展,详见 formily 文档:理解 Form Schemaopen in new window。iform 在 formily 的基础上,又扩展了 linkagemetadata 两个字段,iform 中使用的的 JsonSchema 结构如下所示:

interface JsonSchema {
  title?: string | React.ReactNode;
  type?: 'string' | 'object' | 'array' | 'number' | 'boolean' | string;
  properties?: {
    [key: string]: JsonSchema;
  };
  items?: Record<string, JsonSchema>;
  'x-props'?: {
    linkage?: Linkage;
    metadata: FieldMetadata;
    [name: string]: any;
  };
  'x-component'?: string;
  'x-component-props'?: {
    [name: string]: any;
  };
  'x-index'?: number;
  default?: any;
  enum?: LabelValuePair[];
  description?: any;
  required?: boolean;
  disabled?: boolean;
}

metadata

metadata 中存放与后端约定好的关于表单配置的描述性信息,方便后端处理。metadata 支持自定义扩展,详见 扩展自定义组件

interface FieldMetadata {
  index?: number;
  key: string;
  name: string;
  uuid: string;
  required?: boolean;
  uneditable?: boolean;
  editorName?: string;
  canBeNotSure?: boolean;
  isModule?: boolean;
  isMultiple?: boolean;
  canAdd?: boolean;
  canDelete?: boolean;
  storeType?: string;
}

联动(Linkage)

由于 formily 中的联动描述 x-linkages 只能描述一对多的联动关系,而 iform 需要实现多对一的联动关系。下面是 iform 中联动描述的定义:

enum LinkageOperator {
  EQ = 'eq',
  NQ = 'nq',
  INCLUDE = 'include',
}

interface LinkageCondition {
  source: string; // 联动中的控制对象
  target?: string; // 联动中的被控对象,当前 field
  rules: [LinkageOperator, any];
}

interface Linkage {
  operator: 'or' | 'and';
  conditions: LinkageCondition[];
}

接入 FormController

iform 实现了默认的 FormController,一般情况下,业务方直接接入即可。示例如下:

import 'antd/dist/antd.css';
import { Button } from 'antd';
import React, { useMemo } from 'react';
import { Editor, Effects, validationAndSubmit } from '@yitu/iform';

import './index.css';
import * as apis from 'path/to/you/services';

export default function() {
  const publish = () => {
    validationAndSubmit().then(data => {
      console.log('...data:', data);
    });
  };

  const effects: Effects = useMemo(
    () => ({
      keyGenerator: apis.genFieldKey,
      createForm: apis.createForm,
      deleteForm: apis.deleteForm,
      saveForm: apis.saveForm,
      fetchForm: apis.fetchForm,
      fetchFormList: apis.fetchFormList,
      fetchDictionary: apis.fetchDictionary,
    }),
    [],
  );

  return (
    <div className="normal">
      <div className="exampleHeader">
        <Button type="primary" onClick={publish}>
          发布
        </Button>
      </div>
      <Editor classNames="crfEditor" effects={effects} />
    </div>
  );
}

在上述的接入代码中,我们从 iform 中引入了一个 Editor 组件, Editor 组件接受两个参数:

  • classNames:可选参数,用于控制 Editor 组件的样式
  • effects:Editor 组件内部使用到的副作用
  • createCopy: 可选参数,复制副本id,0.6.x新增参数

effects的定义

interface Effects {
  keyGenerator: KeyGenerator;
  // 创建表单并返回对应的 id
  createForm: (name: string, key: string, content: any) => Promise<{ formId: string }>;
  // 删除表单
  deleteForm: (formId: string) => Promise<{ success: boolean }>;
  // 保存表单草稿
  saveForm: (
    formId: string,
    formKey: string,
    name: string,
    content: any,
  ) => Promise<{ success: boolean }>;
  // 获取单个表单的内容(已配置过的字段)
  fetchForm: (formId: string) => Promise<{ success: boolean; data: any }>;
  // 获取表单列表
  fetchFormList: () => Promise<Array<{ id: string; name: string; key: string }>>;
  // 获取数据字典
  fetchDictionary: () => Promise<DictionaryTable[]>;
}

0.6.x新增fetchTemplateModule、createCopyTemplate 副本创建

KeyGenerator

在配置表单时,每个表单组件都有一个唯一性 key(至少要保证 key 在单个表单作用域内的唯一性)。key 的生成方式因业务而已,其具体的生成函数由各业务方实现,并传递给 iform。KeyGenerator 的定义如下:

interface KeyGeneratorParams {
  name: string;
  oldKey: string;
  uuid: string;
  form?: {
    id: string;
    name?: string;
    key?: string;
  };
}

type KeyGenerator = (params: KeyGeneratorParams) => Promise<string>;

表单配置的校验与发布

iform 导出了一个名为 validationAndSubmit 的异步函数。在该函数内部,首先会校验表单配置的完备性,完备性校验通过后,会返回所有的表单配置。

saveForm

image-20220124112440592

这里有个点击空白处会有快速保存的功能,它会直接跳过表单完备性校验,在四合一0.6.x版本,这里我在 editor 导出validateQuestion方法供 controller 预览按钮、提交进行完备性校验。

接入 FormEditor

默认的 FormController 可能无法满足特定的业务场景,这时,可以在 FormEditor 的基础上封装自己的 FormController,具体请参考默认 FormController 的实现。下面详细介绍 FormEditor 的用法。

初始化 FormEditor

FormEditor 的一些内部流程,以及 ComponentEditor registry 等,都需要初始化后才能使用。

import { initializeFormEditor } from '@yitu/iform-editor';

initializeFormEditor();

注册 ComponentEditor

FormEditor 在初始化后,还需要注册对应的 ComponentEditor。

import { SingleSelectEditor } from '@yitu/iform-component-editors';
import { registerComponentEditor } from '@yitu/iform-editor';

registerComponentEditor(SingleSelectEditor);

接入 FormEditor 组件

import React, { useMemo } from 'react';

import { KeyGenerator, JsonSchema } from '@yitu/iform/shared';
import FormEditor, { FormEditorHooks, FormEditorEffects } from '@yitu/iform-editor';

interface FormEditorProps {
  id: string;
  keyGenerator: KeyGenerator;
}

const FormEditorWrapper: React.FC<FormEditorProps> = props => {
  const formEditorEffects = useMemo<FormEditorEffects>(
    () => ({
      onDeleteForm: (formId: string) => {
        // 删除表单事件
      },
      onSaveForm: ({ formId, jsonSchema, message }) => {
        // 保存草稿事件
      },
    }),
    [],
  );

  const formEditorHooks = useMemo<FormEditorHooks>(
    () => ({
      onBeforeUpdateFormName: (formId: string, name: string) => {
        return true;
      },
    }),
    [],
  );
  
  const deletable = () => {
    // 判断当前表单是否可删除
    return true;
  }

  const jsonSchema = () => {
    // 获取当前表单对于的 JsonSchema
    return {} as JsonSchema;
  }

  return (
    <FormEditor
      id={props.id}
      deletable={deletable()}
      hooks={formEditorHooks}
      jsonSchema={jsonSchema()}
      keyGenerator={props.keyGenerator}
      effects={formEditorEffects}
    />
  );
};

export default FormEditorWrapper;

FormEditorProps

上述示例代码中,我们从 iform/form-editor 导入了一个 FormEditor 组件,该组件能接收的 props 的定义如下:

interface FormEditorProps {
  id: string;
  deletable: boolean;
  jsonSchema: JsonSchema;
  keyGenerator?: KeyGenerator;
  hooks?: FormEditorHooks;
  effects?: FormEditorEffects;
}

其中:

  • id 表示当前编辑的表单数据的 id
  • deletable 用于控制删除表单按钮是否可点击
  • jsonSchema 表示当期编辑的表单数据。jsonSchema 只在初始化 FormEditor 组件时起作用,相当于 initialValue,后续状态由 FormEditor 在其内部维护
  • keyGenerator:用于生成表单组件的唯一性 key,默认为 uuid

FormEditorHooks

在编辑表单配置的过程中,可能需要一些外部的同步介入。例如,在编辑完表单名称并保存时,需要校验当前保存的表单名称的唯一性。如果表单名称唯一,退出表单名称的编辑态,保存成功;如果存在同名表单,则中断当前的保存流程,并提示用户,此时表单名称组件依然处于编辑态。但 FormEditor 是一个单表单的编辑环境,无法感知其他表单的存在,此时就需要外部的同步介入。

FormEditorHooks 提供了介入表单编辑流程的能力,hooks 中的方法同步调用。当 hooks 返回 true 时,不影响当前的编辑流程;当返回 false 时,终端当前的流程,并提示用户。FormEditorHooks 的定义如下:

interface FormEditorHooks {
  onBeforeUpdateFormName?: (formId: string, name: string) => boolean;
}

FormEditorEffects

在编辑表单配置的过程中,会向外暴露一些事件,外部系统对这些事件的响应通过 effects 传递到 FormEditor 内部。当前只向外暴露了删除表单和保存草稿这两个事件。

interface FormEditorEffects {
  onDeleteForm: (formId: string) => void;
  onSaveForm: (param: SaveFormParam) => void;
}
Last Updated 2024/12/27 11:36:49