可视化编程的一次尝试

本文为可视化编程的初尝试,仅实现基本核心功能。以配置后台表单功能页为主。

功能演示

配置页面:
image.png
生成代码:

import React, { FC, useEffect, useState } from 'react';
import { PageContainer } from '@ant-design/pro-layout';
import Components from '@/components';
import { getStudent } from '@/services'

const { Card, Form, Row, FormItemInput, SearchBtn, FormItemButton, MyTable } = Components;
const layout = {
  labelCol: {
    span: 8,
  },
  wrapperCol: {
    span: 16,
  },
};
const App: FC<any> = (props: any) => {
  const columns: any = [
    {
      title: '姓名',
      key: 'name',
      width: 20,
      dataIndex: 'name',
    },
    {
      title: '学号',
      key: 'id',
      width: 20,
      dataIndex: 'id',
    },
    {
      title: '学校',
      key: 'school',
      width: 20,
      dataIndex: 'school',
    },
  ];
  const [params, setParam] = useState<any>({
    name: '',
    id: '',
    school: '',
    pageNo: 1,
    pageSize: 10,
  });
  const [data, setData] = useState([]);
  const [dataTotal, setTotal] = useState<number>(0);
  function getData(pageNo?: number) {
    getStudent({ ...params, pageNo: pageNo || 1 }).then((res: any) => {
      setData(res.data.records);
      setTotal(res.data.total);
    });
  }

  useEffect(() => { }, []);
  return (
    <Form {...layout} hideRequiredMark>
      <PageContainer>
        <Card bordered={false}>
          <Row>
            <FormItemInput
              label="姓名"
              name="name"
              onChange={(e: any) => {
                setParam({ ...params, name: e.target.value });
              }}
            ></FormItemInput>
            <FormItemInput
              label="学号"
              name="id"
              onChange={(e: any) => {
                setParam({ ...params, id: e.target.value });
              }}
            ></FormItemInput>
            <FormItemInput
              label="学校"
              name="school"
              onChange={(e: any) => {
                setParam({ ...params, school: e.target.value });
              }}
            ></FormItemInput>
            <FormItemButton>
              <SearchBtn onClick={getData}></SearchBtn>
            </FormItemButton>
          </Row>
          <MyTable columns={columns} data={data} dataTotal={dataTotal} onChange={getData}></MyTable>
        </Card>
      </PageContainer>
    </Form>
  );
};

export default App;
复制代码

程序架构

传统技能,上图:

image.png

程序的数据流使用Redux进行统一管理,声明了3个变量:ifDrap、domList、selectDom。
程序设计思想为:通过拖拽实现将选择的组件数据录入到domList中。组件树框提供了选中编辑,点击需要编辑的组件既可以设置selectDom,在属性设置中设置相应属性。

数据流

Redux数据结构:

export interface StateType {
ifDrap: boolean, 
domList: any,
selectDom: any
}

export interface LoginModelType {
namespace: string;
state: StateType;
effects: {
  ifDrap: Effect;
  domList: Effect
  selectDom: Effect
};
reducers: {
  changeIfDrap: Reducer<StateType>;
  changeDomList: Reducer<StateType>
  changeSelectDom: Reducer<StateType>
};
}
复制代码
ifDrap

拖拽插件使用的react-dnd,因需要实现多层的拖拽,插件本身并不提供。因ReactDND监听事件是冒泡事件,故需此参数判断拖拽的层级。

const [_, droper] = useDrop({
        accept: accept,
        collect: (minoter: DropTargetMonitor) => {
        },
        drop: (item) => {
            if (!userLogin.ifDrap) { 
                dispatch({
                    type: 'login/login',
                    payload: {
                        ifDrap: true,//修改ifDrap,因拖拽为冒泡事件,后续的拖拽事件将不会触发
                        domList: [...]
                    },
                });
            }
        },
    })
复制代码
domList
[
  {
    childDom: (4)[{ … }, { … }, { … }, { … }]
    prop: { style: { … } }
    type: "Row"
  },
  {
    prop: { style: { … }, columns: Array(3), data: Array(0), dataTotal: "0", onChange: ƒ }
    type: "MyTable"
  }
]
复制代码

domList存储了从reactCategory里拖拽过来的组件JSON,整个程序的核心数据。

ReactDnd

ReactDnd的api较为简单,可以前往官网自行学习。但ReactDND视乎并没有提供多层嵌套的标识,故需自定义如上的ifDrop作为标识。

reactCategory 组件的JSON合集

...
MyTable: {
  components: {
    MyTable: {
      props: [
        { style: { backgroundColor: '#fff' }, columns: [], data: [], dataTotal: '0', onChange: () => { } }
      ]
    }
  }
}
...
复制代码

props为组件的属性,setProps操作对象。

JSON生成组件


    function renderDragItem() {

        let renderChild = prop.children
        if (childDom) {
            renderChild = childDom.map((item: any, index: number) => {
                let temp = cloneDeep(level)
                temp.push(index)
                return <DropItem type={item.type} level={temp} prop={item.prop} childDom={item.childDom}></DropItem>
            })
        }
        return createElement(get(MyCom, type, type), {
            ...prop,
        }, renderChild)
    }
复制代码
  • level:例:[0,0]。level是组件的深度位置,标识出组件在domList中的位置。

exportFun 导出代码

导出代码的设计为:

image.png

getNewComponentNode 数组JSON to AST
  // 组件属性处理
      let propStr = map(prop, (item, key) => {
       if (noConfigPro.get(key)) {
           return noConfigPro.get(key)
       }
       if (Object.prototype.toString.call(item) === '[object Object]') {
           if (key === 'style') {
               return ''
           }
           if (key === 'children') {
               return children?.push(item)
           }
           let objPropStr = map(item, (propItem, propKey) => {
               return `${propKey}: "${propItem}"`
           }).join(',')
           return `${key}={{${objPropStr}}} `
       } else if (Object.prototype.toString.call(item) === '[object Array]') {
           if (key !== 'data') {
               stateConst(item, key)
           }
           return `${key}={${key}} `
       } else if (Object.prototype.toString.call(item) === '[object String]') {
           if (key === 'children') {
               let temp = cloneDeep(children)
               temp?.push(item)
               children = temp
               return ''
           }
           return `${key}="${item}" `
       }
   }).join(' ')
复制代码

组件的属性有多种,通过 Object.prototype.toString.call进行判断做各种解析。考虑到需要加入自定义组件,一些自定义的组件的属性需要特殊解析,故加了componyAddParsePros。

componyAddParsePros: 组件附加解析规则
const componyAddParsePros = new Map()

componyAddParsePros.set('MyTable', MyTableParse)

function MyTableParse(ast?: any, noAction?: boolean, prop?: any) {
   let noConfigPro = new Map()
   traverse(ast, {
       XXX(node: any) {
          ...
       }
   })
   noConfigPro.set(xxx, xxxx)
   return noConfigPro
}
复制代码

componyAddParsePros返回Map,每次解析数组JSON的属性都会匹配map,如果存在自定义的解析规则则使用匹配到的自定义解析规则。

小结

通过AST+ babel实现组件拖拽生成代码,组件库可为自定义组件也可为开源组件库。组件的特殊属性配置也可自定义解析规则。生成的代码可读性高,二次修改也较为简单。本次开发功能集中于管理后台的重复表格操作功能页,但也可适用于H5的活动页生成。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享