可視化搭建 - 容器組件設計

可視化搭建會遇到如下三類容器組件:

  1. 簡單容器:以 children 容納子組件的容器。

  2. 卡片容器:以 props.header 加上 props.header 等多個插槽容納子組件的容器。

  3. Tab 容器:以 props.tabPanel[x] 等動態數量插槽容納子組件的容器。

畫布本身也是一個容器組件,所以可視化搭建離不開容器。

另一方面,我們應該允許給組件 props 傳入 React 組件實例,但組件樹是可序列化的 JSON 結構,因此需要一種定義方式,將某些屬性轉化爲 React 組件實例傳給組件實例。

容器的定義

任何組件都可能是容器組件,只要它將 props.childrenprops.footer 等任何屬性作爲 ReactNode 渲染。因此我們不需要特殊聲明組件是否爲容器,而僅需將某些組件 Key 聲明爲 ReactNode 節點。

Children

children 因爲太常用因此單獨強調出來,可以只在在組件實例定義 children 屬性,它爲是一個數組:

import { ComponentInstance } from "designer";

const componentTree: ComponentInstance = {
  componentName: "div",
  children: [
    {
      componentName: "input",
    },
  ],
};

對於這個組件,Designer 會將 children 定義的屬性理解爲組件實例,並真正解析爲 React 實例傳遞給 props.children,因此組件渲染代碼可以直接使用 children 渲染:

import { ComponentMeta } from "designer";

const divMeta: ComponentMeta = {
  componentName: "div",
  element: ({ children }) => <div>{children}</div>,
};

這種約定的好處是直觀自然,組件代碼也沒有關心到框架邏輯,自然而然實現了容器功能。

treeLike 結構

只要將任意組件 props 定義爲數組模式,並且包含 componentName,Designer 就認爲應該解析爲 ReactNode。

如下面的例子,我們定義的 div 組件初始化就會渲染一個 input 組件在 props.header 位置:

import { ComponentMeta } from "designer";

const divMeta: ComponentMeta = {
  componentName: "div",
  element: ({ header }) => <div>{header}</div>,
  defaultProps: {
    header: [
      {
        componentName: "input",
      },
    ],
  },
};

也可以在描述組件樹時直接寫在對應 props 位置:

import { ComponentInstance } from "designer";

const componentTree: ComponentInstance = {
  componentName: "div",
  props: {
    header: [
      {
        componentName: "input",
      },
    ],
  },
};

這種約定的好處是直觀的支持了任意 props key 爲組件實例,但依然存在限制,因此 Designer 還需要支持一種用戶 100% 掌控的申明式定義:propTypes

PropTypes

在組件元信息 propTypes 屬性定義更細緻的容器插槽位置,比如:

const tabMeta = {
  componentName: "tab",
  propTypes: {
    tabs: [
      {
        panel: "element",
      },
    ],
  },
};

那麼當組件實例如下定義時:

const componentInstance = {
  componentName: "tab",
  props: {
    tabs: [
      {
        title: "tab1",
        panel: {
          componentName: "card",
        },
      },
      {
        title: "tab2",
        panel: {
          componentName: "text",
        },
      },
    ],
  },
};

組件拿到的 props.tabs[0].panel 就是一個可以直接渲染的 React 組件實例,因爲在 propTypes 定義了 tabs[].panel 路徑是一個組件實例。

這樣設計需要考慮組件樹遍歷的問題,因爲組件實例位置定義在組件元信息上,因此僅靠組件樹無法做遍歷(因爲遍歷父節點時,不結合 componentMeta 就無法確認哪些 props 位置是子組件實例),這樣會帶來兩個問題:

  1. 遍歷組件非常麻煩,極端情況下,如果大量組件是遠程註冊的三方組件,會導致需要一層層串行遠程拉取組件實例,導致遍歷過程變慢。

  2. 更極端的場景是,當組件版本升級導致 propTypes 變化,一些原本不是組件實例的位置成爲了組件實例,或者反之,此時拉取最新組件元信息讀取的 propTypes 可能就是錯的。

因爲以上兩個原因,實現方案應該是將組件元信息定義的 propTypes 拷貝一份到組件實例,這樣就可以僅憑組件樹自身來遍歷組件樹了,而且定義在組件樹上的 propTypes 一定對應當前組件樹的結構。

總結

我們通過 children 與 props 上 treeLike 這兩個約定,實現了業務基本夠用的容器定義能力,僅憑這兩個約定就可以實現幾乎所有容器需要的效果。

propTypes 定義補全了約定拓展性的不足,讓 props 任何位置都可能成爲組件實例,只需要付出額外定義 propTypes 的代價。

閱讀到這,相信你已經理解到,可視化搭建其實不存在容器組件的概念,因爲這個組件之所以是容器,僅僅因爲它的某個 prop 屬性是組件實例,而它恰好將該屬性渲染到某個位置(甚至用 createPortal 掛載到其他 dom 節點),所以它僅僅是一種 prop 屬性的體現,因此對容器組件,我們沒有設計一種新 type,而是允許任意位置屬性定義爲實例。

下一節我們會介紹爲組件元信息添加取數與篩選聯動的鉤子,讓篩選器 + 查詢場景可以輕鬆被實現。

討論地址是:精讀《容器組件設計》· Issue #468 · dt-fe/weekly

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/i2KOPIhBz8hlVDBiWYXprw