当前位置:   article > 正文

低代码引擎实战 - 从零封装低代码组件_react lowcode

react lowcode

一、 Container

构造页面时需要给其他组件一个容器来包裹,先用 vant 的 Card 组件来封装我们的容器组件 Container。

src/components 目录下新建 Container 文件夹,再创建 Container.tsx 和 index.tsx 文件

Container.tsx

import * as React from 'react';
import {createElement} from 'react';
import {Card} from 'react-vant';
import './index.scss'

export interface ContainerProps {
  title?: string;
  style?: 'object'
  direction?: 'row' | 'column'
}

/**
 *  Card 组成的 container 容器
 * @param title
 * @param children
 * @param otherProps
 * @constructor
 */
const JContainer: React.FC<ContainerProps> = ({title, children, direction = 'column', style = {}, ...otherProps}) => {
  const _style = style || {} as any;
  _style.flexDirection = direction;
  const _otherProps = otherProps || {} as any;
  _otherProps.style = _style;
  
  return (
    <Card>
      {title && <Card.Header>{title}</Card.Header>}
      <Card.Body>
        <div className={'container-wrapper'} {..._otherProps}>
          {children}
        </div>
      </Card.Body>
    </Card>
  )
}

export default JContainer
复制代码
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38

direction 属性是控制 Container 里面元素的排列方式,对应 flex 布局的 flex-direction 属性。

index.tsx

import Container from './Container'

export type {ContainerProps} from './Container'
export default Container;
复制代码
  • 1
  • 2
  • 3
  • 4
  • 5

然后在 src/index.tsx 导出

export type {ContainerProps} from './components/container'
export {default as Container} from './components/container'
复制代码
  • 1
  • 2
  • 3

运行命令 npm run lowcode:dev 会看到跟 src 同级的目录 lowcode 目录下多了个 container 文件夹,里面有个 meta.ts 文件,这是根据代码生成的组件描述文件,在拖拽使用这个组件时,低代码引擎根据这个描述文件来解析组件。

import { ComponentMetadata, Snippet } from '@alilc/lowcode-types';

const ContainerMeta: ComponentMetadata = {
  "componentName": "Container",
  "title": "Container",
  "docUrl": "",
  "screenshot": "",
  "devMode": "proCode",
  "npm": {
    "package": "mini-elements",
    "version": "0.1.1",
    "exportName": "Container",
    "main": "src/index.tsx",
    "destructuring": true,
    "subName": ""
  },
  "configure": {
    "props": [
      {
        "title": {
          "label": {
            "type": "i18n",
            "en-US": "title",
            "zh-CN": "title"
          }
        },
        "name": "title",
        "setter": {
          "componentName": "StringSetter",
          "isRequired": true,
          "initialValue": ""
        }
      }
    ],
    "supports": {
      "style": true
    },
    "component": {}
  }
};
const snippets: Snippet[] = [
  {
    "title": "Container",
    "screenshot": "",
    "schema": {
      "componentName": "Container",
      "props": {}
    }
  }
];

export default {
  ...ContainerMeta,
  snippets
};
复制代码
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56

默认生成的描述文件,可能不能满足需求,需要拓展。

想要更多自定义配置,有两种方式:

在代码中写 propTypes 自动生成
手动配置
定义好组件的 Props 之后,运行 npm run lowcode:dev 命令会根据当前定义的 props 自动生成描述文件,基本类型自动生成的描述一般没啥问题,但如果是复杂对象可能会描述不太准确。

注意这里有个坑,只有第一次运行以上命令才会自动生成描述文件,如果这个组件的描述文件已经存在,我们又修改了组件,再次运行命令则不会将新增的属性追加进描述文件中,换句话说以后都需要手动配置了。

有个小技巧可以减轻工作量,如果你没有手动改过配置文件,那修改组件源码后,每次运行前把描述文件删掉,就可以按照最新的 Props 自动生成新的描述文件了。

但是如果按下面的方式手动配置过描述文件,不建议删掉重新生成,之前手动配置的都会丢失。

更改 lowcode/contianer/meta.ts,想要它成为一个容器,在 component 对象下设置 isContainer 即可。

如果想添加新的属性,或者代码中组件的 props 中定义的属性没有显示出来,则需要手动新增 props。

