当前期刊数: 274
虽然底层框架提供了通用的组件值与联动配置,可以建立对组件任意 props 的映射,但这只是一个能力,还不是协议。
业务层是可以确定一个协议的,还要让这个协议具有拓展性。
我们先从使用者角度设计 API,再看看如何根据已有的组件值与联动能力去实现。
设计联动协议
首先,不同的业务方会定义不同的联动协议,因此该联动协议需要通过拓展的方式注入:
1 2 3 4
| import { createDesigner } from 'designer' import { onReadComponentMeta } from 'linkage-protocol'
return <Designer onReadComponentMeta={onReadComponentMeta} />
|
首先可视化搭建框架支持 onReadComponentMeta
属性,用于拓展所有已注册的组件元信息,而联动协议的拓展就是基于组件值与组件联动能力的,因此这种是最合理的拓展方式。
之后我们就注册了一个固定的联动协议,它形如下:
1 2 3 4 5 6 7 8 9
| { "componentName": "input", "linkage": [{ "target": "input1", "do": { "value": "{{ $self.value + 'hello' }}" } }] }
|
只要在组件实例上定义 linkage
属性,就可以生效联动。比如上面的例子:
target
: 联动目标。
do
: 联动效果,比如该例子为,组件 ID 为 input1
的组件,组件值同步为当前组件实例的组件值 + 'hello'
。
$self
: 描述自己实例,比如可以从 $self.value
拿到自己的组件值,从 $self.props
拿到自己的 props。
更近一步,target
还可以支持数组,就表示同时对多个组件生效相同规则。
我们还可以支持更复杂的语法,比如让该组件可以同步其他组件值:
1 2 3 4 5 6 7 8 9
| { "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
属性,将联动定义转化为组件联动关系,因为联动协议本质上就是产生了组件联动。接下来代码片段比较长,因此会尽量使用代码注释来解释:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| const extendMeta = { valueRelates: ({ componentId, selector }) => { const linkage = selector(({ componentInstance }) => componentInstance.linkage)
return linkage.map(relation => { const result = [];
const payload = { type: 'simpleRelation', do: JSON.parse( JSON.stringify(relation.do) .replace( /\$deps\[([0-9]+)\]/g, (match: string, index: string) => `$deps['${relation.deps[Number(index)]}']`, ) .replace(/\$self/g, () => `$deps['${componentId}']`), ), };
relation.target.forEach((targetComponentId) => { if (relation.deps) { relation.deps.forEach((depIdPath: string) => { result.push({ sourceComponentId: depIdPath, targetComponentId, }); }); }
result.push({ sourceComponentId: componentId, targetComponentId, payload, }); });
return result; }).flat() } }
|
上述代码利用 valueRelates
,将联动协议的关联关系提取出来,转化为值联动关系。
接着,我们要实现 props 同步功能,实现这个功能自然是利用 runtimeProps
以及 selector.relates
,将关联到当前组件的组件值,按照联动协议的表达式执行,并更新到对应 key 上,下面是大致实现思路:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
| const extendMeta = { runtimeProps: ({ componentId, selector, getProps, getMergedProps }) => { const relates = selector(({ relates }) => relates);
let relationProps: any = {};
const $deps = relates?.reduce( (result, next) => ({ ...result, [next.componentId]: { value: next.value, }, }), {}, );
relates .filter((relate) => relate.payload?.type === 'simpleRelation') .forEach((relate) => { const expressionArgs = { $deps, get, getProps: relate.componentId === componentId ? getProps : getMergedProps, };
if (isObject(relate.payload?.do?.props)) { Object.keys(relate.payload?.do?.props).forEach((propsKey) => { relationProps = set( propsKey, selector( () => getExpressionResult( get(propsKey, relate.payload?.do?.props), expressionArgs, ), { compare: equals, cache: false, }, ), relationProps, ); }); } });
return relationProps } }
|
其中比较复杂函数就是 getExpressionResult
,它要解析表达式并执行,原理就是利用代码沙盒执行字符串函数,并利用正则替换变量名以匹配上下文中的变量,大致代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| function sandBox(code: string) { const withStr = `with(obj) { ${code} }`; const fun = new Function('obj', withStr);
return function (obj: any) { return fun(obj); }; }
function getSandBoxReturnValue(code: string, args = {}) { try { return sandBox(code)(args); } catch (error) { console.warn(error); } }
function getExpressionResult(code: string, args = {}) { if (code.startsWith('{{') && code.endsWith('}}')) { let codeContent = code.slice(2, code.length - 2);
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 是沙盒执行时替换代码上下文的关键。
总结
componentMeta.valueRelates
与 componentMeta.runtimeProps
可以灵活的定义组件联动关系,与更新组件 props,利用这两个声明式 API,甚至可以实现组件联动协议。总结一下,包含以下几个关键点:
- 将
deps
和 target
利用 valueRelates
转化为组件值关联关系。
- 将联动协议定义的相对关系(比较容易写于容易记)转化为绝对关系(利用 componentId 定位),方便框架处理。
- 利用
with
执行表达式上下文。
- 利用
runtimeProps
+ selector
实现注入组件 props 与响应联动值 relates
变化,从而实现按需联动。
讨论地址是:精读《定义联动协议》· Issue ##471 · dt-fe/weekly
如果你想参与讨论,请 点击这里,每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。
关注 前端精读微信公众号
版权声明:自由转载-非商用-非衍生-保持署名(创意共享 3.0 许可证)