目录
一、前言
二、流量规则项目简介
三、配置可视化(基于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
关注得物技术,每周一、三、五更新技术干货
要是觉得文章对你有帮助的话,欢迎评论转发点赞~
未经得物技术许可严禁转载,否则依法追究法律责任。
“
扫码添加小助手微信
如有任何疑问,或想要了解更多技术资讯,请添加小助手微信: