关于javascript:组件注册与画布渲染

2次阅读

共计 4723 个字符,预计需要花费 12 分钟才能阅读完成。

接着可视化搭建的实践形象,咱们开始勾画一个具体的 React 可视化搭建器。

精读

如果咱们将可视化搭建整体定义为 <Designer>,那么 API 可能是这样的:

<Designer componentMetas={[]} componentTree={} />
  • componentMetas: 定义组件元信息的数组。
  • componentTree: 定义组件树结构。

只有注册了组件元信息与组件树,可视化搭建的画布就能够渲染进去了,这很好了解。

咱们先看组件树如何定义:

组件树

组件树里有各组件的实例,那么最好的设计是,组件树与组件实例构造是同构的,称为 ComponentInstance – 组件实例:

{
  "componentName": "container",
  "children": [
    {
      "componentName": "text",
      "props": {"name": "我是一个文本组件"}
    }
  ]
}

下面的构造既能够当做单个组件的 组件实例信息 ,也能够认为是一个 组件树,也就是组件树的任何组件节点都能够拎进去成为一个新组件树,这就是同构的含意。

咱们定义了最最根底的组件树结构,当前所有性能都基于这三个因素来拓展:

  • componentName: 组件名,形容组件类型,比方是个文本、图片还是表格。
  • props: 该组件实例的所有配置信息,透传给组件 props
  • children: 子组件,类型为 ComponentInstance[]

每一个概念都不可或缺,让咱们从概念必要性再剖析一下这三个属性:

  • componentName: 必须领有的属性,否则怎么渲染该节点都无从谈起。所以相应的,咱们须要组件元信息来定义每个组件名应该如何渲染。
  • props: 即使是雷同组件名的不同实例,也可能领有不同配置,这些配置放在 props 里足够了,没必要开额定的其余属性存储各种各样的业务配置。
  • children: 实践上能够合并到 props.children,但因为子组件概念太常见,倡议 childrenprops.children 这两种地位同时反对,同时定义时,前者优先级更高。

除此之外,还有一个可选属性 componentId,即组件惟一 ID。咱们从可选性与必要性两个角度剖析一下这个属性:

  • componentId 的可选性:组件实例在 组件树的门路 就是人造的组件惟一 ID,比方下面的文本组件的组件惟一 ID 能够认为是 children.0
  • componentId 的必要性:用组件树门路代替组件惟一 ID 的害处是,组件在组件树上挪动后其唯一性就会隐没,此时就要用上 componentId 了。

一个好的可视化搭建实现是反对 componentId 的可选性。

组件元信息

接着下面说的,至多要定义一个组件名是如何渲染的,所以组件元信息(ComponentMeta)的必要构造如下:

const textMeta = {
  componentName: "text",
  element: ({name}) => <span>{name}</span>,
};
  • componentName: 定义哪个组件名的元信息。
  • element: 该组件的渲染函数。

实现这些最根底性能后,尽管该可视化搭建器没有人任何实质性的性能,但至多实现了一个外围根底工作:将组件树结构的形容与实现离开了。哪怕当前什么性能也不再减少,也永恒的扭转了开发模式,咱们须要先定义组件元信息,再将其搁置在组件树上。

对于画板工具软件,如果不思考布局等简单的画布性能,该构造形容足以实现大部分工作的技术形象:配置面板批改组件实例的 props 属性,甚至布局地位也能够存储在 props 上。

对于 element 的命名,可能会产生分歧,比方还有其余命名格调如 renderrendererreactNode 等等,但不论叫什么名字,只有是基于 React 响应式定义的,最终应该都必由之路,最多对于各类 Key 的名称定义有所不同,这块能够保留本人的观点。

咱们持续聚焦组件元信息的 element 属性,看以下 element 代码:

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

下面的例子中,咱们能够辨认出 childrenheader 类型吗?能够辨认一部分:

  • children: 肯定是 React 实例,能够是一个或多个组件实例。
  • header: 可能是数字、字符串,也可能是 React 实例。

props.children 对应了 componentInstance.children 形容,那么如何辨认 header 是一个一般对象还是 React 实例呢?

Props 上的 ComponentTreeLike 属性

ComponentTreeLike 指的是:组件 props 属性上,辨认出“像组件实例的属性”,并将其转换为真正的组件实例传给组件。

假如一个失常的 props.header 值为 "some text",那么组件 props 理论拿到的 props.header 值也是字符串 "some text"

{
  "componentName": "div",
  "props": {"header": "some text"}
}
const divMeta = {
  componentName: "div",
  element: ({header}) => (
    <div>
      {header} {/** 字符串 "some text" */}
    </div>
  ),
};

如果将 props.header 写成类 children 构造,可视化搭建框架就会辨认为组件实例,将其转化为真正的 React 实例再传给组件:

{
  "componentName": "div",
  "props": {
    "header": [
      {"componentName": "text"}
    ]
  }
}
const divMeta = {
  componentName: "div",
  element: ({header}) => (
    <div>
      {header} {/** React 组件实例,此时会渲染出组件实例 */}
    </div>
  ),
};

这样设计是基于一个准则:组件树应该能形容出任何组件想要的 props 属性。咱们反过来站在 element 角度来看,假如你注入了一个 Antd 等框架组件,如果在不改一行源码的状况下,就心愿接入平台,那平台必须满足可配置出任何 props 的能力。除了根底变量外,更简单的还有 React 组件实例与函数,当初咱们解决了传组件实例的问题,至于如何传函数,咱们下一大节再讲。

这样设计存在两个缺点:

  1. 因为 ComponentTreeLike 会主动转成实例,所以没有方法让组件拿到 ComponentTreeLike 的原始值。
  2. 因为 ComponentTreeLike 地位不确定,为了防止深层解析产生的性能损耗,只解析 props 的第一级节点会导致嵌套层级较深的 ComponentTreeLike 无奈被解析到。

如果要解决这两个缺点,就须要在组件元信息上定义 Props 的类型,比方:

const divMeta = {
  componentName: "div",
  propsType: {
    header: "element",
    content: ["element"],
    tabs: [
      {panel: "element",},
    ],
  },
};

解释一下下面的例子代表的含意:

  • header: 是单个 React Element。
  • content: 是 React Element 数组。
  • tabs: 是一个数组构造,每一项是对象,其中 panel 是 React Element。

这样配合以下组件树的形容,就能够准确的将对应 element 类型转化为组件实例了,而对于根本类型 primitive 放弃原样传给组件:

{
  "componentName": "div",
  "props": {
    "header": {"componentName": "text"},
    "names": ["a", "b", "c"],
    "content": [
      {"componentName": "text"},
      {"componentName": "text"}
    ],
    "tabs": [
      {
        "title": "tab1",
        "panel": {"componentName": "text"}
      }
    ]
  }
}

如此一来,没有定义为 Element 的属性不会解决成 React 实例,第一个问题就天然解决了。通过配置更深层嵌套的构造,第二个问题也天然解决。

componentMeta.propsType 之所以不采纳 JSONSchema 构造,是因为框架没必要内置对 props 类型校验的能力,这个能力能够交给业务层来解决,所以这里就能够采纳简化版构造,不便书写,也容易浏览。

留神:propsType{} 示意 value 是对象,而 [] 示意 value 是数组。为数组时,仅反对单个子元素,因为单项即是对数组每一项类型的定义。

给组件注入函数

当初曾经能给 componentMeta.element 传入任意根底类型、React 实例的 props 了,当初还缺函数类型或者 Set、Map 等简单类型问题须要解决。

因为组件树结构须要序列化入库,所以必须为一个能够序列化的 JSON 构造,而这个构造又须要裸露给开发者,所以也不适宜定义一些 hack 的序列化、反序列化规定。因而要给组件 props 注入函数,须要定义在组件元信息上,因为其定义了额定的 props 属性,且不在组件树中,所以咱们将其命名为 runtimeProps:

const divMeta = {
  componentName: "div",
  runtimeProps: () => ({onClick: () => {console.log('click') }
  })
  element: ({onClick}) => (<button onClick={onClick}>
      点击我
    </button>
  ),
};

点击按钮后,会打印出 click。这是因为 runtimeProps 定义了函数类型 onClick 在运行时传入了组件 props。

当组件树与 componentMeta.runtimeProps 同时定义了同一个 key 时,runtimeProps 优先级更高。

总结

本节咱们介绍了组件注册与画布渲染的根底内容,咱们再从新梳理一下。

首先定义了 <Designer /> API,并反对传入 componentTreecomponentMetas,有了组件树与组件元信息,就能够实现可视化搭建画布的渲染了。

咱们还介绍了如何在组件元信息定义组件的渲染函数,如何给渲染函数 props 传入根本变量、React 实例以及函数,让渲染函数能够对接任何成熟的组件库,而不须要组件库做任何适配工作。

但这只是可视化搭建的第一步,在真正开始做我的项目后,你还会遇到越来越多的问题,比方除了渲染画布,还要在业务层定义属性配置面板、组件拖拽列表、图层列表、撤销重做等等性能,这些性能如何拿到画布属性?如何与画布交互?runtimeProps 如何基于我的项目数据流给组件注入不同的属性或函数?如何依据组件 props 的变动动静注入不同函数?如何保障注入的函数援用不变?

要解决这些问题,须要在本章的根底上实现一套零碎的数据流规定以及配套 API,这也是下一讲的内容。

探讨地址是:精读《组件注册与画布渲染》· Issue #464 · dt-fe/weekly

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

关注 前端精读微信公众号

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

版权申明:自在转载 - 非商用 - 非衍生 - 放弃署名(创意共享 3.0 许可证)

正文完
 0