Skip to content

Commit

Permalink
270
Browse files Browse the repository at this point in the history
  • Loading branch information
ascoders committed Feb 6, 2023
1 parent c357b17 commit b3a37f1
Show file tree
Hide file tree
Showing 2 changed files with 244 additions and 1 deletion.
3 changes: 2 additions & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

前端界的好文精读,每周更新!

最新精读:<a href="./可视化搭建/269.%E7%BB%84%E4%BB%B6%E6%B3%A8%E5%86%8C%E4%B8%8E%E7%94%BB%E5%B8%83%E6%B8%B2%E6%9F%93.md">269.组件注册与画布渲染</a>
最新精读:<a href="./可视化搭建/270.%E7%94%BB%E5%B8%83%E4%B8%8E%E7%BB%84%E4%BB%B6%E5%85%83%E4%BF%A1%E6%81%AF%E6%95%B0%E6%8D%AE%E6%B5%81.md">270.画布与组件元信息数据流</a>

素材来源:[周刊参考池](https://github.com/ascoders/weekly/issues/2)

Expand Down Expand Up @@ -303,6 +303,7 @@

- <a href="./可视化搭建/268.%E5%A6%82%E4%BD%95%E6%8A%BD%E8%B1%A1%E5%8F%AF%E8%A7%86%E5%8C%96%E6%90%AD%E5%BB%BA.md">268.如何抽象可视化搭建</a>
- <a href="./可视化搭建/269.%E7%BB%84%E4%BB%B6%E6%B3%A8%E5%86%8C%E4%B8%8E%E7%94%BB%E5%B8%83%E6%B8%B2%E6%9F%93.md">269.组件注册与画布渲染</a>
- <a href="./可视化搭建/270.%E7%94%BB%E5%B8%83%E4%B8%8E%E7%BB%84%E4%BB%B6%E5%85%83%E4%BF%A1%E6%81%AF%E6%95%B0%E6%8D%AE%E6%B5%81.md">270.画布与组件元信息数据流</a>

### SQL

Expand Down
242 changes: 242 additions & 0 deletions 可视化搭建/270.画布与组件元信息数据流.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
接下来需要解决两个问题:

1. 可视化搭建的其他业务元素如何与画布交互。比如拓展属性配置面板、图层列表、拖拽添加组件、定位锚点、主题等等。
2. `runtimeProps` 如何访问到当前组件实例的 `props`

这两个问题非常重要,而恰好又可以通过良好的数据流设计一次性解决,接下来让我们分别分析讨论一下。

**问题一:可视化搭建的其他业务元素如何与画布交互。比如拓展属性配置面板、图层列表、拖拽添加组件、定位锚点、主题等等**

需要设计一个 Hooks API,可以访问到画布提供的方法、数据。在 React 设计中,访问 Hooks API 需要在一定上下文内,所以可以将 `<Designer>` 拆为 `<Designer>``<Canvas>`,其中 `<Designer>` 提供 Hooks 上下文,`<Canvas>` 负责渲染画布。这样开发者的使用方式就变成了这样:

```jsx
import { createDesigner } from 'designer'

const { Designer, Canvas, useDesigner } = createDesigner()

const EditPanel = {
const { addComponent } = useDesigner()

return <button onClick={() => addComponent(/** ... */)}>创建组件</button>
}

const App = () => {
<Designer>
<Canvas />
<EditPanel />
</Designer>
}
```

为了支持多个 Designer 实例间隔离,通过 `createDesigner` 创建一套上下文独立的 API,这样就可以让画布、配置面板同时用 Designer 实现,用一套技术方案同时实现画布与配置表单,这样学习上下文、组件规范都可以统一为一套,表单、画布能力也可以共享。

`<Designer>` 内的组件可以通过 `useDesigner` 直接访问数据与方法,比如上面例子在直接访问内置方法 `addComponent` 时,不需要附加任何参加,而 `addComponent` 方法也永远保持引用不变,此时 `useDesigner` 不会导致 `EditPanel` 重渲染。

如果需要访问当前组件树,并在组件树变化时重渲染,可以通过如下方式访问:

```jsx
const EditPanel = {
const { componentTree } = useDesigner(state => ({
componentTree: state.componentTree
}))
}
```

该写法的效果是,当 `state.componentTree` 变化了,会触发 `EditPanel` 重新渲染,并拿到最新值。

同时也可以传入第二个参数 `compare` 自定义对比方法,默认为 `shallowEqual`

```jsx
useDesigner(
(state) => ({
componentTree: state.componentTree,
}),
isEqual
);
```

如此一来,无论给画布拓展多少 UI 元素都没有问题,而且 UI 元素可以自由的访问画布方法与数据。

**问题二:`runtimeProps` 如何访问到当前组件实例的 `props`**

`componentMeta.runtimeProps` 中,我们构造一个 `selector` 函数用于访问当前组件 props:

```jsx
const divMeta = {
componentName: "div",
runtimeProps: ({ selector }) => {
const name = selector(({ props }) => props.name)

return {
fullName: `full-${name}`
}
}
element: /** ... */
};
```

首先支持从 `runtimeProps` 回调里拿到 `selector`,并且该 `selector` 支持传入一个回调函数,该回调函数的参数中 `props` 指向当前组件实例的 `props`,通过该方法就可以访问组件 props 了。

该 selector 仅在 `props.name` 改变时重新执行,并且也遵循 `compare` 对比规则,即当 `props.name` 变化时,`selector` 回调函数的返回值通过 `compare` 与上一次值进行对比,如果没有变化就返回上一次的旧值,变化了则返回新值。默认对比函数为 `shallowEqual`,与 `useDesigner` 类似,也可以在第二个参数位置覆写 `compare` 方法。

那组件元信息如何访问内置静态方法呢?由于静态方法引用不变,因此可以在 `selector` 同级直接传入:

```jsx
const divMeta = {
componentName: "div",
runtimeProps: ({ addComponent }) => {
return {
add: () => {
/** addComponent(...) */
}
}
}
element: /** ... */
};
```

如此一来,**我们就将数据流与组件元信息打通了**,即 UI 可以通过 `useDesigner` 访问与操作数据流,组件元信息也可以直接拿到方法,或通过 `selector` 拿到数据,相应的也可以访问与操作数据流。这样的设计在以后拓展更多组件元信息函数时,都可以继承下来,开发者只要学习一次语法,就可以获得非常强力的拓展性。

## 拓展应用状态与静态方法

刚才介绍了一些内置的状态(`componentTree`)与方法(`addComponent`),在下一接会系统介绍笔者梳理了哪些内置状态与方法。首先抛开内置状态与方法不谈,应用肯定需要定义自己的状态与方法,我们可以提供两种模式给用户。

第一种是应用的状态与方法定义在外部,对应受控模式。

假设你的应用在对接 Designer 之前就已经用 Redux、Dva、Zustand 等状态管理库,那么就可以使用受控模式直接接入:

```jsx
const App = () => {
// 伪代码,不管是 useState 还是其他数据流管理状态,假这里拿到了数据与方法
const { getAppInfo } = useSomeLib();
const { userName } = useSomeLib("userName");

return <Designer actions={{ getAppInfo }} state={{ userName }} />;
};
```

将方法传给 `actions`,状态传给 `state`

第二种是应用的状态与方法通过 `<Designer>` 定义,对用非受控模式。

假设你的应用之前没有使用任何数据流,那么也可以直接将 Designer 的数据流作为项目数据流使用:

```jsx
import { createMiddleware, createDesigner } from "designer";

const middleware1 = createMiddleware({
state: { userName: "bob " },
actions: { getAppInfo: () => {} },
});

const { Designer } = createDesigner(middleware1);

const App = () => {
return <Designer />;
};
```

通过 `createMiddleware` 创建一个中间件定义状态与函数,传入 `createDesigner` 即可生效。

也可以在 `createMiddleware` 里通过第二个参数定义自定义 hooks,或者拿到方法更改 State:

```jsx
const middleware1 = createMiddleware(
{
state: { userName: "bob " },
},
({ setState }) => {
const setUserName = React.useCallback((newName: string) => {
setState((state) => ({
...state,
userName: newName,
}));
});

return { setUserName };
}
);
```

Designer 内部采用最朴素的 Redux 管理状态,提供了最基础的 `getState``setState` 获取与修改状态,基于它们封装业务函数即可。

无论是受控模式,还是非受控模式(亦或两种模式同时使用),定义的状态与方法都可以在以下两个位置访问,第一个位置是 `useDesigner`

```jsx
const {
/** 自定义函数 */,
setUserName,
/** 自定义函数 */
getAppInfo,
/** 内置函数 */
addComponent,
// 内置变量
componentTree,
// 自定义变量
userNamee
} = useDesigner(state => ({
componentTree: state.componentTree,
userName: state.userName
}))
```

第二个位置是组件元信息上的回调函数,比如 `runtimeProps`

```jsx
const divMeta = {
componentName: "div",
runtimeProps: ({
selector,
/** 自定义函数 */,
setUserName,
/** 自定义函数 */
getAppInfo,
/** 内置函数 */
addComponent
}) => {
const {
/** 内置变量 */
componentTree,
/** 自定义变量 */
userName
} = selector(({ state }) => ({
componentTree: state.componentTree,
userName: state.userName
}))

return { componentTree, userName }
}
element: /** ... */
};
```

至此,我们实现了一套完整的数据流定义,包括:

- 不同 Designer 之间上下文隔离。
- 可无缝对接项目数据流,也可作为独立数据流方案提供。
- 内置变量与函数与自定义变量、函数混合。
- 无论在 UI 通过 `useDesigner`,还是在组件元信息通过 `selector` 都可访问这些变量与函数。

## 总结

一个基本可用的可视化搭建框架在本章就算设计完了。但这只是可视化搭建问题的冰山一角,未来的章节,笔者会逐渐为大家介绍更多可视化搭建的设计。

但无论框架未来怎么发展,也永远会基于这前三章的基本设定,总结一下,这三章的基本设定就是:设计一个逻辑与 UI 分离的可视化搭建协议,数据流、组件元信息、组件实例是永远的铁三角,数据流可以对接任意已存在的实现,或基于 Designer 规范实现,组件元信息与组件实例仅存储最基本信息,得益于数据流的自定义能力,以及无论何处都有完全的数据流访问能力,使业务框架既遵循规则,又可以千变万化。

抛开具体 API 设计或者命名不谈,一个有简洁、抽象,又提供极少量 API 却能满足所有业务定制诉求,是可视化搭建永远追求的目标。只要熟悉了这套规范,就可以几乎仅根据业务表现,一眼猜出是基于哪些 API 封装实现的,那么维护成本与理解成本将大大降低,规范的意义就体现在这里。

也许有同学会觉得,现在各个大厂都有无数可视化搭建的实现,可视化搭建概念都已经烂大街了,为什么还要重新设计一个呢?

因为也许数量不代表质量,维护的时间越久,参与的同学越多,越容易使设计变得冗余,概念变得复杂,要对抗这些递增的熵,唯有不断重新设计,从零开始反思方案。

下一讲理论思考会少一些,介绍可视化搭建框架会考虑内置哪些变量与方法。

> 讨论地址是:[精读《画布与组件元信息数据流》· Issue #466 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/464)
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**

> 关注 **前端精读微信公众号**
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">

> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh)

0 comments on commit b3a37f1

Please sign in to comment.