低程式碼平臺前端的設計與實現(四)元件大綱樹的構建設計

w4ngzhen發表於2023-03-05

在上篇文章,我們已經設計了一個簡單的設計態的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型別檢查。

相關文章