1

可视化搭建会遇到如下三类容器组件:

  1. 简单容器:以 children 容纳子组件的容器。
  2. 卡片容器:以 props.header 加上 props.header 等多个插槽容纳子组件的容器。
  3. Tab 容器:以 props.tabPanel[x] 等动态数量插槽容纳子组件的容器。

画布本身也是一个容器组件,所以可视化搭建离不开容器。

另一方面,我们应该允许给组件 props 传入 React 组件实例,但组件树是可序列化的 JSON 结构,因此需要一种定义方式,将某些属性转化为 React 组件实例传给组件实例。

容器的定义

任何组件都可能是容器组件,只要它将 props.childrenprops.footer 等任何属性作为 ReactNode 渲染。因此我们不需要特殊声明组件是否为容器,而仅需将某些组件 Key 声明为 ReactNode 节点。

Children

children 因为太常用因此单独强调出来,可以只在在组件实例定义 children 属性,它为是一个数组:

import { ComponentInstance } from "designer";

const componentTree: ComponentInstance = {
  componentName: "div",
  children: [
    {
      componentName: "input",
    },
  ],
};

对于这个组件,Designer 会将 children 定义的属性理解为组件实例,并真正解析为 React 实例传递给 props.children,因此组件渲染代码可以直接使用 children 渲染:

import { ComponentMeta } from "designer";

const divMeta: ComponentMeta = {
  componentName: "div",
  element: ({ children }) => <div>{children}</div>,
};

这种约定的好处是直观自然,组件代码也没有关心到框架逻辑,自然而然实现了容器功能。

treeLike 结构

只要将任意组件 props 定义为数组模式,并且包含 componentName,Designer 就认为应该解析为 ReactNode。

如下面的例子,我们定义的 div 组件初始化就会渲染一个 input 组件在 props.header 位置:

import { ComponentMeta } from "designer";

const divMeta: ComponentMeta = {
  componentName: "div",
  element: ({ header }) => <div>{header}</div>,
  defaultProps: {
    header: [
      {
        componentName: "input",
      },
    ],
  },
};

也可以在描述组件树时直接写在对应 props 位置:

import { ComponentInstance } from "designer";

const componentTree: ComponentInstance = {
  componentName: "div",
  props: {
    header: [
      {
        componentName: "input",
      },
    ],
  },
};

这种约定的好处是直观的支持了任意 props key 为组件实例,但依然存在限制,因此 Designer 还需要支持一种用户 100% 掌控的申明式定义:propTypes

PropTypes

在组件元信息 propTypes 属性定义更细致的容器插槽位置,比如:

const tabMeta = {
  componentName: "tab",
  propTypes: {
    tabs: [
      {
        panel: "element",
      },
    ],
  },
};

那么当组件实例如下定义时:

const componentInstance = {
  componentName: "tab",
  props: {
    tabs: [
      {
        title: "tab1",
        panel: {
          componentName: "card",
        },
      },
      {
        title: "tab2",
        panel: {
          componentName: "text",
        },
      },
    ],
  },
};

组件拿到的 props.tabs[0].panel 就是一个可以直接渲染的 React 组件实例,因为在 propTypes 定义了 tabs[].panel 路径是一个组件实例。

这样设计需要考虑组件树遍历的问题,因为组件实例位置定义在组件元信息上,因此仅靠组件树无法做遍历(因为遍历父节点时,不结合 componentMeta 就无法确认哪些 props 位置是子组件实例),这样会带来两个问题:

  1. 遍历组件非常麻烦,极端情况下,如果大量组件是远程注册的三方组件,会导致需要一层层串行远程拉取组件实例,导致遍历过程变慢。
  2. 更极端的场景是,当组件版本升级导致 propTypes 变化,一些原本不是组件实例的位置成为了组件实例,或者反之,此时拉取最新组件元信息读取的 propTypes 可能就是错的。

因为以上两个原因,实现方案应该是将组件元信息定义的 propTypes 拷贝一份到组件实例,这样就可以仅凭组件树自身来遍历组件树了,而且定义在组件树上的 propTypes 一定对应当前组件树的结构。

总结

我们通过 children 与 props 上 treeLike 这两个约定,实现了业务基本够用的容器定义能力,仅凭这两个约定就可以实现几乎所有容器需要的效果。

propTypes 定义补全了约定拓展性的不足,让 props 任何位置都可能成为组件实例,只需要付出额外定义 propTypes 的代价。

阅读到这,相信你已经理解到,可视化搭建其实不存在容器组件的概念,因为这个组件之所以是容器,仅仅因为它的某个 prop 属性是组件实例,而它恰好将该属性渲染到某个位置(甚至用 createPortal 挂载到其他 dom 节点),所以它仅仅是一种 prop 属性的体现,因此对容器组件,我们没有设计一种新 type,而是允许任意位置属性定义为实例。

下一节我们会介绍为组件元信息添加取数与筛选联动的钩子,让筛选器 + 查询场景可以轻松被实现。

讨论地址是:精读《容器组件设计》· Issue #468 · dt-fe/weekly

如果你想参与讨论,请 点击这里,每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。

关注 前端精读微信公众号

<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">

版权声明:自由转载-非商用-非衍生-保持署名(创意共享 3.0 许可证

黄子毅
7k 声望9.5k 粉丝