direction 属性想要枚举值,只有 row 和 column 两个属性值。查询支持的设置器,发现 RadioGroupSetter 可以满足需求,按照定义写我们自己的属性和设置器

{
  name: 'direction',
  description: '内容的排列方向',
  setter: {
    componentName: 'RadioGroupSetter',
    initialValue: 'column',
    props: {
      options: [
        'column',
        'row'
      ],
    }
  }
}
复制代码
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

完整的定义如下:

import {ComponentMetadata, Snippet} from '@alilc/lowcode-types';

const ContainerMeta: ComponentMetadata = {
  "componentName": "Container",
  "title": "Container",
  "docUrl": "",
  "screenshot": "",
  "devMode": "procode",
  "npm": {
    "package": "mini-elements",
    "version": "0.1.1",
    "exportName": "Container",
    "main": "src/index.tsx",
    "destructuring": true,
    "subName": ""
  },
  "configure": {
    "props": [
      {
        "title": {
          "label": {
            "type": "i18n",
            "en-US": "title",
            "zh-CN": "title"
          }
        },
        "name": "title",
        "setter": {
          "componentName": "StringSetter",
          "isRequired": false,
          "initialValue": ""
        }
      },
      {
        name: 'direction',
        description: '内容的排列方向',
        setter: {
          componentName: 'RadioGroupSetter',
          initialValue: 'column',
          props: {
            options: [
              'column',
              'row'
            ],
          }
        }
      }
    ],
    "supports": {
      "style": true
    },
    "component": {
      isContainer: true,
      nestingRule: {
        // 允许拖入的组件白名单
        // childWhitelist: ['ColorfulButton', 'Button'],
        // 同理也可以设置该组件允许被拖入哪些父组件里
        // parentWhitelist: ['Tab'],
      },
    }
  }
};
const snippets: Snippet[] = [
  {
    "title": "Container",
    "screenshot": "",
    "schema": {
      "componentName": "Container",
      "props": {}
    }
  }
];

export default {
  ...ContainerMeta,
  snippets
};
复制代码
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78

效果如图,可配置一个 title 属性,如果有值则渲染 Header,没有就不渲染。

还可选择 direction 的值,默认 column。

里面可以拖入其他组件,但仅限白名单里的组件。

在这里插入图片描述

二、Panel 组件

先看下效果图,Panel 组件包含两部分:Title 和 Content,重点突出 content 的内容。

右边可配置的属性为:

title: 标题
content:内容,一般为数字
flex: flex 布局下所占的份数,同 css 的 flex 属性,默认 1

在这里插入图片描述

在 src/components 下新建 panel 目录,并创建 Panel.tsx、index.tsx 和 index.scss 三个文件

// Panel.tsx
import React, { createElement } from 'react'
import './index.scss'

export interface PanelProps {
  title: string;
  content: string;
  flex: number;
}

const Panel: React.FC<PanelProps> = ({title, content, flex = 1, children, ...otherProps}) => {
  
  const _otherProps = otherProps || {} as any;
  // @ts-ignore
  _otherProps.style = otherProps.style || {} as any
  _otherProps.style.flex = flex;
  
  return (
    <div className={'panel'} {...otherProps}>
      <div className={'title'}>{title || 'Panel标题'}</div>
      <div className={'content'}>{content || 'Panel内容'}</div>
    </div>
  )
}

export default Panel


// index.tsx
import Panel from './Panel'
export type {PanelProps} from './Panel'
export default Panel;


// index.scss
@import "./src/variables";

.panel {
  display: flex;
  flex-direction: column;

  .title {
    font-size: 12px;
    color: $text-minor;
  }
  .content {
    font-size: 28px;
    font-weight: 500;
    color: $text-main;
  }
}
复制代码
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52

同样需要修改生成的 lowcode/panel/meta.ts 文件,一般来说如果只是修改可配置的属性,只需改 configure.props 属性即可。

import { ComponentMetadata, Snippet } from '@alilc/lowcode-types';

