npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

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

  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 大漠穷秋<[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 组件。
  • 开发组件对应的属性面板。

7. License

MIT licensed.