6

低代码平台前端的设计与实现(四)组件大纲树的构建设计 - w4ngzhen

 1 year ago
source link: https://www.cnblogs.com/w4ngzhen/p/17181716.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

在上篇文章,我们已经设计了一个简单的设计态的Canvas,能够显示经过BuildEngine生成的ReactNode进行渲染。本文,我们将继续上一篇文章的成果,设计并实现一个能够显示组件节点大纲树的组件。

什么是组件大纲树?

我们希望用户能通过一个地方比较明显的看到当前整个ElementNode的树状结构;当用户点击某个ElementNode的时候,既能够在DesignCanvas上高亮当前选中的UI元素,同时对于组件大纲树上也能高亮对应的树状节点。

PS:我们所设计的低开前端平台定位是轻量级。所以,我们在构建整个平台核心库的时候,并不会设计的非常复杂,本次我们将不会设计直接将元素进行拖拉拽到画布的内容,而是会围绕整个节点大纲树,来优化我们的低开体验。

010-effect

如何设计实现大纲树与设计态UI界面的统一?

在本次设计与开发之前,我们需要回顾一下上篇文章中(低代码平台前端的设计与实现(三)设计态画布DesignCanvas的设计与实现 - 知乎 (zhihu.com))关于DesignCanvas的设计。DesignCanvas的过程设计如下:

020-DesignCanvasOldVersion

正如上图所示,DesignCanvas的执行过程中,step4 -> data5 -> step6 -> data7 是在一个函数处理过程中的。

为了实现本次的需求,我们可能需要对上述的过程进行一定的修改,达到UI的渲染与元素节点大纲树组件在同一个DesignCanvas中的渲染的目的。在讨论如何修改前,我们先采用一个流程图来展示这个过程:

030-DesignCanvasWithNodeTreeIdea

从上图,我们可以很容易的知道,为了让ElementNode树到UI界面的生成与ElementNode树到节点大纲树的生成是一致且同步的。需要原本在DesignCanvas中渲染过程中,将ElementNode的JSON字符串解析为ElementNode节点数据,再把ElementNode数据交给BuildEngine生成ReactNode的这个整体的步骤,拆分为两个先后两个步骤,并将ElementNode object作为state。这样,我们才能将ElementNode object分别交给节点大纲树组件进行节点树渲染,同时交给BuildEngine进行UI的ReactNode生成、渲染。

在这样一套设计下,无论点击大纲树任意树节点,还是点击设计态UI界面的任意UI组件。我们都能够通过相关的事件(对于大纲树来说是树节点的点击事件;对于设计态UI界面上的UI组件来说是前面设计的wrapper的点击事件)拿到当前点击的元素的唯一path标识;然后,我们将拿到的path标识设置给selectedElementNodePath这个state,最后再由该state来同时控制大纲树的节点高亮和设计态UI界面上的UI组件的边框高亮。这个过程由下面的流程图来简单描述:

040-selectedElementNodePathChangeEvent

大纲树组件实现

首先,我们选择了antd5的Tree树形组件。对于该组件我们会以受控的方式来使用,具体来讲,Tree树形组件的节点选中通过属性selectedKeys控制;树形组件的节点展开通过属性expandedKeys来控制。当然,一旦我们选择该组件以受控方式使用,那么不可避免的需要用对应的onSelect事件onExpand事件来获取当前状态值,再交给上述的selectedKeysexpandedKeys

Tree组件的基本用法

本节内容主要讲antd5的Tree树形组件的基本用法,目的是为了后面我们具体的大纲树组件做基础准备,可以完全当作独立的一节内容来看。

Tree的selectedKeys接收的是一个数组,用以表现被选中的节点。但需要特别注意:

Tree在默认的使用场景下是单个选中。也就是说,用户点击任意一个节点时,就选中该节点;点击其他节点,则选中其他节点。同一时间只会有一个被选中的节点。selectedKeys尽管是一个数组,但在单选场景下,要不是一个空数组来表示没有节点选中,要不是一个只有一个元素的数组,表示某一个节点选中。下面用一个Demo来演示:

 /**
 * 首先准备一段测试数据:
 * 1
 * ├ 1-1
 * └ 1-2
 *   └ 1-2-1
 * 注意:TREE_DATA是一个数组!
 **/
const TREE_DATA = [
    {
        key: '1',
        title: 'title 1',
        children: [{
            key: '1-1',
            title: 'title 1-1'
        }, {
            key: '1-2',
            title: 'title 1-2',
            children: [{
                key: '1-2-1',
                title: 'title 1-2-1'
            }]
        }]
    }
]

然后,编写一段代码,将selectedKeys设置为1-2-1,也就是说,我们选中了上面的1-2-1节点:

export const TreeDemo = () => {
    return <Tree selectedKeys={['1-2-1']} treeData={TREE_DATA}/>
}
// 再次强调,selectedKeys是一个数组,但是在默认情况下,该数组只有一个元素或者空。

这个例子的效果如下:

selectedKeys

