前端可視化搭建定義聯動協議實現
引言
雖然底層框架提供了通用的組件值與聯動配置,可以建立對組件任意 props 的映射,但這只是一個能力,還不是協議。
業(yè)務層是可以確定一個協議的,還要讓這個協議具有拓展性。
我們先從使用者角度設計 API,再看看如何根據已有的組件值與聯動能力去實現。
設計聯動協議
首先,不同的業(yè)務方會定義不同的聯動協議,因此該聯動協議需要通過拓展的方式注入:
import { createDesigner } from 'designer' import { onReadComponentMeta } from 'linkage-protocol' return <Designer onReadComponentMeta={onReadComponentMeta} />
首先可視化搭建框架支持 onReadComponentMeta
屬性,用于拓展所有已注冊的組件元信息,而聯動協議的拓展就是基于組件值與組件聯動能力的,因此這種是最合理的拓展方式。
之后我們就注冊了一個固定的聯動協議,它形如下:
{ "componentName": "input", "linkage": [{ "target": "input1", "do": { "value": "{{ $self.value + 'hello' }}" } }] }
只要在組件實例上定義 linkage
屬性,就可以生效聯動。比如上面的例子:
target
: 聯動目標。do
: 聯動效果,比如該例子為,組件 ID 為input1
的組件,組件值同步為當前組件實例的組件值 +'hello'
。$self
: 描述自己實例,比如可以從$self.value
拿到自己的組件值,從$self.props
拿到自己的 props。
更近一步,target
還可以支持數組,就表示同時對多個組件生效相同規(guī)則。
我們還可以支持更復雜的語法,比如讓該組件可以同步其他組件值:
{ "componentName": "input", "linkage": [{ "deps": ["input1", "input2"] "props": { "text": "{{ $deps[0].value + deps[1].value }}" } }] }
上面的例子表示,該組件實例的 props.text
同步為 input1 + input2 的組件值:
deps
: 描述依賴列表,每個依賴實例都可以在表達式里用$deps[]
訪問到,比如$deps[0].props
可以訪問組件 ID 為input1
組件的 props。props
: 同步組件的 props。
如果定義了 target
則作用于目標組件,未定義 target
則作用于自身。但無論如何,表達式的 $self
都指向自己實例。
總結一下,該聯動協議允許組件實例實現以下效果:
- 設定組件值、組件 props 的聯動效果。
- 可以將自己的組件值同步給組件實例,也可以將其他組件值同步給自己。
基本上,可以滿足任意組件聯動到任意組件的訴求。而且甚至支持組件間傳遞,比如 A 組件的組件值同步組件 B, B 組件的組件值同步組件 C,那么 A 組件 setValue()
后,組件 B 和 組件 C 的組件值會同時更新。
實現聯動協議
以上聯動協議只是一種實現,我們可以基于組件值與組件聯動設定任意協議,因此實現聯動協議的思維具備通用性,但為了方便,我們以上面說的這個協議為例子,說明如何用可視化搭建框架的基礎功能實現協議。
首先解讀組件實例的 linkage
屬性,將聯動定義轉化為組件聯動關系,因為聯動協議本質上就是產生了組件聯動。接下來代碼片段比較長,因此會盡量使用代碼注釋來解釋:
const extendMeta = { // 定義 valueRelates 關系,就是我們上一節(jié)提到的定義組件聯動關系的 key valueRelates: ({ componentId, selector }) => { // 利用 selector 讀取組件實例 linkage 屬性 // 由于 selector 的特性,會實時更新,因此聯動協議變化后,聯動狀態(tài)也會實時更新 const linkage = selector(({ componentInstance }) => componentInstance.linkage) // 返回聯動數組,結構: [{ sourceComponentId, targetComponentId, payload }] return linkage.map(relation => { const result = []; // 定義此類聯動類型,就叫做 simpleRelation const payload = { type: 'simpleRelation', do: JSON.parse( JSON.stringify(relation.do) // 將 $deps[index] 替換為 $deps[componentId] .replace( /\$deps\[([0-9]+)\]/g, (match: string, index: string) => `$deps['${relation.deps[Number(index)]}']`, ) // 將 $self 替換為 $deps[componentId] .replace(/\$self/g, () => `$deps['${componentId}']`), ), }; // 經過上面的代碼,表達式里無論是 $self. 還是 $deps[0]. 都轉化為了 // $deps[componentId] 這個具體組件 ID,這樣后面處理流程會簡單而統一 // 讀取 deps,并定義 dep 組件作為 source,target 作為目標組件 // 這是最關鍵的一步,將 dep -> target 關系綁定上 relation.target.forEach((targetComponentId) => { if (relation.deps) { relation.deps.forEach((depIdPath: string) => { result.push({ sourceComponentId: depIdPath, targetComponentId, }); }); } // 定義自己到 target 目標組件的聯動關系 result.push({ sourceComponentId: componentId, targetComponentId, payload, }); }); return result; }).flat() } }
上述代碼利用 valueRelates
,將聯動協議的關聯關系提取出來,轉化為值聯動關系。
接著,我們要實現 props 同步功能,實現這個功能自然是利用 runtimeProps
以及 selector.relates
,將關聯到當前組件的組件值,按照聯動協議的表達式執(zhí)行,并更新到對應 key 上,下面是大致實現思路:
const extendMeta = { runtimeProps: ({ componentId, selector, getProps, getMergedProps }) => { // 拿到作用于自己的值關聯信息: relates const relates = selector(({ relates }) => relates); // 記錄最終因為值聯動而影響的 props let relationProps: any = {}; // 記錄關聯到自己的組件此時組件值 const $deps = relates?.reduce( (result, next) => ({ ...result, [next.componentId]: { value: next.value, }, }), {}, ); // 為了讓每個依賴變化都能生效,多對一每一項 do 都帶過來了,需要按照 relationIndex 先去重 relates .filter((relate) => relate.payload?.type === 'simpleRelation') .forEach((relate) => { const expressionArgs = { // $deps[].value 指向依賴的 value $deps, get, getProps: relate.componentId === componentId ? getProps : getMergedProps, }; // 處理 props 聯動 if (isObject(relate.payload?.do?.props)) { Object.keys(relate.payload?.do?.props).forEach((propsKey) => { relationProps = set( propsKey, selector( () => // 這個函數是關鍵,傳入組件 props 與表達式,返回新的 props 值 getExpressionResult( get(propsKey, relate.payload?.do?.props), expressionArgs, ), { compare: equals, // 根據表達式數量可能不同,所以不啟用緩存 cache: false, }, ), relationProps, ); }); } }); return relationProps } }
其中比較復雜函數就是 getExpressionResult
,它要解析表達式并執(zhí)行,原理就是利用代碼沙盒執(zhí)行字符串函數,并利用正則替換變量名以匹配上下文中的變量,大致代碼如下:
// 代碼執(zhí)行沙盒,傳入字符串 js 函數,利用 new Function 執(zhí)行 function sandBox(code: string) { // with 是關鍵,利用 with 定制代碼執(zhí)行的上下文 const withStr = `with(obj) { $[code] }`; const fun = new Function('obj', withStr); return function (obj: any) { return fun(obj); }; } // 獲取沙盒代碼執(zhí)行結果,可以傳入參數覆蓋沙盒內上下文 function getSandBoxReturnValue(code: string, args = {}) { try { return sandBox(code)(args); } catch (error) { // eslint-disable-next-line no-console console.warn(error); } } // 如果對象是字符串則直接返回,是 {{}} 表達式則執(zhí)行后返回 function getExpressionResult(code: string, args = {}) { if (code.startsWith('{{') && code.endsWith('}}')) { // {{}} 內的表達式 let codeContent = code.slice(2, code.length - 2); // 將形如 $deps['id'].props.a.b.c // 轉換為 get('a.b.c', getProps('id')) codeContent = codeContent.replace( /\$deps\[['"]([a-zA-Z0-9]*)['"]\]\.props\.([a-zA-Z0-9.]*)/g, (str: string, componentId: string, propsKeyPath: string) => { return `get('${propsKeyPath}', getProps('${componentId}'))`; }, ); return getSandBoxReturnValue(`return ${codeContent}`, args); } return code; }
其中 with 是沙盒執(zhí)行時替換代碼上下文的關鍵。
總結
componentMeta.valueRelates
與 componentMeta.runtimeProps
可以靈活的定義組件聯動關系,與更新組件 props,利用這兩個聲明式 API,甚至可以實現組件聯動協議??偨Y一下,包含以下幾個關鍵點:
- 將
deps
和target
利用valueRelates
轉化為組件值關聯關系。 - 將聯動協議定義的相對關系(比較容易寫于容易記)轉化為絕對關系(利用 componentId 定位),方便框架處理。
- 利用
with
執(zhí)行表達式上下文。 - 利用
runtimeProps
+selector
實現注入組件 props 與響應聯動值relates
變化,從而實現按需聯動。
討論地址是:精讀《定義聯動協議》· Issue #471 · dt-fe/weekly
以上就是前端可視化搭建定義聯動協議實現的詳細內容,更多關于前端可視化搭建聯動協議的資料請關注腳本之家其它相關文章!
相關文章
微信小程序promsie.all和promise順序執(zhí)行
這篇文章主要介紹了微信小程序promsie.all和promise順序執(zhí)行的相關資料,希望通過本文能幫助到大家,需要的朋友可以參考下2017-10-10微信小程序Server端環(huán)境配置詳解(SSL, Nginx HTTPS,TLS 1.2 升級)
這篇文章主要介紹了微信小程序Server端環(huán)境配置詳解(SSL, Nginx HTTPS,TLS 1.2 升級)的相關資料,需要的朋友可以參考下2017-01-01