視覺化搭建內建 API

黃子毅發表於2023-02-13

在設計好畫布與元件資料流體系後,理論上主體功能已經完成,但缺乏方便易用的 API,所以還需要內建一些狀態與方法。

但是內建狀態與方法必須尋求業務的最大公約數,極具抽象性,新增需慎重。

接下來我們從必須有與建議有的角度,看看一個視覺化搭建需要內建哪些 API。

狀態

狀態是可變的,引用方式有如下兩種。

第一種在任意 React 元件內透過 useDesigner 訪問,當狀態變化時會觸發所在元件重渲染:

const { componentTree } = useDesigner((state) => ({
  componentTree: state.componentTree,
}));

第二種在任意元件元資訊內透過 selector 訪問,當狀態變化時會觸發不同行為,比如在 runtimeProps 會觸發元件重渲染,在 fetcher 會觸發重新查詢:

const tableMeta = {
  /** ... */
  runtimeProps: ({ selector }) => {
    const { componentTree } = selector(({ state }) => ({
      componentTree: state.componentTree,
    }));
    return { componentTree };
  },
};

componentTree

  • 評價:必須有
  • 型別:ComponentInstance

描述完整元件樹 JSON 結構。在非受控模式下,元件樹就儲存在 <Designer /> 例項內部,而受控模式下,元件樹儲存在外部狀態。

但我們允許這兩種模式都可以訪問此狀態,這樣在開發視覺化搭建應用的過程中,就不用關心受控或非受控模式了,即一套程式碼同時相容受控與非受控模式。

selectedComponentIds

  • 評價:建議有
  • 型別:string[]

定義當前選中元件例項 id 列表。

雖然這個狀態業務也可以定義,但選中元件在視覺化搭建是一種常見行為,以後定義外掛、自定義元件也許都會讀取當前選中的元件,如果框架定義了此通用 key,那麼外掛和自定義元件就可無縫結合到任意業務程式碼裡。反之如果在業務層定義該狀態,外掛或者自定義元件也不知道如何標準的讀取到當前選中的元件。

canUndo, canRedo

  • 評價:建議有
  • 型別:boolean

描述當前狀態是否能撤銷或重做。

該狀態需要結合內建方法 undo() redo() 一起提供,屬於 “有了更好” 的狀態。但有時候也會產生困擾,比如你的應用分了多個 sheet,每個 sheet 內是一個畫布例項,而你希望撤銷重做可以跨 sheet,那就不適合用單例項提供的方法了。

方法

狀態引用不可變,引用方式有如下兩種。

第一種在任意 React 元件內透過 useDesigner 訪問,它不會變化,因此不會導致元件重渲染:

const { addComponent } = useDesigner();

第二種在任意元件元資訊內透過回撥訪問:

const tableMeta = {
  /** ... */
  runtimeProps: ({ addComponent }) => {},
};

getState()

  • 評價:必須有
  • 型別:() => State

獲取應用全部狀態,包括內建與業務自定義。

setState()

  • 評價:必須有
  • 型別:(state: State) => void

更新應用全部狀態,包括內建與業務自定義。

getComponentTree()

  • 評價:必須有
  • 型別:() => ComponentInstance

返回當前元件樹。

並不是有了 componentTree 狀態就萬事大吉了,很多回撥函式並不依賴元件樹重渲染,而僅僅在觸發時獲取其瞬時值必須呼叫此方法。

雖然該方法一定程度上可以用 getState().componentTree 代替,但元件樹概念太重要了,以至於單獨定義一個方法不會增加理解成本。另外在受控模式下,getState().componentTree 不一定等價於 getComponentTree(),因為前者是從 <Designer /> 拿元件樹,而後者直接請求外部狀態最新的元件樹,當元件樹受控模式沒有及時觸發渲染同步時,後者值會比前者更新。

setComponentTree()

  • 評價:必須有
  • 型別:(callback: (now: ComponentInstance) => ComponentInstance) => boolean

更新當前元件樹。

在非受控模式下等價於 setState() 修改 componentTree,但在非受控模式下,會直接透傳到外部狀態,直接修改一手元件樹,因此極端情況下表現更穩定。

addComponent()

  • 評價:必須有
  • 型別 (componentInstance, parentIdPath?, index?, position?) => void

新增元件例項。

基於 setComponentTree() 實現,但因為其太常見且意圖較為複雜,抽成一個獨立函式還是很有必要的。

  • componentInstance 必選,預設把元件例項新增到根節點的 children 位置。
  • parentIdPath 可選,描述要新增到的父節點 ID,當父節點沒定義元件 ID 時,也可以用例如 children.0 這種元件樹路徑代替,所以名稱不叫 parentId,而是 parentIdPath
  • index 可選,描述要新增到父節點子元素下標,比如新增到 children 的第幾項。
  • position 可選,描述要新增到父節點 children 還是 props.header 等位置,畢竟元件例項並不只有 children 一個地方。

deleteComponent()

  • 評價:必須有
  • 型別:(componentIdPath: string) => boolean

刪除元件例項。

基於 setComponentTree() 實現,但同理太常用,所以單獨提供。

這裡還有個細節,就是 componentIdPath 指可傳元件 ID,也可傳元件樹路徑,而真正刪除肯定要從樹上刪,框架內部為了快速從元件 ID 定位到 treePath,維護了一個對映表,因此使用該函式無論何時都是 O(1) 的時間複雜度。

getComponent()

  • 評價:必須有
  • 型別:(componentIdPath: string) => ComponentInstance

查詢元件例項。

基於 getComponentTree() 實現。“增刪” 都有了,“查” 還能沒有嗎?

setComponent()

  • 評價:必須有
  • 型別:(componentIdPath, callback) => boolean

修改元件例項。

基於 setComponentTree() 實現,“增改查” 都有了,就差一個 “改” 了。

setProps()

  • 評價:建議有
  • 型別:(componentIdPath, callback) => boolean

修改元件例項的 props。

基於 setComponent() 實現,因為修改元件 props 屬性比修改整個元件例項常見,建議實現。

getProps()

  • 評價:建議有
  • 型別:(componentIdPath) => any

獲取元件例項的 props。

基於 getComponent() 實現,同理,呼叫可能比 getComponent() 更常見,因此建議實現。

getComponents()

  • 評價:建議有
  • 型別:() => ComponentInstance[]

獲取全量元件例項陣列。

因為元件樹是樹狀結構,業務除了用遞迴方式遍歷外,還可以提供這種獲取打平形式的元件樹以備不時之需。

getParentId()

  • 評價:必須有
  • 型別:(componentIdPath: string) => string

獲取元件的父元件 ID。

以為 componentTree 為樹狀結構,所以直接從元件例項上找不到父節點,因此提供一個快速找父節點的函式是非常必要的。

當然框架內部實現尋找父節點肯定不會用遍歷,而是提前解析元件樹時就建立好關聯對映表,所有內建方法時間複雜度都是 O(1) 的。

getParentBy()

  • 評價:建議有
  • 型別:(componentIdPath: string, finder: (parent: ComponentInstance) => boolean) => string

一直向上尋找父節點,直到找到為止。

基於 getParentId() 實現,方便業務向上尋找符合條件的父節點。

setParent()

  • 評價:必須有
  • 型別:(componentIdPath, parentIdPath, index, position) => boolean

調整某個元件的父節點。引數和 addComponent() 很像,只是把第一個從元件例項改為了元件 ID,引數含義相同。

當畫布涉及元件跨父節點移動時,這個方法就顯得很關鍵了,雖然底層也是基於 setComponentTree 實現的。一個比較複雜的場景是,當元件跨節點移動時,在元件樹上操作還是比較複雜的,因為移除 + 新增無論先做哪個,都會導致元件樹變化,從而導致後一個操作位置可能錯誤。如果每次都重新定址效能會較差,如果想用聰明的方法繞過,邏輯還是比較複雜的,因此有必要內建該方法。

setComponentMeta()

  • 評價:必須有
  • 型別:(componentName: string, componentMeta: ComponentMeta) => void

更新元件元資訊。

提供這個方法其實對框架的挑戰比較大,在提供很多生命週期的情況下,隨時可能發生元件例項的更新,要保證整體邏輯符合預期,需要仔細設計一下。

getComponentMeta()

  • 評價:必須有
  • 型別:(componentName: string) => ComponentMeta

獲取元件元資訊。

既然可以註冊元件元資訊,就可以獲取它。注意透過 <Designer /> 受控或者非受控模式註冊,或者直接呼叫 setComponentMeta 註冊的元件元資訊都應該可以正常獲取到。

getComponentMetas()

  • 評價:建議有
  • 型別:() => ComponentMeta[]

批次獲取所有已註冊的元件元資訊。

說不定業務會有什麼特別的用途,建議提供。

clearComponentMetas()

  • 評價:建議有
  • 型別:() => void

清空所有元件元資訊。

說不定業務會有什麼特別的用途,建議提供。

setSelectedComponentIds()

  • 評價:建議有
  • 型別:(ids: string[]) => void

修改內建狀態 selectedComponentIds

如果你提供了 selectedComponentIds 這個內建狀態,那提供對應的修改方法就是強烈建議了。雖然也可透過 setState() 更新 selectedComponentIds Key 來實現。

getTreePath()

  • 評價:建議有
  • 型別:(componentIdPath: string) => string

根據元件 ID 查詢在元件樹上的路徑。

也許業務想要自己操作元件樹,那麼框架提供根據元件 ID 找到元件樹路徑的方法就挺合適。

undo(), redo()

  • 評價:建議有
  • 型別:() => void

撤銷,重做。

如果提供了 canUndocanRedo 內建狀態,那麼一定要提供 undo()redo() 內建函式。

getMergedProps()

  • 評價:建議有
  • 型別:(componentIdPath: string) => any

返回元件最終混合後的 props。

由於元件 props 可能來自元件樹,也可能來自 runtimeProps,為了防止傻傻分不清,因此規定 getProps() 僅獲取元件樹上序列化的 props,而 getMergedProps() 獲取了包含 runtimeProps 處理後的最終 props。

getComponentDom()

  • 評價:建議有
  • 型別:(componentIdPath: string) => HTMLElement

根據元件 ID 獲取 DOM 例項。

框架最好透過一些技巧,讓元件即便不用 forwardRef 也能拿到 DOM,那麼元件只要存在 DOM,就可以透過該方法拿到,非常方便。

afterDomRender()

  • 評價:建議有
  • 型別:(componentIdPath: string, callback: () => void) => Promise

當元件 ID 的 DOM 例項掛載後,執行 callback

因為元件 DOM 依賴渲染,所以不能保證 getComponentDom 時 DOM 真的完成了渲染,因此可以將時機放在 afterDomRender() 後,保證一定可以拿到 DOM。

總結

這一章我們設計了內建 API,設計思路總結如下:

  1. 從元件樹這個核心概念散開,設定了必要的 API,以及一些邏輯複雜,或者使用很方便的推薦 API。
  2. 雖然元件樹是樹狀結構,但內建 API 需要考慮易用性,所有操作都以元件 ID 作為引數,在內部實現時轉化為操作元件樹,並內建好 O(1) 時間複雜度的最佳化措施。
  3. 核心 API 只有寥寥幾個,其餘 API 都以便利性為目的提供,且都以核心 API 為基礎實現,這樣框架核心會更穩定,框架大部分 API 只是一種實現規則,業務利用核心 API 擁有更大的實現自由。
討論地址是:精讀《視覺化搭建內建 API》· Issue #467 · dt-fe/weekly

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

關注 前端精讀微信公眾號

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

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

相關文章