自定義布局頁面的思路與實現
- 2021 年 12 月 30 日
- 筆記
- javascript, js, 自定義布局
前言
最近做了一個需求:自定義首頁。
用戶或運營可以自己修改首頁的布局,做到千人千面。
這個需求類似於當年的自定義QQ空間,不過怕是年輕一些的沒玩過這個東西。
所以你也可以簡單理解為是博客園的皮膚,只是不能寫樣式和代碼,但是可以調整各個組件的布局。
明確需求
這並不是一個低代碼頁面設計器,不是給程序員用的。
只是一個自定義布局的頁面而已,是面向客戶或者運營的。
如果遇到類似的需求,大家可以針對具體的業務需求,從而一步步明確用戶到底需要改變哪些布局屬性。
強調這一點的必要在於,需求每明確和精簡一次,都會極大簡化後面的設計和開發。
可能用戶不需要細緻到像素級的寬高,而僅僅就是簡單的幾個字——對齊和能換位置,這在後續實現上有很大差別。
技術最終還是要用來解決問題,而不是創造問題。
給用戶過多的自由度,給他一大堆設置選項,除了增加開發周期,更多地只會讓他茫然無措。
不懂代碼的用戶甚至根本不會也不願意使用你做的這個東西。
所以這個自定義布局頁面最核心的一點在於:理解簡單,操作簡單。
整體流程圖
我們將需求簡化一下,做成一個如下的流程圖:
我們需要一個設計器,去設計自定義布局的頁面,設計完成後會將這個數據保存在服務端。
同時首頁再從服務端獲取數據,用一個渲染器去渲染頁面。
按照這個思路,我們可以獨立開發渲染器和設計器,只要保證兩者之間的數據一致即可。
後續如果有業務變更,需要替換設計器和渲染器,也可以分開替換,而不需要同時替換。
不好的案例
四五年前我其實做過一個類似的需求。
當時的做法是:設計時記錄下各個組件的寬高信息和樣式,然後首頁渲染時,根據相應的信息直接調整各個組件的位置,寬高和樣式。
這種做法無疑是可以實現的,但是業務組件和布局信息耦合在了一起,新增業務組件或者調整UI時工作量很大。
對用戶而言,調整布局時,需要自行確認寬高,不斷進行適配,碰到需要適配不同分辨率屏幕的情況,不懂代碼的用戶可能心態直接就崩了。
而且,使用這種方式,在後續的業務迭代中,改動這塊代碼很容易使用戶出現線上樣式問題,導致開發根本就不敢動這塊代碼。
簡易渲染器結構
為了解決上述問題,關鍵就在於將布局組件與業務組件分離。
我將所渲染的首頁區域,作為一個容器組件。
然後將裏面的各個內容區域組件分為布局組件和業務組件,兩者完全分離後再由用戶進行綁定。
這樣在渲染的時候我們只用關心渲染簡單的布局組件就行了,布局組件內部各種複雜的業務組件及其邏輯與我們毫無關係。
為了簡單,布局組件推薦直接採用Ant Design的24列柵格設計,使用Row和Col進行布局,並不需要設定寬高,只用設定布局組件占幾列就行了。
簡易設計器結構
設計器分為兩個區域:預覽區和屬性區。
左側為預覽區域:
有且僅有一個容器組件,右側有一個新增按鈕,點擊後在下方新增一個布局組件。
布局組件可通過拖拽進行排序,這裡我通過react-sortable-hoc來實現,技術細節無需贅述。
右側為屬性區域:
點擊容器組件後,右側屬性區域顯示容器組件的屬性,比如各塊之間的間距和整體的背景圖。
點擊布局組件後,右側屬性區域顯示布局組件的屬性,比如在柵格系統中佔多少列,綁定哪個業務組件。
想像一下我們生成的數據結構,用TypeScript可以簡化為:
// 容器組件屬性
interface IContainerInfo{
//...各種屬性
}
// 布局組件屬性
interface ILayoutInfo{
//...各種屬性
}
// 最終數據結構
interface IData{
container:IContainerInfo
layouts:ILayoutInfo[]
}
至於組件的排序,用layouts數組的索引即可。
複雜布局與樹
上面的布局設定針對大多數簡單布局而言,綽綽有餘。
如果你的需求比較簡單,比如移動端之類的首頁布局,這個完全足夠了。
但是對稍微複雜一點的布局而言,上面的方案是有問題的,比如下面這種布局:
使用一個Row和Col很難實現上面這個布局。
如果要實現,我們需要使用設計器生成下面的結構:
<Row>
<Col span={12}>
<Row>
<Col span={24}>組件A</Col>
<Col span={24}>組件B</Col>
</Row>
</Col>
<Col span={12}>
組件C
</Col>
</Row>
實際上這就是一個樹形結構:
容器組件作為唯一根節點,無需改動。
之前綁定業務組件的布局組件,都可以理解為葉子節點,也無需改動。
唯一需要引入的是樹枝節點。
為了渲染器能夠渲染出目標結果,我們可以修改一下原先的數據結構為:
// 容器組件屬性
interface IContainerInfo{
//...各種屬性
}
// 布局組件屬性
interface ILayoutInfo{
//...各種屬性
isLeaf:boolean
children:ILayoutInfo[]
}
// 最終數據結構
interface IData{
container:IContainerInfo
layouts:ILayoutInfo[]
}
可以看到我們給布局組件引入了兩個屬性
- isLeaf 是否為葉子節點
- children 如果是樹枝節點,那麼children下面為一個布局組件屬性數組
既然數據結構確定,反推到我們的設計器,那麼就是加一個是否為葉子節點的複選框屬性。
勾選該屬性,為葉子節點,那麼展示綁定業務組件的屬性。
不勾選該屬性,為樹枝節點,展示子組件數量這個屬性。
各位可能很疑惑為什麼是子組件數量這個數字類型,而不是children之類的數據格式。
實際上,用戶可以簡單設定子組件數量,來設定樹枝節點下子組件的個數,然後再點擊左側的子組件來進行進一步的設置。
這樣在操作上來講,會簡單很多。
當然,實際開發中考慮的也需要更多,比如子組件數字增大,需要插入新的子組件,數字變小,需要刪除子組件,設計器保存和加載時子組件數量這個屬性與children這個屬性的互相轉換。
細節實現
上面的方案應該是可以滿足絕大多數情況了,但是您可能在實際開發過程中遇到以下細節問題:
設計器中節點定位
在設計器中,點擊左側布局組件,在右側展示屬性這一功能,有些朋友可能會存在一點困惑:如何獲取當前組件在樹形結構中的位置?
我這裡實現時,是給每一個節點給了一個code,這個code是從根節點到當前節點的index數組,用字符分隔後得到的字符串。
在點擊布局組件時,可以在當前節點上拿到這個字符串,然後快速還原成index數組,從而在樹形結構中存取數據。
設計器中業務組件的預覽
組件預覽的這塊,在綁定業務組件後,不必直接加載業務組件來展示。
因為這裡只調整布局,不需要業務組件內部的邏輯。
所以我推薦這裡用一張圖片,或者一些簡化後的組件代替即可。
如非必要,設計器這裡的預覽業務組件和業務組件應該完全分離,用code關聯即可,否則代碼的可讀性和可維護性都會受到挑戰。
渲染器中業務組件隱藏的情況
通常我們可能會遇到一些隱藏業務組件的需求,比如如果是管理員,那麼就在首頁展示這個組件,否則隱藏。
我們通過上述手段實現的布局中,如果業務組件隱藏,包裹它的布局組件是不會隱藏的。
所呈現的效果就是,這個隱藏的業務組件附近,塊之間的間隔會比正常間隔大。
解決這個問題,可以使用下面三種方法:
- 方法一(不推薦):取消塊間隔這一屬性,組件的間隔由業務組件實現。這樣最簡單,但是業務組件承擔了過多的布局任務。
- 方法二(不推薦):在業務組件中判定需要隱藏後,觸發佈局組件傳遞進來的回調函數,從而隱藏布局組件。但是這種子組件控制父組件的方式並不好,可讀性差。而且業務組件應該與布局組件解耦,不能直接控制布局組件。另外這種控制方法,對於隱藏操作而言只能生效一次。
- 方法三(推薦):在根節點組件中,設置控制邏輯。傳遞一個操作信息數組到每個子節點,如果布局組件的業務組件Code在這個操作信息數組中存在,並且操作為隱藏,那麼就隱藏該布局組件。
推薦使用上述方法三,可以在渲染器上保持業務邏輯和布局邏輯始終分離開來,並且不僅僅能用於隱藏信息,還能用於其它的組件聯動操作。
總結
以上便是我自己對這樣一個自定義布局頁面的思考和實現。
因為是公司的項目,所以無法開源。
但是如果參照上面的思路,實現起來應該也只是填充一些技術細節就可以了,希望對您起到幫助作用。
如果您有更好的方案,也希望不吝賜教。