JS前端组件注册与画布渲染实例

 更新时间:2023年02月06日 14:26:11   作者:黄子毅  
这篇文章主要为大家介绍了JS前端组件注册与画布渲染实例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

引言

接着可视化搭建的理论抽象,我们开始勾勒一个具体的 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 组件实例与函数,现在我们解决了传组件实例的问题,至于如何传函数,我们下一小节再讲。

这样设计存在两个缺陷:

  • 由于 ComponentTreeLike 会自动转成实例,所以没有办法让组件拿到 ComponentTreeLike 的原始值。
  • 由于 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,这也是下一讲的内容,更多关于JS组件注册画布渲染的资料请关注脚本之家其它相关文章!

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

相关文章

  • JavaScript中ES6 Babel正确安装过程

    JavaScript中ES6 Babel正确安装过程

    这篇文章主要介绍了JavaScript中ES6 Babel正确安装过程的相关资料,需要的朋友可以参考下
    2016-07-07
  • 基于Require.js使用方法(总结)

    基于Require.js使用方法(总结)

    下面小编就为大家带来一篇基于Require.js使用方法(总结)。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-10-10
  • 谈谈JavaScript中浏览器兼容问题的写法小议

    谈谈JavaScript中浏览器兼容问题的写法小议

    本篇文章主要介绍了JavaScript中浏览器兼容问题的写法小议,具有一定的参考价值,感兴趣的小伙伴们可以参考一下。
    2016-12-12
  • postman+json+springmvc测试批量添加实例

    postman+json+springmvc测试批量添加实例

    下面小编就为大家分享一篇postman+json+springmvc测试批量添加实例,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2018-03-03
  • es7中的async、await使用方法示例详解

    es7中的async、await使用方法示例详解

    async、await是es7里面的新语法,async申明一个function是异步,而await等待一个异步方法执行完成。替代promise中的then。async函数返回一个Promise对象,当函数执行的时候,一旦遇到await就会先返回,等到触发的异步操作完成,再接着执行函数体内后面的语句。
    2023-01-01
  • JS判断是否长按某一键的方法

    JS判断是否长按某一键的方法

    这篇文章主要介绍了JS判断是否长按某一键的方法,涉及JavaScript针对键盘按键的判断及响应操作技巧,需要的朋友可以参考下
    2016-03-03
  • JS for in遍历对象顺序不对解决办法

    JS for in遍历对象顺序不对解决办法

    最近使用for-in语句遍历对象属性时发现遍历顺序并非属性构建顺序,这篇文章主要给大家介绍了关于JS for in遍历对象顺序不对的解决办法,需要的朋友可以参考下
    2023-11-11
  • JavaScript定义全局对象的方法示例

    JavaScript定义全局对象的方法示例

    这篇文章主要介绍了JavaScript定义全局对象的方法,结合实例形式分析了javascript全局对象的简单定义流程与实现技巧,需要的朋友可以参考下
    2017-01-01
  • js DOM的学习笔记

    js DOM的学习笔记

    DOM是Document Object Model文档对象模型的缩写;使用JavaScript操作DOM进行DHTML开发
    2011-12-12
  • js获取页面description的方法

    js获取页面description的方法

    这篇文章主要介绍了js获取页面description的方法,涉及javascript操作页面元素的相关技巧,需要的朋友可以参考下
    2015-05-05

最新评论