长亭百川云 - 文章详情

可视化流量录制规则探索和实践|得物技术

得物技术

57

2024-07-13

目录

一、前言

二、流量规则项目简介

三、配置可视化(基于ReactFlow引擎)

四、配置智能化

    1. 中心化工具栏

    2. 快速视图入口

    3. 本地缓存策略

五、组织数据驱动视图

    1. ReactFlow引擎如何开始工作

    2. 流量规则工具如何规划数据

    3. 自研数据->视图更新引擎

六、其他技术细节实践

    1. 根据屏幕尺寸实时调整视图窗口

    2. 区分条件和断言的背景区域

    3. 小地图颜色区分条件、断言

七、总结

前言

流量回放平台是将生产环境的流量录制下来在线下环境进行mock或不mock回放,但流量回放不能很好地细化接口规则,无法有效组织和验证业务接口内的预期表现,针对这一痛点,我们急需一个“构建智能规则验证流程,一键直达验证结果”的平台。前期经过深入调研,发现业内往往采用的是复杂的表单交互,从而也带来了以下几个问题:

  • 配置复杂,可读性不高;

  • 且/或(and/or)关系表达不够直观,用户往往需要很大的心智负担来学习,配置成本高;

  • 组织关系不够合理,无法实现单一页面完成所有接口流量规则配置。

旧的表现形式:

面对以上问题,我们需要解决以下问题:

  • 尽可能通过流程图直观地表达且/或(and/or)关系视图;

  • 通过单一页面完成规则项的添加管理工作;

  • 更好的引导,减少用户学习的心智负担。

流量规则项目简介

可视化验证流量规则示例图

通过对业务接口的Respone、Request、子调用的拦截,提取JsonPath,如下图:

JsonPath提取示例

通过预选操作符和期望值组合不同关系、断言语句,从而验证每个关系组合是否达到预期,如下图:

验证流程图

配置可视化(基于ReactFlow引擎)

经过需求讨论和前期调研,前端视图需要把每个规则通过and/or关系符进行链接,最好有一个可用于统一编辑的面板,ReactFlow(用于构建基于节点的编辑器和交互式图表)基本满足我们的需求,需要通过ReactFlow实现如下功能:

  • 自定义关系节点、规则、组、连接线的样式;

  • 前后方插入组、规则;

  • 根据插入位置,实时计算并更新位置;

  • 组内新增规则,并实现组的宽度位置动态变化;

  • 节点的新增、编辑、转组、删除等操作。

可视化操作演示:

配置智能化

中心化工具栏

中心化操作工具栏实例图

快速视图入口

节点操作示例图

本地缓存策略

智能化本地储存策略,保证用户在未点击保存按钮时,离开或者刷新页面可以获得最近的数据,主要缓存策略如下:

  • 数据更新时,本地做一份Json的缓存数据;

  • 如果未操作视图,10s更新一次本地缓存;

  • 视图位置发生改变,本地缓存更新。

组织数据驱动视图

本篇开始,主要介绍基于ReactFlow一技术细节,例如:如何规划数据、绘制视图、更新视图、交互细节、关系语句和元数据的匹配等。通过本节,可以更好地理解ReactFlow工作方式以及如何组织复杂的数据交互!

ReactFlow引擎如何开始工作

`import React from 'react';``import ReactFlow from 'reactflow';` `import 'reactflow/dist/style.css';` `   ``// 初始化一些数据``const initialNodes = [`    `{ id: '1', position: { x: 0, y: 0 }, data: { label: '1' } },``    { id: '2', position: { x: 0, y: 100 }, data: { label: '2' } }`  `];` `// 这是一个连接线``const initialEdges = [{ id: 'e1-2', source: '1', target: '2' }];` `   ``// 渲染页面``export default function App() {  ``     return (     ``         <div style={{ width: '100vw', height: '100vh' }}>       ``             <ReactFlow nodes={initialNodes} edges={initialEdges} />     ``          </div>   ``    );`  `}`

流量规则工具如何规划数据

基础数据:

