ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

前端精读周刊:可视化搭建中的联动协议设计与实现

前端精读周刊:可视化搭建中的联动协议设计与实现 文档技术博客教程【免费下载链接】weekly前端精读周刊。帮你理解最前沿、实用的技术。项目地址https://gitcode.com/GitHub_Trending/we/weekly点击查看免费下载导读本文是“前端精读周刊”可视化搭建系列的第 7 篇承接 《组件值与联动》 中基于valueRelates的底层联动能力从“能力”走向“协议”业务层如何通过onReadComponentMeta注入一套可拓展的linkage联动协议并利用valueRelates、runtimeProps、selector与表达式沙盒将其落地。读完本文你将掌握一套“组件值同步 props 同步 链式传递”的完整联动协议设计以及{{ }}表达式解析、with沙盒执行等关键实现细节可直接复用到自己的可视化搭建框架中。为什么需要联动协议在 《组件值与联动》 中我们引入了两个底层概念组件值每个组件实例唯一通过getValue(componentId)/setValue(componentId, value)或componentMeta.value声明式访问值联动通过componentMeta.valueRelates声明式定义{ sourceComponentId, targetComponentId, payload }形式的联动关系目标组件通过selector(({ relates }) relates)拿到作用于自己的联动状态。但这里有一个关键认知底层框架提供的只是“能力”还不是“协议”。框架允许建立对组件任意 props 的映射业务层可以在此之上确定一套具体的协议并且这套协议要具备拓展性。协议的价值在于业务方不需要理解valueRelates与runtimeProps的内部机制只需要按照约定的 JSON 结构写配置就能声明式地实现组件联动。本文将以一个具体协议为例说明“先站在使用者角度设计 API再基于已有的组件值与联动能力去实现”的完整路径。设计联动协议不同的业务方会定义不同的联动协议因此这套协议必须通过拓展方式注入而不是写死在框架里。可视化搭建框架支持onReadComponentMeta属性用于拓展所有已注册的组件元信息——联动协议的拓展正是基于组件值与组件联动能力所以这是最合理的注入点import { createDesigner } from designer import { onReadComponentMeta } from linkage-protocol return Designer onReadComponentMeta{onReadComponentMeta} /这种拓展方式的通用性在 《场景实战》 中也有体现无论是事件协议events配置触发源、动作类型、目标组件还是本文的联动协议都可以通过onReadComponentMeta为所有组件元信息统一增加逻辑从而解读组件实例上定义的规则实现任意协议。基础联动linkage与target、do、$self注册后我们就拥有了一套固定形态的联动协议。只要在组件实例上定义linkage属性就可以生效联动{ componentName: input, linkage: [{ target: input1, do: { value: {{ $self.value hello }} } }] }这个例子中各字段的含义target联动目标即组件 ID 为input1的组件。do联动效果本例表示目标组件的组件值同步为当前组件实例的组件值 hello。$self描述自己实例可以从$self.value拿到自己的组件值从$self.props拿到自己的 props。target还支持数组表示同时对多个组件生效相同规则{ target: [input1, input2, input3], do: { value: {{ $self.value }} } }复杂联动deps与$deps我们还可以支持更复杂的语法让该组件同步其他组件的值{ 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区别于do.value同步组件值。协议语义总结一个关键规则是如果定义了target则作用于目标组件未定义target则作用于自身但无论如何表达式里的$self都指向自己实例。总结下来该联动协议允许组件实例实现以下效果设定组件值、组件 props 的联动效果。可以将自己的组件值同步给组件实例也可以将其他组件值同步给自己。基本上可以满足任意组件联动到任意组件的诉求。而且协议天然支持组件间传递比如 A 组件的组件值同步组件 BB 组件的组件值同步组件 C那么 A 组件setValue()后组件 B 和组件 C 的组件值会同时更新——因为relates会随着链式联动不断刷新形成持续性的联动链路。实现联动协议从协议到值联动关系以上联动协议只是一种实现。基于组件值与组件联动我们可以设定任意协议因此“实现联动协议的思维”具备通用性。下面以上面这个协议为例说明如何用可视化搭建框架的基础功能实现协议。第一步解读组件实例的linkage属性将联动定义转化为组件联动关系——因为联动协议本质上就是产生了组件联动。核心是使用componentMeta.valueRelates它会返回结构为[{ sourceComponentId, targetComponentId, payload }]的联动数组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 组件作为 sourcetarget 作为目标组件 // 这是最关键的一步将 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() } }这段代码值得仔细品味它完成了三个关键动作实时读取selector(({ componentInstance }) componentInstance.linkage)读取组件实例的linkage属性由于selector特性联动协议变化后联动状态也会实时更新。表达式归一化将协议中相对友好的$self、$deps[0]写法统一替换为$deps[componentId]这种以具体组件 ID 定位的绝对写法让后续处理流程简单而统一。这一点对应了协议实现的关键思路——把相对关系转化为绝对关系。关系绑定将deps中每个依赖组件与target目标组件建立source - target的关系同时把自己到目标的联动关系也推入结果数组其中携带payloadsimpleRelation类型及处理后的表达式。实现 props 同步runtimePropsselector.relates接着要实现 props 同步功能。自然要利用runtimeProps以及selector.relates——把关联到当前组件的组件值按照联动协议的表达式执行并更新到对应 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 } }实现要点relates是联动的输入通过selector(({ relates }) relates)拿到作用于自己的值关联信息这延续了 《组件值与联动》 中“组件通过selector(({ relates }) )的relates拿到自己当前联动状态”的设计。$deps上下文构建把relates中每个关联组件的componentId与最新value组装成表达式所需的$deps对象。表达式求值getExpressionResult是核心传入组件 props 与表达式返回新的 props 值外层用selector包裹并设置compare: equals与cache: false——由于表达式数量可能不同不启用缓存。set按 key 写入把求值结果按propsKey写入relationProps最终返回给组件实现按需联动。值得注意的细节是getProps与getMergedProps的选择当依赖组件就是自己时使用getProps组件树上序列化的 props否则使用getMergedProps包含runtimeProps处理后的最终 props。这与 《可视化搭建内置 API》 中关于getMergedProps的定义一致——“由于组件 props 可能来自组件树也可能来自runtimePropsgetProps()仅获取组件树上序列化的 props而getMergedProps()获取了包含runtimeProps处理后的最终 props”。表达式执行with沙盒getExpressionResult是其中比较复杂的函数它要解析表达式并执行。原理就是利用代码沙盒执行字符串函数并利用正则替换变量名以匹配上下文中的变量// 代码执行沙盒传入字符串 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\[[](https://link.gitcode.com/i/fa141f200b5af654787c058868065c45)[]\]\.props\.([a-zA-Z0-9.]*)/g, (str: string, componentId: string, propsKeyPath: string) { return get(${propsKeyPath}, getProps(${componentId})); }, ); return getSandBoxReturnValue(return ${codeContent}, args); } return code; }三个函数的职责分工非常清晰sandBox把用户表达式代码包进with(obj) { ... }中通过new Function(obj, withStr)构造执行函数。这里的with语句是沙盒执行时替换代码上下文的关键——它让表达式中的变量名直接绑定到传入的上下文对象上从而让$deps、get、getProps等变量在表达式内部“裸用”。getSandBoxReturnValue真正执行沙盒代码并捕获异常、打印console.warn避免表达式写错导致整个渲染崩溃。getExpressionResult判断字符串是否以{{开头、}}结尾是则取出内部表达式先把$deps[id].props.a.b.c形式替换为get(a.b.c, getProps(id))调用再交给沙盒执行并return结果不是表达式则原样返回字符串。至此联动协议的完整实现链路就闭合了组件实例 linkage 属性 → valueRelates 解读并归一化表达式$self / $deps[i] → $deps[componentId] → 建立 source → target 值联动关系payload: simpleRelation → 目标组件 runtimeProps 通过 selector.relates 拿到 relates → getExpressionResult 沙盒执行表达式set 到对应 props key → 组件 props 实时更新实现按需联动协议的拓展性与通用性这套实现思路最大的特点是协议本身可替换。由于valueRelates与runtimeProps是通用能力业务方完全可以在onReadComponentMeta中注入自定义解析逻辑把任何业务约定的 JSON 结构翻译成底层联动关系。回顾 《组件值与联动》 结尾的论断“许多搭建框架都对联动、查询等场景做了定制化约束使每个框架或多或少存在着私有协议而这个系列想强调的是可以进一步抽象让框架提供业务自由定义协议的能力而不是提供某个固定的协议。”本文的linkage协议正是这一理念的落地示范——框架只提供valueRelates、runtimeProps、selector这些基础原子能力协议由业务层定义与注入。同时在 《场景实战》 中可以看到这类协议的典型应用上卷下钻本质上就是“组件作用于自身的联动”自己联动自己与普通筛选联动的区别仅仅是联动触发源是自己而任意事件协议如events配置打开 Modal同样基于onReadComponentMetaruntimeProps的元信息拓展实现。这说明联动协议的实现思维可以横向迁移到筛选、下钻、事件等多种业务场景。总结componentMeta.valueRelates与componentMeta.runtimeProps可以灵活地定义组件联动关系与更新组件 props。利用这两个声明式 API甚至可以实现组件联动协议。核心关键点如下关系转化将协议中的deps和target利用valueRelates转化为组件值关联关系让协议配置与底层联动引擎对接。相对转绝对将联动协议定义的相对关系$self、$deps[0]比较容易写也容易记转化为绝对关系利用componentId定位即$deps[componentId]方便框架统一处理。with执行上下文利用with语句构建表达式沙盒让{{ }}模板表达式在注入的$deps、get、getProps等上下文中执行。按需联动利用runtimePropsselector实现注入组件 props 与响应联动值relates变化配合compare: equals与禁用缓存实现按需联动避免无效渲染。这套方案的价值在于协议层与框架层正交业务方既可以使用本文的linkage协议也可以定义自己的协议而框架侧只需要保证valueRelates、runtimeProps、selector这三个原子的稳定性。理解了这一点再回头审视 《如何抽象可视化搭建》 中“逻辑层提供标准规范与 API 接口上层注册组件、实现布局一切围绕标准化的逻辑层进行拓展”的分层思想就更容易体会为什么“业务框架既遵循规则又可以千变万化”了。延伸阅读如何抽象可视化搭建可视化搭建的分层设计与逻辑层抽象。组件注册与画布渲染组件树、组件元信息与runtimeProps的由来。画布与组件元信息数据流useDesigner、selector与数据流打通机制。可视化搭建内置 APIgetProps、getMergedProps等内置方法语义。组件值与联动组件值与valueRelates的底层能力本文前提。组件值校验依托selector的另一种声明式能力。场景实战上卷下钻、Tabs、富文本内嵌与任意协议实现。赞分享文档技术博客教程【免费下载链接】weekly前端精读周刊。帮你理解最前沿、实用的技术。项目地址https://gitcode.com/GitHub_Trending/we/weekly点击查看免费下载相关推荐Claude Code 行为调优指南让 AI 少写冗余代码的 4 条守则Claude Code 行为调优指南让 AI 少写冗余代码的 4 条守则 同一个需求「写个函数算折扣」交给 Claude Code没引入 andrej kaAI 技能提示工程前端模块化设计原则打造高内聚低耦合的现代前端架构前端模块化设计原则打造高内聚低耦合的现代前端架构 前端模块化设计是构建可维护、可扩展Web应用的核心原则。随着前端项目规模不断扩大模块化已从可选优化变为必备文档技术博客教程如何永久保存微信聊天记录WeChatMsg完整使用指南让数据真正属于你如何永久保存微信聊天记录WeChatMsg完整使用指南让数据真正属于你 你是否曾为微信聊天记录的丢失而懊恼那些珍贵的对话、重要的约定、温馨的回忆是否因为手上一篇如何快速掌握Go数据库操作sqlx构建高性能数据处理系统的终极指南下一篇CodeQL 1.20 C 分析能力升级查询改进、提取修复与 QL 库增强全景解析创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表