在上篇文章,我們已經設計了一個簡單的設計態的Canvas,能夠顯示經過BuildEngine生成的ReactNode進行渲染。本文,我們將繼續上一篇文章的成果,設計並實現一個能夠顯示元件節點大綱樹的元件。
什麼是元件大綱樹?
我們希望使用者能透過一個地方比較明顯的看到當前整個ElementNode的樹狀結構;當使用者點選某個ElementNode的時候,既能夠在DesignCanvas上高亮當前選中的UI元素,同時對於元件大綱樹上也能高亮對應的樹狀節點。
PS:我們所設計的低開前端平臺定位是輕量級。所以,我們在構建整個平臺核心庫的時候,並不會設計的非常複雜,本次我們將不會設計直接將元素進行拖拉拽到畫布的內容,而是會圍繞整個節點大綱樹,來最佳化我們的低開體驗。
如何設計實現大綱樹與設計態UI介面的統一?
在本次設計與開發之前,我們需要回顧一下上篇文章中(低程式碼平臺前端的設計與實現(三)設計態畫布DesignCanvas的設計與實現 - 知乎 (zhihu.com))關於DesignCanvas的設計。DesignCanvas的過程設計如下:
正如上圖所示,DesignCanvas的執行過程中,step4 -> data5 -> step6 -> data7
是在一個函式處理過程中的。
為了實現本次的需求,我們可能需要對上述的過程進行一定的修改,達到UI的渲染與元素節點大綱樹元件在同一個DesignCanvas中的渲染的目的。在討論如何修改前,我們先採用一個流程圖來展示這個過程:
從上圖,我們可以很容易的知道,為了讓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元件的邊框高亮。這個過程由下面的流程圖來簡單描述:
大綱樹元件實現
首先,我們選擇了antd5的Tree樹形元件。對於該元件我們會以受控的方式來使用,具體來講,Tree樹形元件的節點選中透過屬性selectedKeys
控制;樹形元件的節點展開透過屬性expandedKeys
來控制。當然,一旦我們選擇該元件以受控方式使用,那麼不可避免的需要用對應的onSelect事件
和onExpand事件
來獲取當前狀態值,再交給上述的selectedKeys
和expandedKeys
。
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是一個陣列,但是在預設情況下,該陣列只有一個元素或者空。
這個例子的效果如下:
從上面的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[])
}}
/>
}
上述的過程,可以用如下的資料流來描述:
上述過程中,currSelectedKeys表明當前選中的Keys(預設的單選模式下,是一個長度為1或0的陣列),傳給Tree的屬性selectedKeys
,Tree元件的UI展示的過程中使用根據selectedKeys
來高亮對應節點;當然,我們點選任意節點的時候,會觸發onSelect事件,該事件第一個引數就是點選選中的節點的Keys,我們可以直接將這個值再次設定給currSelectedKeys這個state。在上述的程式碼下,我們可以看到效果如下:
現在,我們分析了selectedKeys
後,再來分析一下Tree樹形元件的expandedKeys
。這個屬性是一個陣列,控制整個Tree節點展開的Keys。我們首先將該值設定為:['1']
:
... ...
return <Tree
treeData={TREE_DATA}
selectedKeys={currSelectedKeys}
onSelect={selectedKeys => {
... ...
}}
+ expandedKeys={['1']}
/>
然後檢視Demo效果:
可以看到,無論怎樣點選節點左側的三角,都無法展開或收起對應的子節點。類似的,我們使用一個state來儲存展開的節點,然後使用onExpand事件來設定,即可達到效果:
元件大綱樹皮膚
有了上面關於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樹形元件的渲染與行為的。這裡,我直接用一個流程圖來描述:
上述過程具體為:首先,為了呈現元件節點樹狀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型別檢查。