前端可視化搭建定義聯(lián)動協(xié)議實現(xiàn)
引言
雖然底層框架提供了通用的組件值與聯(lián)動配置,可以建立對組件任意 props 的映射,但這只是一個能力,還不是協(xié)議。
業(yè)務(wù)層是可以確定一個協(xié)議的,還要讓這個協(xié)議具有拓展性。
我們先從使用者角度設(shè)計 API,再看看如何根據(jù)已有的組件值與聯(lián)動能力去實現(xiàn)。
設(shè)計聯(lián)動協(xié)議
首先,不同的業(yè)務(wù)方會定義不同的聯(lián)動協(xié)議,因此該聯(lián)動協(xié)議需要通過拓展的方式注入:
import { createDesigner } from 'designer'
import { onReadComponentMeta } from 'linkage-protocol'
return <Designer onReadComponentMeta={onReadComponentMeta} />
首先可視化搭建框架支持 onReadComponentMeta 屬性,用于拓展所有已注冊的組件元信息,而聯(lián)動協(xié)議的拓展就是基于組件值與組件聯(lián)動能力的,因此這種是最合理的拓展方式。
之后我們就注冊了一個固定的聯(lián)動協(xié)議,它形如下:
{
"componentName": "input",
"linkage": [{
"target": "input1",
"do": {
"value": "{{ $self.value + 'hello' }}"
}
}]
}
只要在組件實例上定義 linkage 屬性,就可以生效聯(lián)動。比如上面的例子:
target: 聯(lián)動目標。do: 聯(lián)動效果,比如該例子為,組件 ID 為input1的組件,組件值同步為當前組件實例的組件值 +'hello'。$self: 描述自己實例,比如可以從$self.value拿到自己的組件值,從$self.props拿到自己的 props。
更近一步,target 還可以支持數(shù)組,就表示同時對多個組件生效相同規(guī)則。
我們還可以支持更復(fù)雜的語法,比如讓該組件可以同步其他組件值:
{
"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 都指向自己實例。
總結(jié)一下,該聯(lián)動協(xié)議允許組件實例實現(xiàn)以下效果:
- 設(shè)定組件值、組件 props 的聯(lián)動效果。
- 可以將自己的組件值同步給組件實例,也可以將其他組件值同步給自己。
基本上,可以滿足任意組件聯(lián)動到任意組件的訴求。而且甚至支持組件間傳遞,比如 A 組件的組件值同步組件 B, B 組件的組件值同步組件 C,那么 A 組件 setValue() 后,組件 B 和 組件 C 的組件值會同時更新。
實現(xiàn)聯(lián)動協(xié)議
以上聯(lián)動協(xié)議只是一種實現(xiàn),我們可以基于組件值與組件聯(lián)動設(shè)定任意協(xié)議,因此實現(xiàn)聯(lián)動協(xié)議的思維具備通用性,但為了方便,我們以上面說的這個協(xié)議為例子,說明如何用可視化搭建框架的基礎(chǔ)功能實現(xiàn)協(xié)議。
首先解讀組件實例的 linkage 屬性,將聯(lián)動定義轉(zhuǎn)化為組件聯(lián)動關(guān)系,因為聯(lián)動協(xié)議本質(zhì)上就是產(chǎn)生了組件聯(lián)動。接下來代碼片段比較長,因此會盡量使用代碼注釋來解釋:
const extendMeta = {
// 定義 valueRelates 關(guān)系,就是我們上一節(jié)提到的定義組件聯(lián)動關(guān)系的 key
valueRelates: ({ componentId, selector }) => {
// 利用 selector 讀取組件實例 linkage 屬性
// 由于 selector 的特性,會實時更新,因此聯(lián)動協(xié)議變化后,聯(lián)動狀態(tài)也會實時更新
const linkage = selector(({ componentInstance }) => componentInstance.linkage)
// 返回聯(lián)動數(shù)組,結(jié)構(gòu): [{ sourceComponentId, targetComponentId, payload }]
return linkage.map(relation => {
const result = [];
// 定義此類聯(lián)動類型,就叫做 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}']`),
),
};
// 經(jīng)過上面的代碼,表達式里無論是 $self. 還是 $deps[0]. 都轉(zhuǎn)化為了
// $deps[componentId] 這個具體組件 ID,這樣后面處理流程會簡單而統(tǒng)一
// 讀取 deps,并定義 dep 組件作為 source,target 作為目標組件
// 這是最關(guān)鍵的一步,將 dep -> target 關(guān)系綁定上
relation.target.forEach((targetComponentId) => {
if (relation.deps) {
relation.deps.forEach((depIdPath: string) => {
result.push({
sourceComponentId: depIdPath,
targetComponentId,
});
});
}
// 定義自己到 target 目標組件的聯(lián)動關(guān)系
result.push({
sourceComponentId: componentId,
targetComponentId,
payload,
});
});
return result;
}).flat()
}
}
上述代碼利用 valueRelates,將聯(lián)動協(xié)議的關(guān)聯(lián)關(guān)系提取出來,轉(zhuǎn)化為值聯(lián)動關(guān)系。
接著,我們要實現(xiàn) props 同步功能,實現(xiàn)這個功能自然是利用 runtimeProps 以及 selector.relates,將關(guān)聯(lián)到當前組件的組件值,按照聯(lián)動協(xié)議的表達式執(zhí)行,并更新到對應(yīng) key 上,下面是大致實現(xiàn)思路:
const extendMeta = {
runtimeProps: ({ componentId, selector, getProps, getMergedProps }) => {
// 拿到作用于自己的值關(guān)聯(lián)信息: relates
const relates = selector(({ relates }) => relates);
// 記錄最終因為值聯(lián)動而影響的 props
let relationProps: any = {};
// 記錄關(guān)聯(lián)到自己的組件此時組件值
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 聯(lián)動
if (isObject(relate.payload?.do?.props)) {
Object.keys(relate.payload?.do?.props).forEach((propsKey) => {
relationProps = set(
propsKey,
selector(
() =>
// 這個函數(shù)是關(guān)鍵,傳入組件 props 與表達式,返回新的 props 值
getExpressionResult(
get(propsKey, relate.payload?.do?.props),
expressionArgs,
),
{
compare: equals,
// 根據(jù)表達式數(shù)量可能不同,所以不啟用緩存
cache: false,
},
),
relationProps,
);
});
}
});
return relationProps
}
}
其中比較復(fù)雜函數(shù)就是 getExpressionResult,它要解析表達式并執(zhí)行,原理就是利用代碼沙盒執(zhí)行字符串函數(shù),并利用正則替換變量名以匹配上下文中的變量,大致代碼如下:
// 代碼執(zhí)行沙盒,傳入字符串 js 函數(shù),利用 new Function 執(zhí)行
function sandBox(code: string) {
// with 是關(guān)鍵,利用 with 定制代碼執(zhí)行的上下文
const withStr = `with(obj) {
$[code]
}`;
const fun = new Function('obj', withStr);
return function (obj: any) {
return fun(obj);
};
}
// 獲取沙盒代碼執(zhí)行結(jié)果,可以傳入?yún)?shù)覆蓋沙盒內(nèi)上下文
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('}}')) {
// {{}} 內(nèi)的表達式
let codeContent = code.slice(2, code.length - 2);
// 將形如 $deps['id'].props.a.b.c
// 轉(zhuǎn)換為 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í)行時替換代碼上下文的關(guān)鍵。
總結(jié)
componentMeta.valueRelates 與 componentMeta.runtimeProps 可以靈活的定義組件聯(lián)動關(guān)系,與更新組件 props,利用這兩個聲明式 API,甚至可以實現(xiàn)組件聯(lián)動協(xié)議。總結(jié)一下,包含以下幾個關(guān)鍵點:
- 將
deps和target利用valueRelates轉(zhuǎn)化為組件值關(guān)聯(lián)關(guān)系。 - 將聯(lián)動協(xié)議定義的相對關(guān)系(比較容易寫于容易記)轉(zhuǎn)化為絕對關(guān)系(利用 componentId 定位),方便框架處理。
- 利用
with執(zhí)行表達式上下文。 - 利用
runtimeProps+selector實現(xiàn)注入組件 props 與響應(yīng)聯(lián)動值relates變化,從而實現(xiàn)按需聯(lián)動。
討論地址是:精讀《定義聯(lián)動協(xié)議》· Issue #471 · dt-fe/weekly
以上就是前端可視化搭建定義聯(lián)動協(xié)議實現(xiàn)的詳細內(nèi)容,更多關(guān)于前端可視化搭建聯(lián)動協(xié)議的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
微信小程序 中wx.chooseAddress(OBJECT)實例詳解
這篇文章主要介紹了微信小程序 中wx.chooseAddress(OBJECT)實例詳解的相關(guān)資料,需要的朋友可以參考下2017-03-03
JS封裝轉(zhuǎn)換前后端接口數(shù)據(jù)格式工具函數(shù)下劃線<=>大寫
這篇文章主要為大家介紹了JS優(yōu)雅封裝轉(zhuǎn)換前后端接口數(shù)據(jù)格式工具函數(shù)下劃線<=>大寫實現(xiàn)詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-03-03
umi插件開發(fā)仿dumi項目實現(xiàn)頁面布局詳解
這篇文章主要為大家介紹了umi插件開發(fā)仿dumi項目實現(xiàn)頁面布局詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-01-01

