diff --git a/readme.md b/readme.md index 28f1e272..948c50c4 100644 --- a/readme.md +++ b/readme.md @@ -6,7 +6,7 @@ 前端界的好文精读,每周更新! -最新精读:268.精读《如何抽象可视化搭建》 +最新精读:269.组件注册与画布渲染 素材来源:[周刊参考池](https://github.com/ascoders/weekly/issues/2) @@ -301,7 +301,8 @@ ### 可视化搭建 -- 268.精读《如何抽象可视化搭建》 +- 268.如何抽象可视化搭建 +- 269.组件注册与画布渲染 ### SQL diff --git "a/\345\217\257\350\247\206\345\214\226\346\220\255\345\273\272/268.\347\262\276\350\257\273\343\200\212\345\246\202\344\275\225\346\212\275\350\261\241\345\217\257\350\247\206\345\214\226\346\220\255\345\273\272\343\200\213.md" "b/\345\217\257\350\247\206\345\214\226\346\220\255\345\273\272/268.\345\246\202\344\275\225\346\212\275\350\261\241\345\217\257\350\247\206\345\214\226\346\220\255\345\273\272.md" similarity index 100% rename from "\345\217\257\350\247\206\345\214\226\346\220\255\345\273\272/268.\347\262\276\350\257\273\343\200\212\345\246\202\344\275\225\346\212\275\350\261\241\345\217\257\350\247\206\345\214\226\346\220\255\345\273\272\343\200\213.md" rename to "\345\217\257\350\247\206\345\214\226\346\220\255\345\273\272/268.\345\246\202\344\275\225\346\212\275\350\261\241\345\217\257\350\247\206\345\214\226\346\220\255\345\273\272.md" diff --git "a/\345\217\257\350\247\206\345\214\226\346\220\255\345\273\272/269.\347\273\204\344\273\266\346\263\250\345\206\214\344\270\216\347\224\273\345\270\203\346\270\262\346\237\223.md" "b/\345\217\257\350\247\206\345\214\226\346\220\255\345\273\272/269.\347\273\204\344\273\266\346\263\250\345\206\214\344\270\216\347\224\273\345\270\203\346\270\262\346\237\223.md" new file mode 100644 index 00000000..30c4c045 --- /dev/null +++ "b/\345\217\257\350\247\206\345\214\226\346\220\255\345\273\272/269.\347\273\204\344\273\266\346\263\250\345\206\214\344\270\216\347\224\273\345\270\203\346\270\262\346\237\223.md" @@ -0,0 +1,260 @@ +接着可视化搭建的理论抽象,我们开始勾勒一个具体的 React 可视化搭建器。 + +## 精读 + +假如我们将可视化搭建整体定义为 ``,那么 API 可能是这样的: + +```tsx + +``` + +- `componentMetas`: 定义组件元信息的数组。 +- `componentTree`: 定义组件树结构。 + +只要注册了组件元信息与组件树,可视化搭建的画布就可以渲染出来了,这很好理解。 + +我们先看组件树如何定义: + +### 组件树 + +组件树里有各组件的实例,那么最好的设计是,组件树与组件实例结构是同构的,称为 `ComponentInstance` - 组件实例: + +```json +{ + "componentName": "container", + "children": [ + { + "componentName": "text", + "props": { + "name": "我是一个文本组件" + } + } + ] +} +``` + +上面的结构既可以当做单个组件的 **组件实例信息**,也可以认为是一个 **组件树**,也就是组件树的任何组件节点都可以拎出来成为一个新组件树,这就是同构的含义。 + +我们定义了最最基础的组件树结构,以后所有功能都基于这三个要素来拓展: + +- `componentName`: 组件名,描述组件类型,比如是个文本、图片还是表格。 +- `props`: 该组件实例的所有配置信息,透传给组件 `props`。 +- `children`: 子组件,类型为 `ComponentInstance[]`。 + +每一个概念都不可或缺,让我们从概念必要性再分析一下这三个属性: + +- `componentName`: 必须拥有的属性,否则怎么渲染该节点都无从谈起。所以相应的,我们需要组件元信息来定义每个组件名应该如何渲染。 +- `props`: 即便是相同组件名的不同实例,也可能拥有不同配置,这些配置放在 `props` 里足够了,没必要开额外的其他属性存储各种各样的业务配置。 +- `children`: 理论上可以合并到 `props.children`,但因为子组件概念太常见,建议 `children` 与 `props.children` 这两种位置同时支持,同时定义时,前者优先级更高。 + +除此之外,还有一个可选属性 `componentId`,即组件唯一 ID。我们从可选性与必要性两个角度分析一下这个属性: + +- `componentId` 的可选性:组件实例在 **组件树的路径** 就是天然的组件唯一 ID,比如上面的文本组件的组件唯一 ID 可以认为是 `children.0`。 +- `componentId` 的必要性:用组件树路径代替组件唯一 ID 的坏处是,组件在组件树上移动后其唯一性就会消失,此时就要用上 `componentId` 了。 + +一个好的可视化搭建实现是支持 `componentId` 的可选性。 + +### 组件元信息 + +接着上面说的,至少要定义一个组件名是如何渲染的,所以组件元信息(`ComponentMeta`)的必要结构如下: + +```jsx +const textMeta = { + componentName: "text", + element: ({ name }) => {name}, +}; +``` + +- `componentName`: 定义哪个组件名的元信息。 +- `element`: 该组件的渲染函数。 + +实现这些最基础功能后,虽然该可视化搭建器没有人任何实质性的功能,但至少完成了一个核心基础工作:将组件树结构的描述与实现分开了。哪怕以后什么功能也不再增加,也永久的改变了开发模式,我们需要先定义组件元信息,再将其放置在组件树上。 + +对于画板工具软件,如果不考虑布局等复杂的画布功能,该结构描述足以完成大部分工作的技术抽象:配置面板修改组件实例的 `props` 属性,甚至布局位置也可以存储在 `props` 上。 + +> 对于 `element` 的命名,可能会产生分歧,比如还有其他命名风格如 `render`、`renderer`、`reactNode` 等等,但不管叫什么名字,只要是基于 React 响应式定义的,最终应该都殊途同归,最多对于各类 Key 的名称定义有所不同,这块可以保留自己的观点。 + +我们继续聚焦组件元信息的 `element` 属性,看以下 element 代码: + +```jsx +const divMeta = { + componentName: "div", + element: ({ children, header }) => ( +
+ {children} + {header} +
+ ), +}; +``` + +上面的例子中,我们可以识别出 `children` 与 `header` 类型吗?可以识别一部分: + +- `children`: 一定是 React 实例,可以是一个或多个组件实例。 +- `header`: 可能是数字、字符串,也可能是 React 实例。 + +`props.children` 对应了 `componentInstance.children` 描述,那么如何识别 `header` 是一个普通对象还是 React 实例呢? + +### Props 上的 ComponentTreeLike 属性 + +ComponentTreeLike 指的是:组件 props 属性上,识别出 “像组件实例的属性”,并将其转换为真正的组件实例传给组件。 + +假设一个正常的 `props.header` 值为 `"some text"`,那么组件 props 实际拿到的 `props.header` 值也是字符串 `"some text"`: + +```json +{ + "componentName": "div", + "props": { + "header": "some text" + } +} +``` + +```jsx +const divMeta = { + componentName: "div", + element: ({ header }) => ( +
+ {header} {/** 字符串 "some text" */} +
+ ), +}; +``` + +如果将 `props.header` 写成类 `children` 结构,可视化搭建框架就会识别为组件实例,将其转化为真正的 React 实例再传给组件: + +```json +{ + "componentName": "div", + "props": { + "header": [ + { + "componentName": "text" + } + ] + } +} +``` + +```jsx +const divMeta = { + componentName: "div", + element: ({ header }) => ( +
+ {header} {/** React 组件实例,此时会渲染出组件实例 */} +
+ ), +}; +``` + +这样设计是基于一个原则:组件树应该能描述出任何组件想要的 props 属性。我们反过来站在 `element` 角度来看,假设你注入了一个 Antd 等框架组件,如果在不改一行源码的情况下,就希望接入平台,那平台必须满足可配置出任何 props 的能力。除了基础变量外,更复杂的还有 React 组件实例与函数,现在我们解决了传组件实例的问题,至于如何传函数,我们下一小节再讲。 + +这样设计存在两个缺陷: + +1. 由于 ComponentTreeLike 会自动转成实例,所以没有办法让组件拿到 ComponentTreeLike 的原始值。 +2. 由于 ComponentTreeLike 位置不确定,为了避免深层解析产生的性能损耗,只解析 `props` 的第一级节点会导致嵌套层级较深的 ComponentTreeLike 无法被解析到。 + +如果要解决这两个缺陷,就需要在组件元信息上定义 Props 的类型,比如: + +```ts +const divMeta = { + componentName: "div", + propsType: { + header: "element", + content: ["element"], + tabs: [ + { + panel: "element", + }, + ], + }, +}; +``` + +解释一下上面的例子代表的含义: + +- `header`: 是单个 React Element。 +- `content`: 是 React Element 数组。 +- `tabs`: 是一个数组结构,每一项是对象,其中 `panel` 是 React Element。 + +这样配合以下组件树的描述,就可以精确的将对应 `element` 类型转化为组件实例了,而对于基本类型 `primitive` 保持原样传给组件: + +```json +{ + "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`: + +```jsx +const divMeta = { + componentName: "div", + runtimeProps: () => ({ + onClick: () => { console.log('click') } + }) + element: ({ onClick }) => ( + + ), +}; +``` + +点击按钮后,会打印出 `click`。这是因为 `runtimeProps` 定义了函数类型 `onClick` 在运行时传入了组件 props。 + +> 当组件树与 `componentMeta.runtimeProps` 同时定义了同一个 key 时,`runtimeProps` 优先级更高。 + +## 总结 + +本节我们介绍了组件注册与画布渲染的基础内容,我们再重新梳理一下。 + +首先定义了 `` API,并支持传入 `componentTree` 与 `componentMetas`,有了组件树与组件元信息,就可以实现可视化搭建画布的渲染了。 + +我们还介绍了如何在组件元信息定义组件的渲染函数,如何给渲染函数 props 传入基本变量、React 实例以及函数,让渲染函数可以对接任何成熟的组件库,而不需要组件库做任何适配工作。 + +但这只是可视化搭建的第一步,在真正开始做项目后,你还会遇到越来越多的问题,比如除了渲染画布,还要在业务层定义属性配置面板、组件拖拽列表、图层列表、撤销重做等等功能,这些功能如何拿到画布属性?如何与画布交互?`runtimeProps` 如何基于项目数据流给组件注入不同的属性或函数?如何根据组件 props 的变化动态注入不同函数?如何保证注入的函数引用不变? + +要解决这些问题,需要在本章的基础上实现一套系统的数据流规则以及配套 API,这也是下一讲的内容。 + +> 讨论地址是:[精读《组件注册与画布渲染》· Issue #464 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/464) + +**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。** + +> 关注 **前端精读微信公众号** + + + +> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))