可视化搭建会遇到如下三类容器组件:
- 简单容器:以
children
容纳子组件的容器。 - 卡片容器:以
props.header
加上props.header
等多个插槽容纳子组件的容器。 - Tab 容器:以
props.tabPanel[x]
等动态数量插槽容纳子组件的容器。
画布本身也是一个容器组件,所以可视化搭建离不开容器。
另一方面,我们应该允许给组件 props 传入 React 组件实例,但组件树是可序列化的 JSON 结构,因此需要一种定义方式,将某些属性转化为 React 组件实例传给组件实例。
容器的定义
任何组件都可能是容器组件,只要它将 props.children
或 props.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 位置是子组件实例),这样会带来两个问题:
- 遍历组件非常麻烦,极端情况下,如果大量组件是远程注册的三方组件,会导致需要一层层串行远程拉取组件实例,导致遍历过程变慢。
- 更极端的场景是,当组件版本升级导致
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 许可证)
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。