JS前端畫布與組件元信息數(shù)據(jù)流示例詳解
正文
接下來需要解決兩個問題:
- 可視化搭建的其他業(yè)務(wù)元素如何與畫布交互。比如拓展屬性配置面板、圖層列表、拖拽添加組件、定位錨點、主題等等。
runtimeProps
如何訪問到當前組件實例的props
。
這兩個問題非常重要,而恰好又可以通過良好的數(shù)據(jù)流設(shè)計一次性解決,接下來讓我們分別分析討論一下。
問題一:可視化搭建的其他業(yè)務(wù)元素如何與畫布交互。比如拓展屬性配置面板、圖層列表、拖拽添加組件、定位錨點、主題等等
需要設(shè)計一個 Hooks API,可以訪問到畫布提供的方法、數(shù)據(jù)。在 React 設(shè)計中,訪問 Hooks API 需要在一定上下文內(nèi),所以可以將 <Designer>
拆為 <Designer>
與 <Canvas>
,其中 <Designer>
提供 Hooks 上下文,<Canvas>
負責渲染畫布。這樣開發(fā)者的使用方式就變成了這樣:
import { createDesigner } from 'designer' const { Designer, Canvas, useDesigner } = createDesigner() const EditPanel = { const { addComponent } = useDesigner() return <button onClick={() => addComponent(/** ... */)}>創(chuàng)建組件</button> } const App = () => { <Designer> <Canvas /> <EditPanel /> </Designer> }
為了支持多個 Designer 實例間隔離,通過 createDesigner
創(chuàng)建一套上下文獨立的 API,這樣就可以讓畫布、配置面板同時用 Designer 實現(xiàn),用一套技術(shù)方案同時實現(xiàn)畫布與配置表單,這樣學習上下文、組件規(guī)范都可以統(tǒng)一為一套,表單、畫布能力也可以共享。
在 <Designer>
內(nèi)的組件可以通過 useDesigner
直接訪問數(shù)據(jù)與方法,比如上面例子在直接訪問內(nèi)置方法 addComponent
時,不需要附加任何參加,而 addComponent
方法也永遠保持引用不變,此時 useDesigner
不會導(dǎo)致 EditPanel
重渲染。
如果需要訪問當前組件樹,并在組件樹變化時重渲染,可以通過如下方式訪問:
const EditPanel = { const { componentTree } = useDesigner(state => ({ componentTree: state.componentTree })) }
該寫法的效果是,當 state.componentTree
變化了,會觸發(fā) EditPanel
重新渲染,并拿到最新值。
同時也可以傳入第二個參數(shù) compare
自定義對比方法,默認為 shallowEqual
:
useDesigner( (state) => ({ componentTree: state.componentTree, }), isEqual );
如此一來,無論給畫布拓展多少 UI 元素都沒有問題,而且 UI 元素可以自由的訪問畫布方法與數(shù)據(jù)。
問題二:runtimeProps
如何訪問到當前組件實例的 props
在 componentMeta.runtimeProps
中,我們構(gòu)造一個 selector
函數(shù)用于訪問當前組件 props:
const divMeta = { componentName: "div", runtimeProps: ({ selector }) => { const name = selector(({ props }) => props.name) return { fullName: `full-${name}` } } element: /** ... */ };
首先支持從 runtimeProps
回調(diào)里拿到 selector
,并且該 selector
支持傳入一個回調(diào)函數(shù),該回調(diào)函數(shù)的參數(shù)中 props
指向當前組件實例的 props
,通過該方法就可以訪問組件 props 了。
該 selector 僅在 props.name
改變時重新執(zhí)行,并且也遵循 compare
對比規(guī)則,即當 props.name
變化時,selector
回調(diào)函數(shù)的返回值通過 compare
與上一次值進行對比,如果沒有變化就返回上一次的舊值,變化了則返回新值。默認對比函數(shù)為 shallowEqual
,與 useDesigner
類似,也可以在第二個參數(shù)位置覆寫 compare
方法。
那組件元信息如何訪問內(nèi)置靜態(tài)方法呢?由于靜態(tài)方法引用不變,因此可以在 selector
同級直接傳入:
const divMeta = { componentName: "div", runtimeProps: ({ addComponent }) => { return { add: () => { /** addComponent(...) */ } } } element: /** ... */ };
如此一來,我們就將數(shù)據(jù)流與組件元信息打通了,即 UI 可以通過 useDesigner
訪問與操作數(shù)據(jù)流,組件元信息也可以直接拿到方法,或通過 selector
拿到數(shù)據(jù),相應(yīng)的也可以訪問與操作數(shù)據(jù)流。這樣的設(shè)計在以后拓展更多組件元信息函數(shù)時,都可以繼承下來,開發(fā)者只要學習一次語法,就可以獲得非常強力的拓展性。
拓展應(yīng)用狀態(tài)與靜態(tài)方法
剛才介紹了一些內(nèi)置的狀態(tài)(componentTree
)與方法(addComponent
),在下一接會系統(tǒng)介紹筆者梳理了哪些內(nèi)置狀態(tài)與方法。首先拋開內(nèi)置狀態(tài)與方法不談,應(yīng)用肯定需要定義自己的狀態(tài)與方法,我們可以提供兩種模式給用戶。
第一種是應(yīng)用的狀態(tài)與方法定義在外部,對應(yīng)受控模式。
假設(shè)你的應(yīng)用在對接 Designer 之前就已經(jīng)用 Redux、Dva、Zustand 等狀態(tài)管理庫,那么就可以使用受控模式直接接入:
const App = () => { // 偽代碼,不管是 useState 還是其他數(shù)據(jù)流管理狀態(tài),假這里拿到了數(shù)據(jù)與方法 const { getAppInfo } = useSomeLib(); const { userName } = useSomeLib("userName"); return <Designer actions={{ getAppInfo }} state={{ userName }} />; };
將方法傳給 actions
,狀態(tài)傳給 state
。
第二種是應(yīng)用的狀態(tài)與方法通過 <Designer>
定義,對用非受控模式。
假設(shè)你的應(yīng)用之前沒有使用任何數(shù)據(jù)流,那么也可以直接將 Designer 的數(shù)據(jù)流作為項目數(shù)據(jù)流使用:
import { createMiddleware, createDesigner } from "designer"; const middleware1 = createMiddleware({ state: { userName: "bob " }, actions: { getAppInfo: () => {} }, }); const { Designer } = createDesigner(middleware1); const App = () => { return <Designer />; };
通過 createMiddleware
創(chuàng)建一個中間件定義狀態(tài)與函數(shù),傳入 createDesigner
即可生效。
也可以在 createMiddleware
里通過第二個參數(shù)定義自定義 hooks,或者拿到方法更改 State:
const middleware1 = createMiddleware( { state: { userName: "bob " }, }, ({ setState }) => { const setUserName = React.useCallback((newName: string) => { setState((state) => ({ ...state, userName: newName, })); }); return { setUserName }; } );
Designer 內(nèi)部采用最樸素的 Redux 管理狀態(tài),提供了最基礎(chǔ)的 getState
與 setState
獲取與修改狀態(tài),基于它們封裝業(yè)務(wù)函數(shù)即可。
無論是受控模式,還是非受控模式(亦或兩種模式同時使用),定義的狀態(tài)與方法都可以在以下兩個位置訪問,第一個位置是 useDesigner
:
const { /** 自定義函數(shù) */, setUserName, /** 自定義函數(shù) */ getAppInfo, /** 內(nèi)置函數(shù) */ addComponent, // 內(nèi)置變量 componentTree, // 自定義變量 userNamee } = useDesigner(state => ({ componentTree: state.componentTree, userName: state.userName }))
第二個位置是組件元信息上的回調(diào)函數(shù),比如 runtimeProps
:
const divMeta = { componentName: "div", runtimeProps: ({ selector, /** 自定義函數(shù) */, setUserName, /** 自定義函數(shù) */ getAppInfo, /** 內(nèi)置函數(shù) */ addComponent }) => { const { /** 內(nèi)置變量 */ componentTree, /** 自定義變量 */ userName } = selector(({ state }) => ({ componentTree: state.componentTree, userName: state.userName })) return { componentTree, userName } } element: /** ... */ };
至此,我們實現(xiàn)了一套完整的數(shù)據(jù)流定義,包括:
- 不同 Designer 之間上下文隔離。
- 可無縫對接項目數(shù)據(jù)流,也可作為獨立數(shù)據(jù)流方案提供。
- 內(nèi)置變量與函數(shù)與自定義變量、函數(shù)混合。
- 無論在 UI 通過
useDesigner
,還是在組件元信息通過selector
都可訪問這些變量與函數(shù)。
總結(jié)
一個基本可用的可視化搭建框架在本章就算設(shè)計完了。但這只是可視化搭建問題的冰山一角,未來的章節(jié),筆者會逐漸為大家介紹更多可視化搭建的設(shè)計。
但無論框架未來怎么發(fā)展,也永遠會基于這前三章的基本設(shè)定,總結(jié)一下,這三章的基本設(shè)定就是:設(shè)計一個邏輯與 UI 分離的可視化搭建協(xié)議,數(shù)據(jù)流、組件元信息、組件實例是永遠的鐵三角,數(shù)據(jù)流可以對接任意已存在的實現(xiàn),或基于 Designer 規(guī)范實現(xiàn),組件元信息與組件實例僅存儲最基本信息,得益于數(shù)據(jù)流的自定義能力,以及無論何處都有完全的數(shù)據(jù)流訪問能力,使業(yè)務(wù)框架既遵循規(guī)則,又可以千變?nèi)f化。
拋開具體 API 設(shè)計或者命名不談,一個有簡潔、抽象,又提供極少量 API 卻能滿足所有業(yè)務(wù)定制訴求,是可視化搭建永遠追求的目標。只要熟悉了這套規(guī)范,就可以幾乎僅根據(jù)業(yè)務(wù)表現(xiàn),一眼猜出是基于哪些 API 封裝實現(xiàn)的,那么維護成本與理解成本將大大降低,規(guī)范的意義就體現(xiàn)在這里。
也許有同學會覺得,現(xiàn)在各個大廠都有無數(shù)可視化搭建的實現(xiàn),可視化搭建概念都已經(jīng)爛大街了,為什么還要重新設(shè)計一個呢?
因為也許數(shù)量不代表質(zhì)量,維護的時間越久,參與的同學越多,越容易使設(shè)計變得冗余,概念變得復(fù)雜,要對抗這些遞增的熵,唯有不斷重新設(shè)計,從零開始反思方案。
下一講理論思考會少一些,介紹可視化搭建框架會考慮內(nèi)置哪些變量與方法,更多關(guān)于JS畫布與組件元信息數(shù)據(jù)流的資料請關(guān)注腳本之家其它相關(guān)文章!
版權(quán)聲明:自由轉(zhuǎn)載-非商用-非衍生-保持署名(創(chuàng)意共享 3.0 許可證)