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

2022-05-23 10:47:05 浏览数 (1)

一、 Container

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

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

Container.tsx

代码语言:javascript复制
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
复制代码

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

index.tsx

代码语言:javascript复制
import Container from './Container'

export type {ContainerProps} from './Container'
export default Container;
复制代码

然后在 src/index.tsx 导出

代码语言:javascript复制
export type {ContainerProps} from './components/container'
export {default as Container} from './components/container'
复制代码

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

代码语言:javascript复制
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
};
复制代码

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

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

  • 在代码中写 propTypes 自动生成
  • 手动配置

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

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

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

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

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

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

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

代码语言:javascript复制
{
  name: 'direction',
  description: '内容的排列方向',
  setter: {
    componentName: 'RadioGroupSetter',
    initialValue: 'column',
    props: {
      options: [
        'column',
        'row'
      ],
    }
  }
}
复制代码

完整的定义如下:

代码语言:javascript复制
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
};
复制代码

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

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

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

二、Panel 组件

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

右边可配置的属性为:

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

src/components 下新建 panel 目录,并创建 Panel.tsxindex.tsxindex.scss 三个文件

代码语言:javascript复制
// 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;
  }
}
复制代码

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

代码语言:javascript复制
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
};
复制代码

三、Table 组件

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

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

本组件基于 antd 的 Table 扩展。

src/components 目录下新建 Table 文件夹,然后新建 Table.tsxindex.ts 文件

代码语言:javascript复制
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
复制代码
代码语言:javascript复制
import Table, {JTableProps} from './Table'

export type {JTableProps}
export default Table;
复制代码

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

代码语言:javascript复制
export type {JTableProps} from './components/Table';
export {default as Table} from './components/Table'
复制代码

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

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

代码语言:javascript复制
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
};
复制代码

效果如图:

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 就会正常加载,项目也就不报错了。

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

总结

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

  1. src/components 文件夹下新建组件的文件夹,写逻辑代码,定义需要对外暴露的 props 。
  2. 根目录/index.tsx 中注册组件。不注册的话页面上看不到。
  3. 运行 npm run lowcode:dev 命令,会在 根目录/lowcode 目录下自动生成组件的描述文件 meta.ts,简单类型的 props 比如 string、bool 一般没啥问题,如果是复杂类型,比如复杂对象、数组,自动生成的描述可能不是我们想要的,这时需要手动改描述文件。

前两步我们都比较熟悉,重点主要在第三步改描述文件。在页面上对组件进行拖拽、配置时,支持的操作都是由描述文件定义的。描述文件的重点是设置器,一个属性支持怎样的交互,是可以输入文字,还是下拉框,还是可增删的数组,都是由设置器定义的。

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

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

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

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

代码语言:javascript复制
链接: https://pan.baidu.com/s/14G-bpVthImHD4eosZUNSFA?pwd=yu27
提取码: yu27

百度云链接不稳定,随时可能会失效,大家抓紧保存哈。

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

开源地址

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

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

0 人点赞