const PanelMeta: ComponentMetadata = {
  "componentName": "Panel",
  "title": "Panel",
  "docUrl": "",
  "screenshot": "",
  "devMode": "procode",
  "npm": {
    "package": "mini-elements",
    "version": "0.1.1",
    "exportName": "Panel",
    "main": "src/index.tsx",
    "destructuring": true,
    "subName": ""
  },
  "configure": {
    "props": [
      {
        "title": {
          "label": {
            "type": "i18n",
            "en-US": "title",
            "zh-CN": "title"
          }
        },
        "name": "title",
        "setter": {
          "componentName": "StringSetter",
          "isRequired": true,
          "initialValue": ""
        }
      },
      {
        "title": {
          "label": {
            "type": "i18n",
            "en-US": "content",
            "zh-CN": "content"
          }
        },
        "name": "content",
        setter: {
          componentName: 'MixedSetter',
          props: {
            setters: [
              'StringSetter',
              'VariableSetter',
            ],
          },
        }
      },
      {
        name: 'flex',
        setter: {
          componentName: 'NumberSetter',
          initialValue: 1
        }
      }
    ],
    "supports": {
      "style": true
    },
    "component": {}
  }
};
const snippets: Snippet[] = [
  {
    "title": "Panel",
    "screenshot": "",
    "schema": {
      "componentName": "Panel",
      "props": {}
    }
  }
];

export default {
  ...PanelMeta,
  snippets
};
复制代码
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82

三、Table 组件

在各种组件中,Table 组件是最复杂的了。要把 Table 封装好,会使用到几乎所有的设置器。

由于时间关系,先只暴露 dataSource 和 columns 属性,通过 columns 属性,我们将学会如何使用 ArraySetter 动态设置数组。通过 dataSource 属性,我们将学会使用 MixedSetter 使属性支持多种设置方式。

本组件基于 antd 的 Table 扩展。

在 src/components 目录下新建 Table 文件夹,然后新建 Table.tsx 和 index.ts 文件

import React, {createElement} from 'react'
import Table, {ColumnsType} from "antd/lib/table";

export interface JTableProps {
  columns: ColumnsType;
  dataSource: any[];
}

const JTable: React.FC<JTableProps> = ({columns, dataSource}) => {

  // 数据处理,防止字段为空
  const _columns = columns?.map((col, index) => {
    if (!col) {
      return {
        dataIndex: `${index}`,
        title: '列名'
      }
    }
    const {dataIndex, title} = col as any;
    return {
      dataIndex: dataIndex || `${index}`,
      title: title || '列名'
    }
  })

  return (
    <Table
      dataSource={dataSource}
      columns={_columns}
    />
  );
}


export default JTable
复制代码
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
import Table,


 {JTableProps} from './Table'

export type {JTableProps}
export default Table;
复制代码
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

别忘了在 src/index.tsx 上注册组件,否则看不到效果。

export type {JTableProps} from './components/Table';
export {default as Table} from './components/Table'
复制代码
  • 1
  • 2
  • 3

运行 npm run lowcode:dev,会在 根目录/lowcode 下生成 table 文件夹,里面的 meta.ts 就是组件的描述文件。

由于我们暴露出的属性 dataSource 和 columns 是复杂结构,自动生成的描述不能满足需求,所以手动更改描述文件:

import { ComponentMetadata, Snippet } from '@alilc/lowcode-types';

const TableMeta: ComponentMetadata = {
  "componentName": "Table",
  "title": "Table",
  "docUrl": "",
  "screenshot": "",
  "devMode": "procode",
  "npm": {
    "package": "mini-elements",
    "version": "0.1.6",
    "exportName": "Table",
    "main": "src/index.tsx",
    "destructuring": true,
    "subName": ""
  },
  "configure": {
    "props": [
      {
        "title": {
          "label": {
            "type": "i18n",
            "en-US": "数据列",
            "zh-CN": "数据列"
          }
        },
        "name": "columns",
        "setter": {
          "componentName": "ArraySetter",
          "props": {
            "itemSetter": {
              "componentName": "ObjectSetter",
              "isRequired": false,
              "props": {
                config: {
                  items: [
                    {
                      "name": "dataIndex",
                      "setter": {
                        "componentName": "StringSetter",
                        "isRequired": true,
                        "initialValue": "id"
                      }
                    },
                    {
                      "name": "title",
                      "setter": {
                        "componentName": "StringSetter",
                        "isRequired": true,
                        "initialValue": "列名"
                      }
                    },
                  ]
                }
              },
            }
          },
          "isRequired": true,
          initialValue: [
            {
              dataIndex: 'id',
              title: 'ID'
            },
            {
              dataIndex: 'name',
              title: '姓名'
            },
            {
              dataIndex: 'age',
              title: '年龄'
            },
          ]
        },
      },
      {
        "name": "dataSource",
        setter: {
          componentName: 'MixedSetter',
          props: {
            setters: [
              'JsonSetter',
              'VariableSetter',
            ],
          },
        }
      }
    ],
    "supports": {
      "style": true
    },
    "component": {}
  }
};
const snippets: Snippet[] = [
  {
    "title": "Table",
    "screenshot": "",
    "schema": {
      "componentName": "Table",
      "props": {}
    }
  }
];

