Skip to content

Commit

Permalink
274
Browse files Browse the repository at this point in the history
  • Loading branch information
ascoders committed Mar 6, 2023
1 parent 215729e commit 66737c2
Show file tree
Hide file tree
Showing 2 changed files with 265 additions and 1 deletion.
3 changes: 2 additions & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

前端界的好文精读,每周更新!

最新精读:<a href="./可视化搭建/273.%E7%BB%84%E4%BB%B6%E5%80%BC%E4%B8%8E%E8%81%94%E5%8A%A8.md">273.组件值与联动</a>
最新精读:<a href="./可视化搭建/274.%E7%BB%84%E4%BB%B6%E5%80%BC%E4%B8%8E%E8%81%94%E5%8A%A8.md">274.组件值与联动</a>

素材来源:[周刊参考池](https://github.com/ascoders/weekly/issues/2)

Expand Down Expand Up @@ -307,6 +307,7 @@
- <a href="./可视化搭建/271.%E5%8F%AF%E8%A7%86%E5%8C%96%E6%90%AD%E5%BB%BA%E5%86%85%E7%BD%AE%20API.md">271.可视化搭建内置 API</a>
- <a href="./可视化搭建/272.%E5%AE%B9%E5%99%A8%E7%BB%84%E4%BB%B6%E8%AE%BE%E8%AE%A1.md">272.容器组件设计</a>
- <a href="./可视化搭建/273.%E7%BB%84%E4%BB%B6%E5%80%BC%E4%B8%8E%E8%81%94%E5%8A%A8.md">273.组件值与联动</a>
- <a href="./可视化搭建/274.%E7%BB%84%E4%BB%B6%E5%80%BC%E4%B8%8E%E8%81%94%E5%8A%A8.md">274.组件值与联动</a>

### SQL

Expand Down
263 changes: 263 additions & 0 deletions 可视化搭建/274.定义联动协议.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
虽然底层框架提供了通用的组件值与联动配置,可以建立对组件任意 props 的映射,但这只是一个能力,还不是协议。

业务层是可以确定一个协议的,还要让这个协议具有拓展性。

我们先从使用者角度设计 API,再看看如何根据已有的组件值与联动能力去实现。

## 设计联动协议

首先,不同的业务方会定义不同的联动协议,因此该联动协议需要通过拓展的方式注入:

```js
import { createDesigner } from 'designer'
import { onReadComponentMeta } from 'linkage-protocol'

return <Designer onReadComponentMeta={onReadComponentMeta} />
```

首先可视化搭建框架支持 `onReadComponentMeta` 属性,用于拓展所有已注册的组件元信息,而联动协议的拓展就是基于组件值与组件联动能力的,因此这种是最合理的拓展方式。

之后我们就注册了一个固定的联动协议,它形如下:

```js
{
"componentName": "input",
"linkage": [{
"target": "input1",
"do": {
"value": "{{ $self.value + 'hello' }}"
}
}]
}
```

只要在组件实例上定义 `linkage` 属性,就可以生效联动。比如上面的例子:

- `target`: 联动目标。
- `do`: 联动效果,比如该例子为,组件 ID 为 `input1` 的组件,组件值同步为当前组件实例的组件值 + `'hello'`
- `$self`: 描述自己实例,比如可以从 `$self.value` 拿到自己的组件值,从 `$self.props` 拿到自己的 props。

更近一步,`target` 还可以支持数组,就表示同时对多个组件生效相同规则。

我们还可以支持更复杂的语法,比如让该组件可以同步其他组件值:

```js
{
"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` 都指向自己实例。

总结一下,该联动协议允许组件实例实现以下效果:

1. 设定组件值、组件 props 的联动效果。
2. 可以将自己的组件值同步给组件实例,也可以将其他组件值同步给自己。

基本上,可以满足任意组件联动到任意组件的诉求。而且甚至支持组件间传递,比如 A 组件的组件值同步组件 B, B 组件的组件值同步组件 C,那么 A 组件 `setValue()` 后,组件 B 和 组件 C 的组件值会同时更新。

## 实现联动协议

以上联动协议只是一种实现,我们可以基于组件值与组件联动设定任意协议,因此实现联动协议的思维具备通用性,但为了方便,我们以上面说的这个协议为例子,说明如何用可视化搭建框架的基础功能实现协议。

首先解读组件实例的 `linkage` 属性,将联动定义转化为组件联动关系,因为联动协议本质上就是产生了组件联动。接下来代码片段比较长,因此会尽量使用代码注释来解释:

```js
const extendMeta = {
// 定义 valueRelates 关系,就是我们上一节提到的定义组件联动关系的 key
valueRelates: ({ componentId, selector }) => {
// 利用 selector 读取组件实例 linkage 属性
// 由于 selector 的特性,会实时更新,因此联动协议变化后,联动状态也会实时更新
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`,将关联到当前组件的组件值,按照联动协议的表达式执行,并更新到对应 key 上,下面是大致实现思路:

```js
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`,它要解析表达式并执行,原理就是利用代码沙盒执行字符串函数,并利用正则替换变量名以匹配上下文中的变量,大致代码如下:
```js
// 代码执行沙盒,传入字符串 js 函数,利用 new Function 执行
function sandBox(code: string) {
// with 是关键,利用 with 定制代码执行的上下文
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) {
// eslint-disable-next-line no-console
console.warn(error);
}
}

// 如果对象是字符串则直接返回,是 {{}} 表达式则执行后返回
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](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/with) 是沙盒执行时替换代码上下文的关键。
## 总结
`componentMeta.valueRelates``componentMeta.runtimeProps` 可以灵活的定义组件联动关系,与更新组件 props,利用这两个声明式 API,甚至可以实现组件联动协议。总结一下,包含以下几个关键点:
1. 将 `deps``target` 利用 `valueRelates` 转化为组件值关联关系。
2. 将联动协议定义的相对关系(比较容易写于容易记)转化为绝对关系(利用 componentId 定位),方便框架处理。
3. 利用 `with` 执行表达式上下文。
4. 利用 `runtimeProps` + `selector` 实现注入组件 props 与响应联动值 `relates` 变化,从而实现按需联动。
> 讨论地址是:[精读《定义联动协议》· Issue #471 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/471)
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
> 关注 **前端精读微信公众号**
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))

0 comments on commit 66737c2

Please sign in to comment.