`{`    `"id": "lr-1", // id唯一标识`    `"position": { // 位置信息`        `"x": 100,`        `"y": 100`    `},`    `"data": { // 详细数据`        `"label": "条件",`        `"id": "lr-1",`        `"group": false, // 是否是组`        `"width": 180,`        `"height": 60,`        `"infos": { // 额外的接口获得的 info`            `"flowConditionItemId": 48,`            `"smallType": 1,`            `"expectValueType": "int",`            `"expectValue": "49",`            `"name": "$.[0].body.condition.projectId equals 49",`            `"projectId": 49,`            `"flowRuleId": 32,`            `"content": 48,`            `"uri": "/console/v1/config/interface-list",`            `"sceneSampleId": 0,`            `"smallName": "equals",`            `"ruleId": 148,`            `"isEdit": false`        `},`        `"params": { // 需要再各个组件内传递的数据`            `"type": "condition", // 类型, 区分条件、断言`            `"isSelect": true, // 是否选中的状态`            `"dependNodeId": "ln-2", // 是否有依赖的组id`            `"isSelectNode": false`        `}`    `},`    `"type": "ruleItemNode", // 渲染类型`    `"parentId": null, // 是否有组的id`    `"width": 180,`    `"height": 65,`    `"positionAbsolute": {`        `"x": 100,`        `"y": 100`    `}``}`

连接线数据:

`{`    `"id": "ln-2=>lr-1", // 连接线id`    `"source": "ln-2", // 从那个块触发开始链接`    `"target": "lr-1", // 结束块 id`    `"animated": true, // 是否有动画`    `"markerEnd": { // 连接线末尾样式`        `"type": "arrowclosed",`        `"width": 20,`        `"height": 20,`        `"color": "#ef4444"`    `},`    `"type": "smoothstep",`    `"style": { // 注入一些额外的样式`        `"strokeWidth": 1,`        `"stroke": "#ef4444"`    `}``}`

自研数据->视图更新引擎

数据->视图流程示意图

`// 更新视图块``const onNodesChange = useCallback(`   `(changes: any) => {`      `setNodes((oldNodes) => applyNodeChanges(changes, oldNodes));`   `},`   `[setNodes]` `);` ` // 更新连接线` `const onConnectHandle = (params: any) => {`    `setEdges((oldEdges) => {`      `return applyEdgeChanges(params, oldEdges);`    `});`  `};`

初始化创建规则数据、组数据、节点数据、连接线:

`// 规则数据``export const initRuleData = (props: intProps) => {`  `const { id, position, data, type, parentId } = props;`  `return {`    `id: id,`    `position: position,`    `data: data,`    `type: type,`    `parentId: parentId,`  `};``};``// 节点数据``export const initNodeData = (props: intProps) => {`  `const { id, show, hidden, children, position, data, type, parentId } = props;`  `return {`    `id: id,`    `hidden: hidden,`    `position: position,`    `parentId: parentId,`    `data: data,`    `type: type,`    `children: children,`  `};``};``// 组数据``export const initGroup = (props: intProps) => {`  `const { id, style, show, hidden, position, data, type } = props;`  `return {`    `id: id,`    `show: show,`    `style: style,`    `hidden: hidden,`    `position: position,`    `data: data,`    `type: type,`  `};``};``// 连接线``export const initEdgesData = (props: intProps) => {`  `const {`    `id,`    `source,`    `target,`    `label,`    `animated,`    `markerEnd,`    `style,`    `sourceHandle,`    `type,`    `lineType,`    `isGroup,`  `} = props;`  `const init_markerEnd = (type: any, label: string) => {`    `return {`      `type: MarkerType.ArrowClosed,`      `width: 20,`      `height: 20,`      `color:`        `type === "condition"`          `? label === "and"`            `? "#f97316"`            `: "#ef4444"`          `: label === "and"`          `? "#7e22ce"`          `: "#a855f7",`    `};`  `};``   `  `const init_style = (type: any, label: string) => {`    `return {`      `strokeWidth: 1,`      `stroke:`        `type === "condition"`          `? label === "and"`            `? "#f97316"`            `: "#ef4444"`          `: label === "and"`          `? "#7e22ce"`          `: "#a855f7",`    `};`  `};`  `return {`    `id: id,`    `source: source,`    `target: target,`    `label: label,`    `animated: animated,`    `markerEnd: isGroup`      `? {}`      `: markerEnd`      `? markerEnd`      `: init_markerEnd(lineType, label),`    `type: type,`    `style: isGroup ? {} : style ? style : init_style(lineType, label),`    `sourceHandle: sourceHandle,`  `};``};`

生成新的节点:

`const createItemNode = (params:paramsType) => {`  `return initNodeData({`    `id: nodeId,`    `hidden: hidden,`    `position: position,`    `parentId: parentId ? parentId : null,`    `data: {`      ``label: `${pattern === "condition" ? "条件" : "断言"}节点`,``      `id: nodeId,`      `infos: {`        `conditionVal: "and", // 默认条件值`      `},`      `group: parentId ? true : false,`      `type: pattern,`      `width: parentId ? constants.NODE_GROUP_WIGHT : constants.NODE_WIGHT,`      `height: parentId ? constants.NODE_GROUP_HEIGHT : constants.NODE_HEIGHT,`      `params: {`        `type: pattern,`        `target: [preItem?.id, nextItem?.id],`        `source: nodeId,`        `onSelectHandle: callback?.onSelectHandle,`        `switchConditionHandle: callback?.switchConditionHandle, // 切换节点`        `isSelect: false,`      `},`    `},`    `type: "nodeItemNode",`  `});``};`

计算节点位置:

`//  计算新的x位置``const createPositionX = (nodes: any[]) => {`  `if (!nodes || !nodes.length) return 100;`  `const lastItem = nodes.at(-1);`  `let marginWidth =`    `lastItem.type === "groupItemNode"`      `? lastItem.width || constants.GROUP_WIGHT`      `: constants.RULE_WIGHT;`  `return lastItem?.position.x + marginWidth + constants.MARGIN_X;``};``   ``// 组内计算x位置``const createInsertGroupPositionX = (nodes: any[]) => {`  `if (!nodes || !nodes.length) return 10;`  `const lastItem = nodes.at(-1);`  `let marginWidth = constants.GROUP_INSERT_RULE_WIGHT;``   `  `return lastItem?.position.x + marginWidth + constants.GROUP_INSERT_MARGIN_X;``};`

推断节点和断言,生成不同的id前缀:

`const getPrefix = (type: string = "groupItemNode") => {`    `if (parentId) {`      `// 组内新增条件`      ``return pattern === "condition" ? `${parentId}-lr/-` : `${parentId}-rr/-`;``    `} else {`      `// 组外新增 组或者条件`      `if (type === "groupItemNode") {`        ``return pattern === "condition" ? `lg-` : `rg-`;``      `} else if (type === "nodeItemNode") {`        ``return pattern === "condition" ? `ln-` : `rn-`;``      `} else {`        ``return pattern === "condition" ? `lr-` : `rr-`;``      `}`    `}`  `};`

创建连接线:

`const createEdges = (nodes: any[]) => {`    `if (!nodes || !nodes.length) return [];`    `let edgesAry: any = [];``   `    `// 获得所有节点`    `const nodeArray =`      `nodes.filter((item: any) => item?.type === "nodeItemNode") || [];``   `    `nodeArray.map((item: any) => {`      `let targetArray = item?.data?.params?.target || [];``   `      `targetArray.map((target: any) => {`        ``let edgeId = `${item?.data?.params?.source}=>${target}`;```   `        `if (edgesAry.findIndex((ed: any) => ed.id === edgeId) <= -1) {`          `const newEdgeItem = initEdgesData({`            ``id: `${item?.data?.params?.source}=>${target}`,``            `source: item?.data?.params?.source,`            `target: target,`            `animated: true,`            `lineType: item?.data?.type,`            `isGroup: item.parentId ? true : false,`            `type: "smoothstep",`          `});``   `          `edgesAry.push(newEdgeItem);`        `}`      `});`    `});``   `    `return edgesAry;`  `};`

视图更新模块:

`const update = (params: paramsType) => {`  `const {newNode, newEdges, parentId, type, newGroupItem, pattern = 'condition', msgType = null}`    `updateHandle(newNode);`    `addEdgesHandle(newEdges);``   `    `//本地储存`    `localInitHandle(newNode, newEdges);`    `let expression = expressionHandle(newNode);`    `    // 整体校验回调`    `validateAllHandle(expression);`    `// 预览结果`    `setExpression(expression);``   `    `// 提示msg`    `if (msgType) {`      `switch (msgType) {`        `case "insertGroup":`          ``message.success(`插入【组】成功`);``          `break;`        `case "insertRule":`          ``message.success(`插入成功`);``          `break;`        `case "deleteGroup":`          ``message.success(`删除【组】成功`);``          `break;`        `case "deleteRule":`          ``message.success(`删除成功`);``          `break;`        `case "changeGroup":`          ``message.success(`转组成功`);``          `break;`        `case "insertGroupAdd":`          ``message.success(`组内新增成功`);``          `break;`        `case "editRule":`          ``message.success(`编辑更新成功!`);``          `break;`        `case "none":`          `break;`      `}`    `}` `   `    `// 添加动态视图窗口移动`    `if (newGroupItem && !parentId) {`      `setViewXY((p: any) => ({`        `...p,`        `y:`          `pattern === "condition"`            `? height <= 1000`              `? -newGroupItem?.position?.y + 150`              `: -newGroupItem?.position?.y + 300`            `: height <= 1000`            `? -newGroupItem?.position?.y + 700`            `: -newGroupItem?.position?.y + 1000,`        `x: -newGroupItem?.position?.x + 1000,`        `zoom: height <= 1000 ? 0.8 : 0.9,`      `}));`    `}`  `};`

