自定義布局頁面的思路與實現

前言

最近做了一個需求:自定義首頁。

用戶或運營可以自己修改首頁的布局,做到千人千面。

這個需求類似於當年的自定義QQ空間,不過怕是年輕一些的沒玩過這個東西。

所以你也可以簡單理解為是博客園的皮膚,只是不能寫樣式和代碼,但是可以調整各個組件的布局。

明確需求

這並不是一個低代碼頁面設計器,不是給程序員用的。

只是一個自定義布局的頁面而已,是面向客戶或者運營的。

如果遇到類似的需求,大家可以針對具體的業務需求,從而一步步明確用戶到底需要改變哪些布局屬性。

強調這一點的必要在於,需求每明確和精簡一次,都會極大簡化後面的設計和開發。

可能用戶不需要細緻到像素級的寬高,而僅僅就是簡單的幾個字——對齊和能換位置,這在後續實現上有很大差別。

技術最終還是要用來解決問題,而不是創造問題。

給用戶過多的自由度,給他一大堆設置選項,除了增加開發周期,更多地只會讓他茫然無措。

不懂代碼的用戶甚至根本不會也不願意使用你做的這個東西。

所以這個自定義布局頁面最核心的一點在於:理解簡單,操作簡單。

整體流程圖

我們將需求簡化一下,做成一個如下的流程圖:

我們需要一個設計器,去設計自定義布局的頁面,設計完成後會將這個數據保存在服務端。

同時首頁再從服務端獲取數據,用一個渲染器去渲染頁面。

按照這個思路,我們可以獨立開發渲染器和設計器,只要保證兩者之間的數據一致即可。

後續如果有業務變更,需要替換設計器和渲染器,也可以分開替換,而不需要同時替換。

不好的案例

四五年前我其實做過一個類似的需求。

當時的做法是:設計時記錄下各個組件的寬高信息和樣式,然後首頁渲染時,根據相應的信息直接調整各個組件的位置,寬高和樣式。

這種做法無疑是可以實現的,但是業務組件和布局信息耦合在了一起,新增業務組件或者調整UI時工作量很大。

對用戶而言,調整布局時,需要自行確認寬高,不斷進行適配,碰到需要適配不同分辨率屏幕的情況,不懂代碼的用戶可能心態直接就崩了。

而且,使用這種方式,在後續的業務迭代中,改動這塊代碼很容易使用戶出現線上樣式問題,導致開發根本就不敢動這塊代碼。

簡易渲染器結構

為了解決上述問題,關鍵就在於將布局組件與業務組件分離。

我將所渲染的首頁區域,作為一個容器組件。

然後將裏面的各個內容區域組件分為布局組件和業務組件,兩者完全分離後再由用戶進行綁定。

這樣在渲染的時候我們只用關心渲染簡單的布局組件就行了,布局組件內部各種複雜的業務組件及其邏輯與我們毫無關係。

為了簡單,布局組件推薦直接採用Ant Design的24列柵格設計,使用RowCol進行布局,並不需要設定寬高,只用設定布局組件占幾列就行了。

簡易設計器結構

設計器分為兩個區域:預覽區和屬性區。

左側為預覽區域:

有且僅有一個容器組件,右側有一個新增按鈕,點擊後在下方新增一個布局組件。

布局組件可通過拖拽進行排序,這裡我通過react-sortable-hoc來實現,技術細節無需贅述。

右側為屬性區域:

點擊容器組件後,右側屬性區域顯示容器組件的屬性,比如各塊之間的間距和整體的背景圖。

點擊布局組件後,右側屬性區域顯示布局組件的屬性,比如在柵格系統中佔多少列,綁定哪個業務組件。

想像一下我們生成的數據結構,用TypeScript可以簡化為:

    // 容器組件屬性
    interface IContainerInfo{
        //...各種屬性
    }

    // 布局組件屬性
    interface ILayoutInfo{
        //...各種屬性
    }

    // 最終數據結構
    interface IData{
        container:IContainerInfo
        layouts:ILayoutInfo[]
    }

至於組件的排序,用layouts數組的索引即可。

複雜布局與樹

上面的布局設定針對大多數簡單布局而言,綽綽有餘。

如果你的需求比較簡單,比如移動端之類的首頁布局,這個完全足夠了。

但是對稍微複雜一點的布局而言,上面的方案是有問題的,比如下面這種布局:

使用一個RowCol很難實現上面這個布局。

如果要實現,我們需要使用設計器生成下面的結構:

<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在這個操作信息數組中存在,並且操作為隱藏,那麼就隱藏該布局組件。

推薦使用上述方法三,可以在渲染器上保持業務邏輯和布局邏輯始終分離開來,並且不僅僅能用於隱藏信息,還能用於其它的組件聯動操作。

總結

以上便是我自己對這樣一個自定義布局頁面的思考和實現。

因為是公司的項目,所以無法開源。

但是如果參照上面的思路,實現起來應該也只是填充一些技術細節就可以了,希望對您起到幫助作用。

如果您有更好的方案,也希望不吝賜教。