JS前端画布与组件元信息数据流示例详解

2023-02-07 12:01:34
目录
正文拓展应用状态与静态方法总结

正文

接下来需要解决两个问题:

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

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

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

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

    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 重渲染。

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

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

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

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

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

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

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

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

    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 同级直接传入:

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

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

    拓展应用状态与静态方法

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

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

    假设你的应用在对接>

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

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

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

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

    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:

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

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

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

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

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

    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 都可访问这些变量与函数。

      总结

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

      但无论框架未来怎么发展,也永远会基于这前三章的基本设定,总结一下,这三章的基本设定就是:设计一个逻辑与>

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

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

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

      下一讲理论思考会少一些,介绍可视化搭建框架会考虑内置哪些变量与方法,更多关于JS画布与组件元信息数据流的资料请关注易采站长站其它相关文章!

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