匹配生成关系语句:

`import { Divider, Tag, Tooltip } from "antd";``   ``export const Expression = (props: any) => {`  `const { list, theme, matching, check } = props;``   `  `const matchNode = matching?.id || "";`  `const matchLeft = matching?.params?.target[0] || "";`  `const matchRight = matching?.params?.target[1] || "";``   `  `// 项目`  `const clause = (value: any, matchId: any = false, path: any = null) => {`    `if (!value) return "";``   `    `let group_matchId = JSON.parse(JSON.stringify(matchId));``   `    `if (matchId && matchId.includes("/-")) {`      `const idLists = matchId.split("-");`      ``matchId = `${idLists[1]}-${idLists[2]}`;``    `}``   `    `return theme === "small" ? (`      `<span`        ``className={`font-[200] align-bottom text-[10px] text-gray-500 ${``          `matchId === matchLeft ||`          `matchId === matchRight ||`          `group_matchId === matchLeft ||`          `group_matchId === matchRight`            `? "text-orange-600 font-bold underline decoration-1"`            `: ""`        ``}`}``      `>`        `{value}`      `</span>`    `) : !check ? (`      `<Tooltip`        `title={`          `<div className="text-[12px] p-1">`            `{/* <div>`              `<span className="text-gray-400">json-path: </span>`              `<span className="text-[#00c1c2]">{path}</span>`            `</div>`            `<Divider className="my-[5px] border-cool-gray-600" /> */}`            `<div>{value}</div>`          `</div>`        `}`      `>`        `{/*  */}`        `<span`          ``className={`font-[200] cursor-pointer truncate max-w-[50px] inline-block align-bottom text-[10px] rounded-sm py-[1px] px-[3px] m-b-[3px] ${``            `theme && theme === "light"`              `? "bg-gray-300 text-[#000]"`              `: "bg-blue-950 text-gray-400 "`          ``}`}``        `>`          `{value}`        `</span>`      `</Tooltip>`    `) : (`      `<span`        ``className={`font-[200] inline-block align-bottom text-[10px] rounded-sm py-[1px] px-[3px] m-b-[3px] ${``          `theme && theme === "light"`            `? "bg-gray-200 text-[#000]"`            `: "bg-blue-950 text-gray-400 "`        ``}`}``      `>`        `{value}`      `</span>`    `);`  `};``   `  `// 连接符号`  `const connector = (`    `value: any,`    `matchId: any = false,`    `isGroup: any = false`  `) => {`    `if (!value) return "";``   `    `return theme === "small" ? (`      `<span`        ``className={`mx-[4px] inline-block align-bottom text-[10px]  ${``          `matchId === matchNode`            `? "text-orange-600 font-bold underline decoration-1" //font-bold underline decoration-1`            `: "text-gray-400"`        ``}  `}``      `>`        `{value}`      `</span>`    `) : (`      `<span`        ``className={`font-bold mx-[4px] inline-block align-top m-t-[1px] text-[12px] ${``          `isGroup`            `? "text-gray-500"`            `: value === "and"`            `? "text-red-600"`            `: "text-yellow-500"`        ``}`}``      `>`        `{value}`      `</span>`    `);`  `};``   `  `// 组`  `const Group = (props: any) => {`    `const { left, right, child } = props;``   `    `return (`      `<>`        `{left ? clause(left?.name, left?.itemId, left?.content) : ""}`        `{child ? connector(child?.conditionVal, child?.itemId, true) : ""}`        `{right ? clause(right?.name, right?.itemId, right?.content) : ""}`      `</>`    `);`  `};``   `  `//处理组的显示`  `const GroupHandle = (props: any) => {`    `const { children } = props;`    `return (`      `<>`        `{!children || !children.length ? (`          `<Group left={null} right={null} child={null} />`        `) : (`          `<>`            `<span`              ``className={`text-gray-400 m-r-[4px] inline-block ${``                `theme === "small" ? "align-bottom" : "-m-t-[6px]  align-middle"`              ``}`}``            `>`              `(`            `</span>`            `{children?.map((child: any, childIndex: number) => {`              `if (childIndex === 0) {`                `return (`                  `<>`                    `{`                      `<Group`                        `left={child?.left}`                        `right={child?.right}`                        `child={child}`                      `/>`                    `}`                  `</>`                `);`              `} else {`                `return (`                  `<>`                    `{<Group left={null} right={child?.right} child={child} />}`                  `</>`                `);`              `}`            `})}``   `            `<span`              ``className={`text-gray-400 m-l-[4px] inline-block ${``                `theme === "small" ? "align-bottom" : "-m-t-[6px]  align-middle"`              ``}`}``            `>`              `)`            `</span>`          `</>`        `)}`      `</>`    `);`  `};``   `  `return (`    `<>`      `{list?.map((item: any, index: number) => {`        `if (index === 0) {`          `return (`            `<>`              `{!item?.left?.children?.length ? (`                `clause(`                  `item?.left?.name,`                  `item?.left?.itemId,`                  `item?.left?.content`                `)`              `) : (`                `<GroupHandle children={item?.left?.children} />`              `)}``   `              `{(item?.left?.name || item?.left?.children?.length) &&`              `(item?.right?.name || item?.right?.children?.length)`                `? connector(item?.conditionVal, item?.itemId)`                `: ""}``   `              `{!item?.right?.children?.length ? (`                `clause(`                  `item?.right?.name,`                  `item?.right?.itemId,`                  `item?.right?.content`                `)`              `) : (`                `<GroupHandle children={item?.right?.children} />`              `)}`            `</>`          `);`        `} else {`          `return (`            `<>`              `{item?.right?.name || item?.right?.children?.length`                `? connector(item?.conditionVal, item?.itemId)`                `: ""}``   `              `{!item?.right?.children?.length ? (`                `clause(item?.right?.name, item?.right?.itemId)`              `) : (`                `<GroupHandle children={item?.right?.children} />`              `)}`            `</>`          `);`        `}`      `})}`    `</>`  `);``};``   `

最终关系节点示例

其他技术细节实践

根据屏幕尺寸实时调整视图窗口

`setViewXY((p: any) => ({`        `...p,`        `y:`          `pattern === "condition"`            `? height <= 1000`              `? -newGroupItem?.position?.y + 150`              `: -newGroupItem?.position?.y + 300`            `: height <= 1000`            `? -newGroupItem?.position?.y + 700`            `: -newGroupItem?.position?.y + 1000,`        `x: -newGroupItem?.position?.x + 1000,`        `zoom: height <= 1000 ? 0.8 : 0.9,`      `}));``   `

区分条件和断言的背景区域

实现上下背景颜色区分

`style={{background:"linear-gradient(to bottom, white 0%, white 50%, #f1f5f9 50%, #e2e8f0 100%)``}}`

小地图颜色区分条件、断言

`const nodeMinColor = (node: any) => {`    `switch (node.data.params.type) {`      `case "condition":`        `return "#f97316";`      `case "assertion":`        `return "#7e22ce";`      `default:`        `return "#ddd";`    `}`  `};`

总结

至此,这篇文章也接近尾声,当然项目中还有很多其他细节,鉴于篇幅原因不再一一介绍,从中我们学习到了ReactFlow引擎一些必要的技巧,以及我们如何组织一些复杂的数据逻辑,如何在复杂的交互场景下,学会用数据引擎的方式来驱动视图!当然我们也为实现更好的流量接口规则探索出一段新的道路!

往期回顾

1. 在得物的小程序生态实践
2. 客服测试流水线编排设计思路和准入准出应用|得物技术
3. 深入剖析时序Prophet模型:工作原理与源码解析|得物技术
4. 深入理解Babel - 项目管理工具lerna解析|得物技术
5. 星创编辑器在投放业务中的落地|得物技术

文 / Zuck

关注得物技术,每周一、三、五更新技术干货

要是觉得文章对你有帮助的话,欢迎评论转发点赞~

未经得物技术许可严禁转载,否则依法追究法律责任。

扫码添加小助手微信

如有任何疑问,或想要了解更多技术资讯,请添加小助手微信:

相关推荐
关注或联系我们
添加百川云公众号,移动管理云安全产品
咨询热线:
4000-327-707
百川公众号
百川公众号
百川云客服
百川云客服

Copyright ©2024 北京长亭科技有限公司
icon
京ICP备 2024055124号-2