視覺化搭建 - 容器元件設計

黃子毅發表於2023-02-20

視覺化搭建會遇到如下三類容器元件:

  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

如果你想參與討論,請 點選這裡,每週都有新的主題,週末或週一釋出。前端精讀 - 幫你篩選靠譜的內容。

關注 前端精讀微信公眾號

<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">

版權宣告:自由轉載-非商用-非衍生-保持署名(創意共享 3.0 許可證

相關文章