forked from ascoders/weekly
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
2 changed files
with
244 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) |