从上面的gif可以看到界面渲染后,选中的节点就是1-2-1。同时,其他的节点无论我们如何点击,都不会有任何的效果(受控)。为了能够点击后,让Tree组件选中对应的节点,我们需要将selectedKeys至少作为一个state来存放,然后通过onSelect来设置该state:

export const TreeDemo = () => {
    // 用一个state来表明当前选择的Keys
    const [currSelectedKeys, setCurrSelectedKeys] = useState<string[]>([]);
    return <Tree
        treeData={TREE_DATA}
        selectedKeys={currSelectedKeys}
        onSelect={selectedKeys => {
            // 当我们点击任何一个节点的时候,都会触发该onSelect,第一个参数则是即将选中的Keys
            // 当然,根据文档,我们重复点击同一节点,也会触发该onSelect事件,但参数 selectedKeys 会是一个空数组
            console.log('onSelect, selectedKeys: ', selectedKeys);
            setCurrSelectedKeys(selectedKeys as string[])
        }}
    />
}

上述的过程,可以用如下的数据流来描述:

060-selectedKeys-workflow

上述过程中,currSelectedKeys表明当前选中的Keys(默认的单选模式下,是一个长度为1或0的数组),传给Tree的属性selectedKeys,Tree组件的UI展示的过程中使用根据selectedKeys来高亮对应节点;当然,我们点击任意节点的时候,会触发onSelect事件,该事件第一个参数就是点击选中的节点的Keys,我们可以直接将这个值再次设置给currSelectedKeys这个state。在上述的代码下,我们可以看到效果如下:

070-selectedKeys-with-control

现在,我们分析了selectedKeys后,再来分析一下Tree树形组件的expandedKeys。这个属性是一个数组,控制整个Tree节点展开的Keys。我们首先将该值设置为:['1']

    ... ...
    return <Tree
        treeData={TREE_DATA}
        selectedKeys={currSelectedKeys}
        onSelect={selectedKeys => {
           ... ...
        }}
+       expandedKeys={['1']}
    />

然后查看Demo效果:

080-expandedKeys-without-control

可以看到,无论怎样点击节点左侧的三角,都无法展开或收起对应的子节点。类似的,我们使用一个state来存储展开的节点,然后使用onExpand事件来设置,即可达到效果:

090-expandedKeys-with-control.gif

组件大纲树面板

有了上面关于antd5的Tree树形组件的受控方式的使用基础,我们开始设计我们自己的组件大纲树组件,这里我们为它取名为:ElementNodeDesignTreePanel。该组件的props如下:

interface ElementNodeDesignTreePanelProps {
    /**
     * 根ElementNode
     */
    rootElementNode: ElementNode;
    /**
     * 选中的元素节点
     */
    selectedElementNodePath: string;
    /**
     * 点击选中Tree中的某个节点的事件回调
     * @param selectedPath 选中指定的ElementNode的Path,
     * 譬如:"/page/panel@0/button@0"
     * @param selectedPathList 选中的指定的ElementNode的完整链路Path列表
     * 譬如:["/page", "/page/panel@0", "/page/panel@0/button@0"]
     */
    onElementNodeSelected: (selectedPath: string) => void;
}

我们再来讨论下这些属性如何关联内部的antd Tree树形组件的渲染与行为的。这里,我直接用一个流程图来描述:

100-ElementNodeDesignTreePanel-workflow.png

上述过程具体为:首先,为了呈现组件节点树状UI,很容易知道至少需要将ElementNode对象传入,因为该对象本身就是树形的,只需要进行简单的数据转换即可完成Tree的树形渲染;其次,为了达到高亮对应的节点效果,则需要传入当前选中的ElementNode的唯一path,在内部转换为selectedKeys和expandedKeys;最后,当我们点击Tree的节点时候,需要把对应的节点信息传到上层,让外部再次控制传入当前选中的ElementNode的path,形成一个闭环的数据流。

当然,这里面还涉及一些转换,还有path的构成规则。这里不再赘述,感兴趣的读者可以阅读有关ElementNodeDesignTreePanel的组件代码。

本次的内容已经提交至Github,并在打上了相应的Git tag标识:

https://github.com/w4ngzhen/lite-lc/tree/chapter_04

commit信息(倒序):

4. 修改DesignCanvas相关逻辑,实现ElementNodeDesignTreePanel组件与BuildEngine生成的组件对于选中的节点path,同步分别高亮树形节点和UI组件。

3. 新增工具方法,支持根据ElementNode的path,得到该节点的整个链路path形成的数组;新增ElementNodeDesignTreePanel组件,内部使用antd的Tree树形控件以呈现ElementNode的树状结构,且通过外部传入的"选中节点path"属性,以受控方式 
控制高亮节点。

2. core/src/canvas下目录新增design目录,将存放于canvas目录下的DesignCanvas、ElementNodeDesignWrapper迁移至design目录,同步修改index中的DesignCanvas。
丰富example中的样例ElementNode Json字符串。

1. 1)core与example项目均升级antd至^5.2.0,antd5采用 CSS in JS方案,故移除antd相关样式文件;2)core项目tsconfig配置修改,跳过对antd等外部库的ts类型检查。

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK