首頁 > 軟體

JS前端畫布與元件元資訊資料流範例詳解

2023-02-08 22:01:39

正文

接下來需要解決兩個問題:

  • 視覺化搭建的其他業務元素如何與畫布互動。比如拓展屬性設定面板、圖層列表、拖拽新增元件、定位錨點、主題等等。
  • runtimeProps 如何存取到當前元件範例的 props

這兩個問題非常重要,而恰好又可以通過良好的資料流設計一次性解決,接下來讓我們分別分析討論一下。

問題一:視覺化搭建的其他業務元素如何與畫布互動。比如拓展屬性設定面板、圖層列表、拖拽新增元件、定位錨點、主題等等

需要設計一個 Hooks API,可以存取到畫布提供的方法、資料。在 React 設計中,存取 Hooks API 需要在一定上下文內,所以可以將 <Designer> 拆為 <Designer><Canvas>,其中 <Designer> 提供 Hooks 上下文,<Canvas> 負責渲染畫布。這樣開發者的使用方式就變成了這樣:

import { createDesigner } from 'designer'
const { Designer, Canvas, useDesigner } = createDesigner()
const EditPanel = {
  const { addComponent } = useDesigner()
  return <button onClick={() => addComponent(/** ... */)}>建立元件</button>
}
const App = () => {
  <Designer>
    <Canvas />
    <EditPanel />
  </Designer>
}

為了支援多個 Designer 範例間隔離,通過 createDesigner 建立一套上下文獨立的 API,這樣就可以讓畫布、設定面板同時用 Designer 實現,用一套技術方案同時實現畫布與設定表單,這樣學習上下文、元件規範都可以統一為一套,表單、畫布能力也可以共用。

<Designer> 內的元件可以通過 useDesigner 直接存取資料與方法,比如上面例子在直接存取內建方法 addComponent 時,不需要附加任何參加,而 addComponent 方法也永遠保持參照不變,此時 useDesigner 不會導致 EditPanel 重渲染。

如果需要存取當前元件樹,並在元件樹變化時重渲染,可以通過如下方式存取:

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

該寫法的效果是,當 state.componentTree 變化了,會觸發 EditPanel 重新渲染,並拿到最新值。

同時也可以傳入第二個引數 compare 自定義對比方法,預設為 shallowEqual

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

如此一來,無論給畫布拓展多少 UI 元素都沒有問題,而且 UI 元素可以自由的存取畫布方法與資料。

問題二:runtimeProps 如何存取到當前元件範例的 props

componentMeta.runtimeProps 中,我們構造一個 selector 函數用於存取當前元件 props:

const divMeta = {
  componentName: "div",
  runtimeProps: ({ selector }) => {
    const name = selector(({ props }) => props.name)
    return {
      fullName: `full-${name}`
    }
  }
  element: /** ... */
};

首先支援從 runtimeProps 回撥裡拿到 selector,並且該 selector 支援傳入一個回撥函數,該回撥函數的引數中 props 指向當前元件範例的 props,通過該方法就可以存取元件 props 了。

該 selector 僅在 props.name 改變時重新執行,並且也遵循 compare 對比規則,即當 props.name 變化時,selector 回撥函數的返回值通過 compare 與上一次值進行對比,如果沒有變化就返回上一次的舊值,變化了則返回新值。預設對比函數為 shallowEqual,與 useDesigner 類似,也可以在第二個引數位置覆寫 compare 方法。

那元件元資訊如何存取內建靜態方法呢?由於靜態方法參照不變,因此可以在 selector 同級直接傳入:

const divMeta = {
  componentName: "div",
  runtimeProps: ({ addComponent }) => {
    return {
      add: () => {
        /** addComponent(...) */
      }
    }
  }
  element: /** ... */
};

如此一來,我們就將資料流與元件元資訊打通了,即 UI 可以通過 useDesigner 存取與運算元據流,元件元資訊也可以直接拿到方法,或通過 selector 拿到資料,相應的也可以存取與運算元據流。這樣的設計在以後拓展更多元件元資訊函數時,都可以繼承下來,開發者只要學習一次語法,就可以獲得非常強力的拓展性。

拓展應用狀態與靜態方法

剛才介紹了一些內建的狀態(componentTree)與方法(addComponent),在下一接會系統介紹筆者梳理了哪些內建狀態與方法。首先拋開內建狀態與方法不談,應用肯定需要定義自己的狀態與方法,我們可以提供兩種模式給使用者。

第一種是應用的狀態與方法定義在外部,對應受控模式。

假設你的應用在對接 Designer 之前就已經用 Redux、Dva、Zustand 等狀態管理庫,那麼就可以使用受控模式直接接入:

const App = () => {
  // 虛擬碼,不管是 useState 還是其他資料流管理狀態,假這裡拿到了資料與方法
  const { getAppInfo } = useSomeLib();
  const { userName } = useSomeLib("userName");
  return <Designer actions={{ getAppInfo }} state={{ userName }} />;
};

將方法傳給 actions,狀態傳給 state

第二種是應用的狀態與方法通過 <Designer> 定義,對用非受控模式。

假設你的應用之前沒有使用任何資料流,那麼也可以直接將 Designer 的資料流作為專案資料流使用:

import { createMiddleware, createDesigner } from "designer";
const middleware1 = createMiddleware({
  state: { userName: "bob " },
  actions: { getAppInfo: () => {} },
});
const { Designer } = createDesigner(middleware1);
const App = () => {
  return <Designer />;
};

通過 createMiddleware 建立一箇中介軟體定義狀態與函數,傳入 createDesigner 即可生效。

也可以在 createMiddleware 裡通過第二個引數定義自定義 hooks,或者拿到方法更改 State:

const middleware1 = createMiddleware(
  {
    state: { userName: "bob " },
  },
  ({ setState }) => {
    const setUserName = React.useCallback((newName: string) => {
      setState((state) => ({
        ...state,
        userName: newName,
      }));
    });
    return { setUserName };
  }
);

Designer 內部採用最樸素的 Redux 管理狀態,提供了最基礎的 getStatesetState 獲取與修改狀態,基於它們封裝業務函數即可。

無論是受控模式,還是非受控模式(亦或兩種模式同時使用),定義的狀態與方法都可以在以下兩個位置存取,第一個位置是 useDesigner

const {
  /** 自定義函數 */,
  setUserName,
  /** 自定義函數 */
  getAppInfo,
  /** 內建函數 */
  addComponent,
  // 內建變數
  componentTree,
  // 自定義變數
  userNamee
} = useDesigner(state => ({
  componentTree: state.componentTree,
  userName: state.userName
}))

第二個位置是元件元資訊上的回撥函數,比如 runtimeProps

const divMeta = {
  componentName: "div",
  runtimeProps: ({
    selector,
    /** 自定義函數 */,
    setUserName,
    /** 自定義函數 */
    getAppInfo,
    /** 內建函數 */
    addComponent
   }) => {
    const {
      /** 內建變數 */
      componentTree,
      /** 自定義變數 */
      userName
    } = selector(({ state }) => ({
      componentTree: state.componentTree,
      userName: state.userName
    }))
    return { componentTree, userName }
  }
  element: /** ... */
};

至此,我們實現了一套完整的資料流定義,包括:

  • 不同 Designer 之間上下文隔離。
  • 可無縫對接專案資料流,也可作為獨立資料流方案提供。
  • 內建變數與函數與自定義變數、函數混合。
  • 無論在 UI 通過 useDesigner,還是在元件元資訊通過 selector 都可存取這些變數與函數。

總結

一個基本可用的視覺化搭建框架在本章就算設計完了。但這只是視覺化搭建問題的冰山一角,未來的章節,筆者會逐漸為大家介紹更多視覺化搭建的設計。

但無論框架未來怎麼發展,也永遠會基於這前三章的基本設定,總結一下,這三章的基本設定就是:設計一個邏輯與 UI 分離的視覺化搭建協定,資料流、元件元資訊、元件範例是永遠的鐵三角,資料流可以對接任意已存在的實現,或基於 Designer 規範實現,元件元資訊與元件範例僅儲存最基本資訊,得益於資料流的自定義能力,以及無論何處都有完全的資料流存取能力,使業務框架既遵循規則,又可以千變萬化。

拋開具體 API 設計或者命名不談,一個有簡潔、抽象,又提供極少量 API 卻能滿足所有業務客製化訴求,是視覺化搭建永遠追求的目標。只要熟悉了這套規範,就可以幾乎僅根據業務表現,一眼猜出是基於哪些 API 封裝實現的,那麼維護成本與理解成本將大大降低,規範的意義就體現在這裡。

也許有同學會覺得,現在各個大廠都有無數視覺化搭建的實現,視覺化搭建概念都已經爛大街了,為什麼還要重新設計一個呢?

因為也許數量不代表質量,維護的時間越久,參與的同學越多,越容易使設計變得冗餘,概念變得複雜,要對抗這些遞增的熵,唯有不斷重新設計,從零開始反思方案。

下一講理論思考會少一些,介紹視覺化搭建框架會考慮內建哪些變數與方法,更多關於JS畫布與元件元資訊資料流的資料請關注it145.com其它相關文章!

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


IT145.com E-mail:sddin#qq.com