export default {
  ...TableMeta,
  snippets
};
复制代码
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109

效果如图:

在这里插入图片描述

columns 是一个数组,我们可以自由的加减列,所以需要用官方提供的 ArraySetter,使用文档 点这里。每一个 item 都是一个 ObjectSetter,说实话结构还挺复杂的。

dataSource 支持绑定数据源和直接写 json,所以使用 MixedSetter。

四、坑点
如果你用的是 antd 组件库,那么会遇到个大坑。

项目中用到了 @ant-design/icons 时,比如在一个组件中引用了某个 icon,会导致组件渲染报错

在这里插入图片描述

原因是找不到这个图标组件,查一下加载的 js 资源,发现并没有加载 ant-design/icons

在这里插入图片描述

没想到自家的组件库竟然不完全支持!测试发现其他的组件库,像 vant、tea 等都没有这个问题。

暂时还没想到在组件库层面的解决办法,还没找到手动注入 ant-design/icons 的入口。

但是在 demo 中用组件库的时候,找到了解决方案。官方 demo 有个 assets.json,这里定义了引用的资源,我们可以手动把 icon 添加进去,这样在项目运行时, ant-design/icons 就会正常加载,项目也就不报错了。

在这里插入图片描述

这种方法有个缺点,在组件库封装过程中,其实是看不到效果的,因为渲染不出来。只有在具体使用组件库的时候,才会渲染出来,调试不方便。

总结
其实自定义封装组件,总结一下就三步:

在 src/components 文件夹下新建组件的文件夹,写逻辑代码,定义需要对外暴露的 props 。
在 根目录/index.tsx 中注册组件。不注册的话页面上看不到。
运行 npm run lowcode:dev 命令,会在 根目录/lowcode 目录下自动生成组件的描述文件 meta.ts,简单类型的 props 比如 string、bool 一般没啥问题,如果是复杂类型,比如复杂对象、数组,自动生成的描述可能不是我们想要的,这时需要手动改描述文件。
前两步我们都比较熟悉,重点主要在第三步改描述文件。在页面上对组件进行拖拽、配置时,支持的操作都是由描述文件定义的。描述文件的重点是设置器,一个属性支持怎样的交互,是可以输入文字,还是下拉框,还是可增删的数组,都是由设置器定义的。

设置器 Setter 的文档在 这里,里面包含了所有官方提供的 Setter。据平时的经验看,官方的设置器能满足 90% 的日常需求。当然还支持自定义 Setter,这部分我还没研究,可以查看官方文档。

官方的 demo 又更新了,新增了 antd 所有组件的支持,如果没有特殊需求,直接用官方提供的组件省时省力。

这个低代码引擎感觉还是在原型阶段,官方的文档、demo 会时不时更新,及时关注 crmeb 可能会有意外收获。

源码附件已经打包好上传到百度云了,大家自行下载即可~

链接: https://pan.baidu.com/s/14G-bpVthImHD4eosZUNSFA?pwd=yu27
提取码: yu27
百度云链接不稳定,随时可能会失效,大家抓紧保存哈。

如果百度云链接失效了的话,请留言告诉我,我看到后会及时更新~

开源地址

码云地址:
http://github.crmeb.net/u/defu

Github 地址:
http://github.crmeb.net/u/defu

开源不易,Star 以表尊重,感兴趣的朋友欢迎 Star,提交 PR,一起维护开源项目,造福更多人!

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/知新_RL/article/detail/520149
推荐阅读
相关标签
  

闽ICP备14008679号