低代码开发实战方案(react)

4,489 阅读2分钟

设计理念

代码设计和渲染的库和解法非常多,因为这里确实很痛。大家都想找一个省去重复劳动的解决方案,同时大家又都怀着千人千面的现实需求,这其实是一个“被诅咒的问题”:要便于使用就要减少配置量,要自由定制就要增加配置量。这样的问题硬解是解不来的,在不断的对接各种业务平台、不断的摸索中通过取舍、限制和牺牲,换来一个即便于使用,也支持定制的方案,一个我们自己愿意使用的方案:使用 JSON 配置来生成页面,可以减少页面开发工作量,极大提升效率。

开源地址 演示地址

极简 api

<CustomComponent value={this.state.value} onChange={this.onChange} />

以此基础最简版的 form-render 的 api

<SchemaRender
  schema={schema}
  formData={this.state.value}
  onChange={this.onChange}
/>

其中 schema 用于描述表单的 UI 必不可少, formData 就是 value。这就是最简版可用的 SchemaRender 了!

import React, { useState } from 'react';
import { SchemaRender } from "~renderer";

const schema = {
  type: 'object',
  properties: {
    string: {
      title: '字符串',
      type: 'string',
    },
    select: {
      title: '单选',
      type: 'string',
      enum: ['a', 'b', 'c'],
      enumNames: ['选项1', '选项2', '选项3'],
    },
  },
};

const Demo = () => {
  const [formData, setFormData] = useState({});
  return (
    <SchemaRender schema={schema} formData={formData} onChange={setFormData} />
  );
};

export default Demo;

复杂场景 Demo

const schema_json = {
  type: "object",
  properties: {
    case1: {
      title: "基础控件",
      type: "object",
      displayType: "column",
      labelWidth: 110,
      properties: {
        input: {
          title: "简单输入框",
          type: "string",
          displayType: "row",
          required: true,
          options: {
            placeholder: "请输入"
          }
        },
        email: {
          title: "邮箱输入",
          description: "邮箱格式验证",
          type: "string",
          displayType: "row",
          format: "email",
          required: true,
          pattern: "^[.a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(.[a-zA-Z0-9_-]+)+$"
        },
        ...
};

设计思路

根据自定义组件类型定义type解析关系

const widgets = {
  checkbox,
  checkboxes, 
  color,
  date,
  dateRange,
  input
  multipleSelect,
  number,
  radio,
  select,
  slider,
  switch,
  textarea,
  html,
  ...
};

灵活处理映射关系mapping

const mapping = {
  default: "input",
  string: "input",
  boolean: "switch",
  integer: "number",
  number: "number",
  object: "map",
  html: "html",
  size: "size",
  select: "select",
  "date:dateTime": "date",
  "string:upload": "upload
  "string:date": "date",
  "string:dateTime": "date",
  "string:time": "date",
  "string:textarea": "textarea",
  "string:color": "color",
  "range:date": "dateRange",
  "range:dateTime": "dateRange",
  ...
};

通过高阶函数包装自定义组件

import React from "react";

// High order component
export default function fetcher(FieldComponent) {
  return class extends React.Component {
    render() {
      return <FieldComponent {...this.props} />;
    }
  };
}

解析json配置生成单元组件

import React, { useRef, useMemo, useEffect, useImperativeHandle } from "react";

// todo: schemaParser解析core配置
function RenderField({ fields, onChange, ...settings }) {
  const { Field, props } = schemaParser(settings, fields);
  if (!Field) {
    return null;
  }
  return <Field isRoot {...props} value={settings.data} onChange={onChange} formData={settings.formData} />;
}

/**
 * @param generated 根据 Widget 生成的 Field
 * @param customized 自定义的 Field
 * @param mapping 字段 type 与 widgetName 的映射关系
 */
function FieldRender({
  className = "",
  name = "$Field",
  schema = {},
  formData = {},
  widgets = {},
  FieldUI = DefaultFieldUI,
  fields = {},
  mapping = {},
  onChange = () => {},
  forwardedRef
}) {
  const originWidgets = useRef();
  const generatedFields = useRef({});

  const rootData = useMemo(() => schemaResolve(schema, formData), [schema, formData]);

  // data修改比较常用,所以放第一位
  const resetData = (newData, newSchema) => {
    const _schema = newSchema || schema;
    const _formData = newData || formData;
    const res = schemaResolve(_schema, _formData);
    return new Promise((resolve) => {
      onChange(res);
      resolve(res);
    });
  };

  useImperativeHandle(forwardedRef, () => ({
    resetData
  }));

  // 用户输入都是调用这个函数
  const handleChange = (key, val) => {
    onChange(val);
  };

  const generated = useMemo(() => {
    let obj = {};
    if (!originWidgets.current) {
      originWidgets.current = widgets;
    }
    Object.keys(widgets).forEach((key) => {
      const oWidget = originWidgets.current[key];
      const nWidget = widgets[key];
      let gField = generatedFields.current[key];
      if (!gField || oWidget !== nWidget) {
        if (oWidget !== nWidget) {
          originWidgets.current[key] = nWidget;
        }
        gField = asField({ FieldUI, Widget: nWidget });
        generatedFields.current[key] = gField;
      }
      obj[key] = gField;
    });
    return obj;
  }, []);

  const settings = {
    className,
    name,
    schema,
    data: rootData,
    formData
  };

  const fieldsProps = {
    // 根据 Widget 生成的 Field
    generated,
    // 自定义的 Field
    customized: fields,
    // 字段 type 与 widgetName 的映射关系
    mapping
  };

  return <RenderField {...settings} fields={fieldsProps} onChange={handleChange} />;
}

// todo: 外层容器同组件耦合抽离
const Wrapper = ({ schema, ...args }) => {
  return <FieldRender schema={combineSchema(schema)} {...args} />;
};

export default Wrapper;

如何做到容器支持无限级嵌套

import React, { useState } from "react";

// todo: getSubField处理子集对象或者数组形式的json解析
function SubComponent((p)  {
  return (
    <>
      {Object.keys(p.value).map((name) => {
        return p.getSubField({
          name,
          value: p.value[name],
          rootValue: p.value,
          onChange(key, val, objValue) {
            let value = { ...p.value, [key]: val };
            // 第三个参数,允许object里的一个子控件改动整个object的值
            if (objValue) {
              value = objValue;
            }
            p.onChange(p.name, value);
          }
        });
      })}
    </>
  );
});

export default memo(SubComponent);

未完待续...