組件註冊與畫布渲染

接着可視化搭建的理論抽象,我們開始勾勒一個具體的 React 可視化搭建器。

精讀

假如我們將可視化搭建整體定義爲 <Designer>,那麼 API 可能是這樣的:

<Designer componentMetas={[]} componentTree={} />

只要註冊了組件元信息與組件樹,可視化搭建的畫布就可以渲染出來了,這很好理解。

我們先看組件樹如何定義:

組件樹

組件樹裏有各組件的實例,那麼最好的設計是,組件樹與組件實例結構是同構的,稱爲 ComponentInstance - 組件實例:

{
  "componentName""container",
  "children"[
    {
      "componentName""text",
      "props"{
        "name""我是一個文本組件"
      }
    }
  ]
}

上面的結構既可以當做單個組件的 組件實例信息,也可以認爲是一個 組件樹,也就是組件樹的任何組件節點都可以拎出來成爲一個新組件樹,這就是同構的含義。

我們定義了最最基礎的組件樹結構,以後所有功能都基於這三個要素來拓展:

每一個概念都不可或缺,讓我們從概念必要性再分析一下這三個屬性:

除此之外,還有一個可選屬性 componentId,即組件唯一 ID。我們從可選性與必要性兩個角度分析一下這個屬性:

一個好的可視化搭建實現是支持 componentId 的可選性。

組件元信息

接着上面說的,至少要定義一個組件名是如何渲染的,所以組件元信息(ComponentMeta)的必要結構如下:

const textMeta = {
  componentName: "text",
  element: ({ name }) => <span>{name}</span>,
};

實現這些最基礎功能後,雖然該可視化搭建器沒有人任何實質性的功能,但至少完成了一個核心基礎工作:將組件樹結構的描述與實現分開了。哪怕以後什麼功能也不再增加,也永久的改變了開發模式,我們需要先定義組件元信息,再將其放置在組件樹上。

對於畫板工具軟件,如果不考慮佈局等複雜的畫布功能,該結構描述足以完成大部分工作的技術抽象:配置面板修改組件實例的 props 屬性,甚至佈局位置也可以存儲在 props 上。

對於 element 的命名,可能會產生分歧,比如還有其他命名風格如 renderrendererreactNode 等等,但不管叫什麼名字,只要是基於 React 響應式定義的,最終應該都殊途同歸,最多對於各類 Key 的名稱定義有所不同,這塊可以保留自己的觀點。

我們繼續聚焦組件元信息的 element 屬性,看以下 element 代碼:

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

上面的例子中,我們可以識別出 childrenheader 類型嗎?可以識別一部分:

props.children 對應了 componentInstance.children 描述,那麼如何識別 header 是一個普通對象還是 React 實例呢?

Props 上的 ComponentTreeLike 屬性

ComponentTreeLike 指的是:組件 props 屬性上,識別出 “像組件實例的屬性”,並將其轉換爲真正的組件實例傳給組件。

假設一個正常的 props.header 值爲 "some text",那麼組件 props 實際拿到的 props.header 值也是字符串 "some text"

{
  "componentName""div",
  "props"{
    "header""some text"
  }
}
const divMeta = {
  componentName: "div",
  element: ({ header }) =(
    <div>
      {header} {/** 字符串 "some text" */}
    </div>
  ),
};

如果將 props.header 寫成類 children 結構,可視化搭建框架就會識別爲組件實例,將其轉化爲真正的 React 實例再傳給組件:

{
  "componentName""div",
  "props"{
    "header"[
      {
        "componentName""text"
      }
    ]
  }
}
const divMeta = {
  componentName: "div",
  element: ({ header }) =(
    <div>
      {header} {/** React 組件實例,此時會渲染出組件實例 */}
    </div>
  ),
};

這樣設計是基於一個原則:組件樹應該能描述出任何組件想要的 props 屬性。我們反過來站在 element 角度來看,假設你注入了一個 Antd 等框架組件,如果在不改一行源碼的情況下,就希望接入平臺,那平臺必須滿足可配置出任何 props 的能力。除了基礎變量外,更復雜的還有 React 組件實例與函數,現在我們解決了傳組件實例的問題,至於如何傳函數,我們下一小節再講。

