@whalecloud/react-codeless-designer
v1.0.2
Published
An approach to build a Codeless Designer with React, React-DND and Ant-Design.
Downloads
5
Readme
React Codeless Designer( 缩写成 RCD ) 是一个轻量级的页面设计器,不与任何服务端技术强绑定,对现有前端代码完全没有入侵性,可以随意集成到任意系统中。
1.启动实例项目
实例项目 react-codeless-designer-demo 用来全面演示 RCD 的各项特性。
- 第一步:git clone https://gitee.com/react-codeless-designer/react-codeless-designer-demo.git
- 第二步:在你的 VSCode 中打开 react-codeless-designer-demo 目录
- 第三步:在你的控制台中进入项目根目录,用 npm 安装依赖, npm install (或者 yarn 安装)
- 第四步:npm start 启动项目
- 第五步:打开你的浏览器,访问 http://localhost:3000/design
看到以下界面说明启动成功了:
2.获取 RCD 源码
你可以按照以下步骤获取并启动 RCD 自身的源代码工程:
- 第一步:git clone https://gitee.com/react-codeless-designer/react-codeless-designer
- 第二步:在 VSCode 中打开 react-codeless-designer 目录
- 第三步:打开控制台,用 npm 安装依赖, npm install (或者 yarn )
- 第四步:npm start 启动项目
源码目录结构:
├───assets //静态资源,目前只有图标
│ └───imgs
├───core //内核代码,主要包括 MagicBox、DropZone、 Desinger、 Renderer 这几个核心类
│ ├───designers
│ ├───magic-box
│ └───renderers
├───design-view //组装好的完整设计器
│ ├───dd-area
│ ├───property-panel-container
│ ├───side-bar
│ └───top-bar
├───m-components //内置的“M系列”组件
│ ├───common-layout
│ ├───h5
│ └───pc
├───property-panels //内置的属性配置面板
└───utils //一些工具函数,主要用来遍历和操作树形结构
RCD 是 MIT 的 License ,你可以随意修改并发布自己的版本。
- 构建发布包的命令:npm run build-prod
- 发布到 npm 的命令:npm publish
3.在你的项目中引入 RCD
3.1 安装依赖
如果你需要在现有项目中使用 RCD ,只要安装 node 依赖即可:
npm i react-codeless-designer --save
我们创建一个全新的 React 项目来示范如何引入 RCD 。
npx create-react-app react-codeless-designer-demo
cd react-codeless-designer-demo
npm i react-codeless-designer --save
OK,这样就可以使用 RCD 暴露出来的所有组件了。
3.2 设计视图 DesignView 的用法
RCD 内置了一个设计视图 DesignerView 组件,设计器的所有功能都被封装在这个巨大的组件里面,包括:
- 顶部工具条 TopBar
- 左侧组件列表 LeftSideBar
- 中间设计区 DDArea(Drag&Drop Area)
- 右侧属性配置面板 PropertyPanel
使用 DesignView 的方式与使用普通的 React 组件完全一致,虽然它的外观非常“巨大”:
<AcceptTypesProvider value={{ acceptTypes: this.state.acceptTypes }}>
<ComponentTypeProvider value={{ componentTypeMap, propertyPanelMap, iconObj }}>
<ComponentTypeConsumer>
{componentTypeContext => {
return <DesignView {...tempProps} {...componentTypeContext} />;
}}
</ComponentTypeConsumer>
</ComponentTypeProvider>
</AcceptTypesProvider>
DesignerView 有几个核心参数:
- 页面数据 pageData:用来存储和加载设计的结果,这是一份巨大的 JSON 结构。
- 左侧的组件列表定义 leftSiderBarConfig 。
- 组件 type 和组件构造函数映射表 componentTypeMap 。
- 组件 type 和属性面板构造函数映射表 propertyPanelMap 。
- 组件类型数组 acceptTypes ,这个数组用来定义 DropZone(空降区) 可以接收什么类型的组件。利用这个特性,使用者可以动态决定某个容器型的组件内部能放置哪些类型的组件。
为了方便使用者集成自定义组件,RCD 利用 React 的 Context 机制,提供了两个 Provider 来辅助工作:
- AcceptTypesProvider
- ComponentTypeProvider
使用者可以向这些 Provider 中动态加入组件和属性面板。
3.3 监听 TopBar 小按钮的事件
DesignerView 右上角的所有小按钮都暴露了自己的回调函数:
<TopBar
designer={this.state.designer}
undo={this.undo.bind(this)}
redo={this.redo.bind(this)}
onViewData={this.onViewData.bind(this)}
onDelComponent={this.onDelComponent.bind(this)}
onPreview={this.onPreview.bind(this)}
onSave={this.onSave.bind(this)}
onPublish={this.onPublish.bind(this)}
onSwitchDesigner={this.onSwitchDesigner.bind(this)}
onHelp={this.onHelp.bind(this)}
/>
外部应用可以传入以下回调(目前共有 8 个):
- undo
- redo
- onViewData
- onDelComponent
- onPreview
- onSave
- onPublish
- onHelp
3.4 渲染器 Renderer 的用法
Renderer 的功能是把已经设计好的页面“渲染”出来,由于在渲染时不再需要拖拽交互,所以 Renderer 不需要 AcceptTypesContext 这个参数(传递进来也无害),其它参数与 DesignView 完全一致:
<PCRenderer componentTypeMap={componentTypeContext.componentTypeMap} pageData={pageData} />
3.5 定制渲染器 Renderer 的外观
在某些场景下,我们不想让 PreviewPage 占据整个页面,只想让它作为页面中的一个局部区域展示出来。由于 PreviewPage 本身也是一个 React 组件,所以我们可以把它嵌入到其它组件内部,然后在外层加一些自定义的样式,示例如下:
const NavbarWrapper = styled.div`
background-color: #f7f7f7;
overflow: hidden;
height: 50px;
padding: 12px;
font-size: 18px;
border: 1px solid #a8a8a8;
font-weight: bold;
`;
const ContentWrapper = styled.div`
padding: 0px 100px;
background-color: #a8a8a8;
overflow: hidden;
`;
const FooterWrapper = styled.div`
background-color: #f7f7f7;
overflow: hidden;
height: 200px;
padding: 12px;
font-size: 18px;
border: 1px solid #a8a8a8;
font-weight: bold;
line-height: 176px;
`;
return (
<React.StrictMode>
<NavbarWrapper>这里可以放导航条</NavbarWrapper>
<ContentWrapper>
<PreviewPage deviceType={DeviceType.PC} />
</ContentWrapper>
<FooterWrapper>这里可以放 Footer</FooterWrapper>
</React.StrictMode>
);
展示效果如下:
有了这个机制之后,对于一些页面宽度固定的场景,我们就可以很方便地实现了。例如:一些电商系统会把页面安全宽度设置为 1200px ,可以借助于这一机制来限定渲染器的尺寸,从而与你整个系统的安全宽度保持一致。
4.编写组件和属性配置面板
市面上已经有大量的开源 UI 组件库,各个公司也开发了大量业务组件,RCD 最强大地方是:它可以把现有的任意 React 组件集成进来,而且不需要对现有组件的代码做任何修改。你只需要 3 步就可以把现有的组件集成到 RCD 中:
- 第一步:编写一个新的 “M 组件” 来包裹现有的 React 组件。
- 第二步:在设计器左侧的列表中配置组件对应的图标和元数据。
- 第三步:为你的组件编写一个属性配置面板 PropertyPanel。
这 3 个步骤是高度模式化的,你只要成功做完一个组件,就完整理解了整个过程。
4.1 编写一个包裹组件
在 RCD 中,包裹组件都带一个 M 前缀,M 是 MagicBox 的缩写,加前缀有 3 个作用:
- 第一是命名规范,让别人一看就知道这是一个 RCD 包裹组件。
- 第二是为了避免 type 字段冲突,因为 RCD 在运行时必须借助于这个 type 来获取组件真正的构造函数,从而动态创建出组件的实例。
- 第三是把一些功能封装在 M 组件内部,例如:需要配置的参数,以及到服务端加载数据的逻辑等,这样可以完全避免入侵现有的组件的代码。
RCD 内置的组件也严格遵守以上约定,为了描述起来更方便,我们把带有 M 前缀的组件叫做“M 组件”。
接下来,我们来动手编写一个全新的 React 组件,并通过包裹“M 组件”把它集成到设计视图中。我们要编写的这个组件名字叫做 NiceTable ,它的全部代码只有 27 行,含注释:
import { Table } from 'antd';
import React, { Component } from 'react';
/**
* @class NiceTable
* 这是一个普通的 React 组件,你只要像编写一个普通的 React 组件一样实现它的功能就可以了,没有任何神奇的地方。
* 你自己编写的组件中可以引用任意需要的第三方组件,与正常的 React 组件写法完全相同。
* @author 大漠穷秋<[email protected]>
*/
export default class NiceTable extends Component {
state = {};
/**
* @see https://reactjs.org/docs/react-component.html#static-getderivedstatefromprops
*/
static getDerivedStateFromProps(props, state) {
state = {
columns: props.tableColumns ? props.tableColumns : [],
data: props.tableData ? props.tableData : [],
};
return state;
}
render() {
return <Table columns={this.state.columns} dataSource={this.state.data} />;
}
}
从以上代码可以看到,NiceTable 其实什么都没做,它只是在内部引用了 AntD 的 Table 组件而已。它和你日常写 React 组件的方式完全一致,没有任何神奇的地方。备注:这里不解释 React 基础知识,如果有需要请参考官网上的文档 https://reactjs.org/
为了把 NiceTable 集成到设计视图中,我们需要为它编写一个对应的“M 组件”,按照命名规范,名为 MNiceTable :
MNiceTable 的关键代码如下:
import React from 'react';
import { MagicBox, MBaseComponent } from 'react-codeless-designer';
import NiceTable from './NiceTable';
/**
* @class MNiceTable
*/
export default class MNiceTable extends MBaseComponent {
static type = 'MNiceTable';
//可以在这里提供默认的样式
static defaultStyle = {
paddingTop: 0,
paddingBottom: 0,
paddingLeft: 0,
paddingRight: 0,
};
//默认列定义
static defaultColumns = [
{
title: 'Name',
dataIndex: 'name',
key: 'name',
render: text => <a>{text}</a>,
},
{
title: 'Email',
dataIndex: 'email',
key: 'email',
},
];
//默认数据
static defaultData = [
{
key: '1',
name: 'John Brown',
email: '[email protected]',
},
{
key: '2',
name: 'Jim Green',
email: '[email protected]',
},
{
key: '3',
name: 'Joe Black',
email: '[email protected]',
},
];
static getDerivedStateFromProps(props, state) {
state = {
...state,
...props,
};
state.nodeData = {
...state.nodeData,
tableColumns: MNiceTable.defaultColumns, //Table 组件自定义的 props 加在这里
tableData: props.tableData ? JSON.parse(props.tableData) : MNiceTable.defaultData, //Table 组件自定义的 props 加在这里
style: {
...MNiceTable.defaultStyle,
...(state.nodeData.style ? { ...state.nodeData.style } : {}),
},
};
state = {
...state,
...state.nodeData,
};
return state;
}
render() {
return (
<MagicBox {...this.state}>
<NiceTable tableColumns={this.state.tableColumns} tableData={this.state.tableData}></NiceTable>
</MagicBox>
);
}
}
以上代码中最关键的注意点解释:
- 第一点:MNiceTable 需要继承一个内置的基础类 MBaseComponent,因为 MBaseComponent 中提供了一组工具函数,方便后续的操作。
export default class MNiceTable extends MBaseComponent
如果你不想继承 MBaseComponent 也可以,你只要读懂 MBaseComponent 的设计意图,然后自己重新实现即可。
- 第二点:MNiceTable 必须提供一个 type 属性,static 型的。
static type = 'MNiceTable';
这个 type 属性是必须的,任何 M 组件都必须提供 type 。原因是:React.createElement 需要根据这个 type 查找组件的构造函数。如果你有 Java 开发背景,联想一下 Java 中的“反射”机制就很容易理解了。**请特别注意,这个 type 的值必须是全局唯一的,目的是为了避免组件数量太多时,造成意外的重复。**另外,type 属性一旦确定之后,最好不要修改,因为它会被存储到数据库里面。
- 第三点:我们需要在 MNiceTable 的 render 函数里面做一些小把戏,在原始组件的外层包裹一层 <MagicBox> 标签,就像这样:
<MagicBox {...this.state}>
<NiceTable tableColumns={this.state.tableColumns} tableData={this.state.tableData}></NiceTable>
</MagicBox>
你可以看到,只要包裹一层 <MagicBox> ,NiceTable 就自动获得了拖拽功能。RCD 把拖拽交互相关的逻辑都封装在了 MagicBox 内部,看起来就像魔法一样,这就是为什么它的名字叫做 MagicBox 的原因。
- 第四点:我们还需要做一个动作,才能让 React 通过 type 找到 MNiceTable 的构造函数。在 DesignerPage 中,我们需要把 MNiceTable 的 type 加入到 ComponentTypeProvider 这个“组件类型注册表”中去:
static getDerivedStateFromProps(props, state) {
//省略非关键代码...
let ctmap = { ...componentTypeMap };
ctmap[MNiceTable.type] = MNiceTable;
//省略非关键代码...
let acTypes = [
...internalTypes,
MNiceTable.type,
];
state = {
acceptTypes: [...acTypes],
componentTypeMap: { ...ctmap },
//省略非关键代码...
};
return state;
}
componentTypeMap 是一个简单的 K-V 映射,以组件的 type 的值为 key,以 MNiceTable 本身的构造函数引用为 value 。你可以把 ComponentTypeProvider 看成一个“注册表”,当 RCD 需要创建一个组件的实例时,它就会到这个“注册表”里面来查找对应的构造函数,然后再利用 React.createElement(...) 把组件的实例真正构造出来。同样地,如果你有 Java 开发背景,联想一下 Java 中的“反射”机制就很容易理解了。在 RendererPage 里面也需要做完全相同的配置,这样渲染器 Renderer 在加载到页面数据之后才能顺利创建出组件的实例。
4.2 配置组件对应的图标和类型信息
请打开 pc-side-bar-icons.js ,新增配置:
import { iconObj } from 'react-codeless-designer';
const pcComponentList = [
//省略不相关代码...
{
moduleId: 64,
moduleCatgId: 4,
category: 'My Component',
label: 'NiceTable',
type: 'MNiceTable',
icon: null,
},
//省略不相关代码...
];
export default pcComponentList;
如上,这是一个普通的 JS 对象,里面描述了组件应该如何展现在 Designer 左侧的图标列表中。在这里的数据结构中,只有 2 个 key 是必不可少的, labe 和 type ,其它都是可选的。RCD 已经把自己内置的组件都配置在默认数组中,如果你不想使用它们,只要从这里把它们删掉,就不会出现在左侧的图标列表中了。当然,在真实的业务场景中,你可能需要把这里的配置以 JSON 的格式存储到数据库中去,然后在打开设计器的时候再加载到页面上。做完以上配置,你就可以在 Designer 左侧的图标列表中看到 MNiceTable 组件的图标了:
如果不给组件提供图标,RCD 会使用内置的默认图标。图标出现在列表中之后,就可以把 MNiceTable 拖到中间的设计区了:
组件出来之后,我们还需要为它编写一个对应的属性配置面板,这样用户才能动态地修改它的各种参数。
4.3 编写属性配置面板 PropertyPanel
编写 PropertyPanel 的过程也是高度模式化的,只要成功写完一个,后面就是工作量的问题了。PropertyPanel 本身也是一个普通的 React 组件,只要像编写普通组件那样去写就可以了。这里不把 NiceTablePropertyPanel 完整代码贴进来,完整可运行的代码已经包含在示例项目中,这里只把一些必要的注意点描述如下:
- 第一点:强烈建议所有属性配置面板都继承 MPropertiesBasePanel 这个基类,因为 MPropertiesBasePanel 里面提供了一组常用的工具函数。
export default class NiceTablePropertyPanel extends MPropertiesBasePanel
- 第二点:在 MPropertiesBasePanel 中修改完了组件的属性之后,需要触发一个 RCD 自定义的事件 PROPERTY_UPDATE 来通知设计器去更新组件的状态。
onSaveProperties() {
//TODO: Validate form values before triggering the event.
const mergedCommonStyles = MPropertiesBasePanel.mergeStyles(this.state.commonStyleFields);
const mergedTextStyles = MPropertiesBasePanel.mergeStyles(this.state.tableStyleFields);
const mergedOtherFields = MPropertiesBasePanel.mergeOtherFields(this.state.tableOtherFields);
const evt = new Event(EventNames.PROPERTY_UPDATE, { bubbles: false });
evt.data = {
...this.state,
style: {
...this.state.style,
...mergedCommonStyles,
...mergedTextStyles,
},
...mergedOtherFields,
};
window.document.dispatchEvent(evt);
message.success('组件 state 已更新');
}
注意:事件名必须是 EventNames.PROPERTY_UPDATE ,如果使用其它事件名, RCD 收不到事件。
DesignerView 内部是这样处理的:
window.document.addEventListener(EventNames.PROPERTY_UPDATE, this.propertyUpdateHandler.bind(this));
当 DesignerView 接受到事件之后,它会根据组件的 id 去 pageData 中查找并修改组件对应的状态,然后利用 React 的 setState 机制来刷新 DOM 。
- 第三点:需要把 NiceTablePropertyPanel 注册到设计器中去,让 RCD 能找到它的构造函数。
static getDerivedStateFromProps(props, state) {
//省略非关键代码...
let ppmap = { ...propertyPanelMap };
ppmap[MNiceTable.type] = NiceTablePropertyPanel;
//省略非关键代码...
state = {
//省略非关键代码...
propertyPanelMap: { ...ppmap },
//省略非关键代码...
};
return state;
}
可以看到,这里的“属性面板注册表”与“组件类型注册表”机制是类似的,以组件的 type 为 key ,以属性面板的构造函数为 value 。
4.4 试试最终效果
做完以上配置之后,在设计区点击 MNiceTable 的时候,RCD 就会自动把对应的 NiceTablePropertyPanel 实例创建出来并展示在右侧:
在属性面板中可以修改 MNiceTable 的各种属性,点击 Save 按钮,可以看到设计区中的表格自动更新了自己的数据。点击右上角的预览按钮(小眼睛),可以看到最终渲染出来的页面。
5.RCD 的内置组件
对于任意 web 页面来说,一些基础组件的使用频率非常高,所以 RCD 内置了以下组件:
- 布局型组件: MRow/MCol
- 通用型基础组件: MText/MImg/MVideo/MIframe
- 图形组件: MChart
内置组件的继承结构:
如上图,在 RCD 内部,两个 Designer 也继承自 MBaseComponent,在“架构篇”中会解释实现细节。
5.1 布局型组件
PC 端的屏幕比较大,2 列、3 列 的布局比较常见:
为了适应这种场景,RCD 内置了 MRow/MCol 两个组件,利用它们可以进行多列布局。MRow/MCol 支持无限嵌套,可以构造出非常复杂的布局模式。
RCD 的“行列”布局思想来自于 Bootstrap :
如果你使用过 Bootstrap 的“网格布局”,理解 MRow/MCol 会非常自然流畅,因为概念模型是完全一样的。
MCol 支持按照比例进行宽度分割,例如:
如果你不想使用内置的 MRow/MCol 机制,完全没有问题,只要把它们从左侧的组件列表中删除掉就可以了,对核心功能没有任何影响。你还可以实现自己的布局组件,把 RCD 内置的 MRow 和 MCol 这两个 key 覆盖成你自己的构造函数,这样 RCD 就会在运行时创建你的组件实例了。
移动端的屏幕比较小,在 UI 设计上一般都做成单列布局,所以默认的例子直接把 MRow/MCol 都注释掉了,移动端不需要它们:
5.2 通用型基础组件
对于任意 web 页面来说,最基础的要素是:文本、图片、视频,所以 RCD 内置了 MText/MImg/MVideo 。这些基础组件都是基于原生的 HTML 标签封装的,没有特别复杂的功能。如果你需要更加高级的功能,比如 Image Gallery 这样的功能,可以参考 react-codeless-designer-demo 中的示例。
RCD 内置了一个 MIframe 组件,用来支持一些特殊的场景。目前,在日常的开发过程中,iframe 的使用量已经很少。但是在一些特殊的场景下,我们还是不得不使用它,比如:一些技术栈完全不同的页面需要集成在一起,不得不使用 iframe 来隔离不同的运行环境。
5.3 图形组件
在实际的业务工程中,图表也是高频需求,所以 RCD 内置了一个 MChart 组件,底层使用了 echarts 和 echarts-for-react 。以下示例项目给了一个非常粗暴的实现方式:把整个 ECharts 的 options 参数全部以 JSON 的方式暴露了出来:
这样我们就可以动态地随意修改 ECharts 的各种配置了,比方说可以把整个 options 改成这样:
这种方式的好处是,只要一个内置的 MChart 组件,就可以实现各种形式的图形,因为 ECharts 的图表只有一个巨型的 options 参数。
注意:这个例子只是为了演示 MChart 组件的功能,在真实的业务系统中,你可能需要像其它 PropertyPanel 那样,把需要修改的属性抽出来做成表单,而不是直接把整个 options 全部暴露给用户。
5.4 RCD 内置的 PropertyPanel
RCD 已经为所有内置组件提供了对应的属性面板:
如果这些属性面板已经能够满足你的需求,可以直接使用它们。如果不能满足,你有两种方法来进行处理:
- 第一种:把内置的组件从左侧的组件列表中删除掉,然后自己从零进行编写,编写的方法可以参考内置属性面板的源码。
- 第二种:你可以编写一个 type 名称相同的组件和属性面板,然后注册到 AcceptTypesProvider 和 ComponentTypeProvider 中去,这样就可以把内置的组件给覆盖掉,具体实现的方法将在下一节中给出。
5.5 覆盖内置的 PropertyPanel
这一小节解决这样一个问题:我想使用内置的 MText 组件,但是我不想使用内置的 TextPropertiesPanel ,而是想使用自定义的属性面板。
MText 内置的属性面板是这样的:
我们希望修改一些局部功能,改成这样:
对于这样局部的定制化,可以这样来实现:
- 第一步:自己编写一个 TextPropertiesPanel 面板,对于不需要改变的功能,直接拷贝内置组件的代码,需要改变的部分进行修改。
- 第二步:向 ComponentTypeProvider 注册一个相同的 type ,覆盖“属性面板注册表”中的配置,指向我们自己编写的属性面板。
后续的实例中会继续使用这个实现来完成一些高级功能。
5.6 编写自己的 TextPropertiesPanel 面板
在我们的业务工程中创建自己的 TextPropertiesPanel 组件,文件路径如下:
我们自己编写的 TextPropertiesPanel 组件源码大多数从内置组件拷贝而来,为了节省篇幅,这里不全部展示,完整可运行的代码已经放在实例项目中,链接路径在本章末尾,这里关键的修改之处有两个配置项:
<Form.Item
name="textLink"
label="选择页面"
rules={[
{
required: true,
message: '请选择选择页面',
},
]}
>
<Select placeholder="选择页面" allowClear={false}>
<Select.Option value="51">测试页面1</Select.Option>
<Select.Option value="52">测试页面2</Select.Option>
</Select>
</Form.Item>
<Form.Item label="新窗口" name="newWindow" valuePropName="checked">
<Switch />
</Form.Item>
覆盖属性面板注册表中的配置
这一步最关键,在 DesignerPage 中,关键的修改之处如下:
import TextPropertiesPanel from 'custom-property-panel/pc/TextPropertiesPanel';
//省略其它 import...
static getDerivedStateFromProps(props, state) {
//省略非关键代码...
let ppmap = { ...propertyPanelMap };
ppmap[MNiceTable.type] = NiceTablePropertyPanel;
ppmap[MForm.type] = MFormPropertyPanel;
ppmap[MTable.type] = MTablePropertyPanel;
ppmap[MText.type] = TextPropertiesPanel; //通过同名的 type 覆盖内置的 TextPropertiesPanel ,指向我们自己的实现
//省略非关键代码
state = {
...state,
acceptTypes: [...acTypes],
componentTypeMap: { ...ctmap },
propertyPanelMap: { ...ppmap },
iconObj,
};
return state;
}
以上代码中,我们首先 import 自己编写的 TextPropertiesPanel ,然后把同名的 MText.type 指向这个自定义的 TextPropertiesPanel 组件。
在 RCD 内部, propertyPanelMap (属性面板注册表)的代码是这样的:
export const propertyPanelMap = Object.fromEntries(
new Map([
[MRow.type, RowPropertiesPanel],
[MCol.type, ColPropertiesPanel],
[MImg.type, ImgPropertiesPanel],
[MText.type, TextPropertiesPanel],
[MVideo.type, VideoPropertiesPanel],
[MChart.type, ChartPropertiesPanel],
[PCDesigner.type, DesignerPropertiesPanel],
[PhoneDesigner.type, DesignerPropertiesPanel],
[MIframe.type, IframePropertyPanel],
]),
);
所有当我们增加了下面这行代码之后,根据 ES6 展开操作符的语法规则,同名的 MText.type 就被覆盖成了我们自己编写的 TextPropertiesPanel :
ppmap[MText.type] = TextPropertiesPanel;
内部的机制是: 当我们在设计区中点击 MText 组件时,RCD 就会根据 MText.type 去 propertyPanelMap 中查找对应的构造函数,然后用 React.createElement() 创建出实例。所以,只要我们把“属性面板注册表”中的映射关系改掉,指向我们自己的构造函数,RCD 创建出来的就是我们自己的组件了。
完成以上两步之后,来看最终效果:
可以看到,当我们再次点击 MText 组件的时候,右侧已经变成了我们自己编写的属性面板。
6.把开源组件集成到 RCD 中
在很多场景中,你可能并不想从零开始编写自己的组件,只想把第三方开源的组件直接集成到设计视图中来。实现的方法与上面的内容完全一致,只要在你的业务工程中给开源组件包装一个“M 组件”即可。
例如,如果你想把 antd-mobile 中的导航组件 NavBar 集成进来,这样实现即可:
import { Icon, NavBar } from 'antd-mobile';
import React from 'react';
import { MagicBox, MBaseComponent } from 'react-codeless-designer';
/**
* @class MNavBar
* @author 大漠穷秋<[email protected]>
*/
export default class MNavBar extends MBaseComponent {
/**
* @required
* React 会根据 type 动态创建组件的实例,type 会被持久化,必须全局唯一。
* type 确定之后不可修改,否则 React.createElement 无法创建实例。
*/
static type = 'MNavBar';
static defaultStyle = {
width: '100%',
paddingTop: 0,
paddingBottom: 0,
paddingLeft: 0,
paddingRight: 0,
};
/**
* @see https://reactjs.org/docs/react-component.html#static-getderivedstatefromprops
*/
static getDerivedStateFromProps(props, state) {
state = {
...state,
...props,
};
state.nodeData = {
...state.nodeData,
style: {
...MNavBar.defaultStyle,
...(state.nodeData.style ? { ...state.nodeData.style } : {}),
},
};
state = {
...state,
...state.nodeData,
};
return state;
}
render() {
return (
<MagicBox>
<NavBar
mode="dark"
leftContent="Back"
rightContent={[<Icon key="0" type="search" style={{ marginRight: '16px' }} />, <Icon key="1" type="ellipsis" />]}
>
NavBar
</NavBar>
</MagicBox>
);
}
}
MNavBar 的图标配置和属性面板编写方式和上面完全相同。
可以看到,RCD 的这种方式非常灵活,它对你现有的组件库没有入侵性,任何 React 组件都可以集成进来。在使用 RCD 的过程中,开发者需要花最多的时间来做两件事:
- 开发业务组件和对应的 M 组件。
- 开发组件对应的属性面板。