1.0.2 • Published 2 years ago

@whalecloud/react-codeless-designer v1.0.2

Weekly downloads
-
License
MIT
Repository
-
Last release
2 years ago

React Codeless Designer( 缩写成 RCD ) 是一个轻量级的页面设计器,不与任何服务端技术强绑定,对现有前端代码完全没有入侵性,可以随意集成到任意系统中。

1.启动实例项目

实例项目 react-codeless-designer-demo 用来全面演示 RCD 的各项特性。

看到以下界面说明启动成功了:

2.获取 RCD 源码

你可以按照以下步骤获取并启动 RCD 自身的源代码工程:

源码目录结构:

├───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 大漠穷秋<damoqiongqiu@126.com>
 */
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: 'damoqiongqiu@126.com',
    },
    {
      key: '2',
      name: 'Jim Green',
      email: 'damoqiongqiu@126.com',
    },
    {
      key: '3',
      name: 'Joe Black',
      email: 'damoqiongqiu@126.com',
    },
  ];

  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 内置了以下组件:

  1. 布局型组件: MRow/MCol
  2. 通用型基础组件: MText/MImg/MVideo/MIframe
  3. 图形组件: 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 大漠穷秋<damoqiongqiu@126.com>
 */
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 组件。
  • 开发组件对应的属性面板。

7. License

MIT licensed.