這樣設計存在兩個缺陷:

  1. 由於 ComponentTreeLike 會自動轉成實例,所以沒有辦法讓組件拿到 ComponentTreeLike 的原始值。

  2. 由於 ComponentTreeLike 位置不確定,爲了避免深層解析產生的性能損耗,只解析 props 的第一級節點會導致嵌套層級較深的 ComponentTreeLike 無法被解析到。

如果要解決這兩個缺陷,就需要在組件元信息上定義 Props 的類型,比如:

const divMeta = {
  componentName: "div",
  propsType: {
    header: "element",
    content: ["element"],
    tabs: [
      {
        panel: "element",
      },
    ],
  },
};

解釋一下上面的例子代表的含義:

這樣配合以下組件樹的描述,就可以精確的將對應 element 類型轉化爲組件實例了,而對於基本類型 primitive 保持原樣傳給組件:

{
  "componentName""div",
  "props"{
    "header"{
      "componentName""text"
    },
    "names"["a""b""c"],
    "content"[
      {
        "componentName""text"
      },
      {
        "componentName""text"
      }
    ],
    "tabs"[
      {
        "title""tab1",
        "panel"{
          "componentName""text"
        }
      }
    ]
  }
}

如此一來,沒有定義爲 Element 的屬性不會處理成 React 實例,第一個問題就自然解決了。通過配置更深層嵌套的結構,第二個問題也自然解決。

componentMeta.propsType 之所以不採用 JSONSchema 結構,是因爲框架沒必要內置對 props 類型校驗的能力,這個能力可以交給業務層來處理,所以這裏就可以採用簡化版結構,方便書寫,也容易閱讀。

注意:propsType{} 表示 value 是對象,而 [] 表示 value 是數組。爲數組時,僅支持單個子元素,因爲單項即是對數組每一項類型的定義。

給組件注入函數

現在已經能給 componentMeta.element 傳入任意基礎類型、React 實例的 props 了,現在還缺函數類型或者 Set、Map 等複雜類型問題需要解決。

由於組件樹結構需要序列化入庫,所以必須爲一個可以序列化的 JSON 結構,而這個結構又需要暴露給開發者,所以也不適合定義一些 hack 的序列化、反序列化規則。因此要給組件 props 注入函數,需要定義在組件元信息上,由於其定義了額外的 props 屬性,且不在組件樹中,所以我們將其命名爲 runtimeProps:

const divMeta = {
  componentName: "div",
  runtimeProps: () =({
    onClick: () ={ console.log('click') }
  })
  element: ({ onClick }) =(
    <button onClick={onClick}>
      點擊我
    </button>
  ),
};

點擊按鈕後,會打印出 click。這是因爲 runtimeProps 定義了函數類型 onClick 在運行時傳入了組件 props。

當組件樹與 componentMeta.runtimeProps 同時定義了同一個 key 時,runtimeProps 優先級更高。

總結

本節我們介紹了組件註冊與畫布渲染的基礎內容,我們再重新梳理一下。

首先定義了 <Designer /> API,並支持傳入 componentTreecomponentMetas,有了組件樹與組件元信息,就可以實現可視化搭建畫布的渲染了。

我們還介紹瞭如何在組件元信息定義組件的渲染函數,如何給渲染函數 props 傳入基本變量、React 實例以及函數,讓渲染函數可以對接任何成熟的組件庫,而不需要組件庫做任何適配工作。

但這只是可視化搭建的第一步,在真正開始做項目後,你還會遇到越來越多的問題,比如除了渲染畫布,還要在業務層定義屬性配置面板、組件拖拽列表、圖層列表、撤銷重做等等功能,這些功能如何拿到畫布屬性?如何與畫布交互?runtimeProps 如何基於項目數據流給組件注入不同的屬性或函數?如何根據組件 props 的變化動態注入不同函數?如何保證注入的函數引用不變?

要解決這些問題,需要在本章的基礎上實現一套系統的數據流規則以及配套 API,這也是下一講的內容。

討論地址是:精讀《組件註冊與畫布渲染》· Issue #464 · dt-fe/weekly

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