尧图精选

基于Vue生态从零搭建视觉系统前端:数据层、Canvas渲染与性能优化

🕒 发布时间:2026/10/30 13:05:41 📁 来源:尧图网络
简介这是一个基于Vue的视觉系统前端项目适合正在学习Vue工程化开发或需要快速搭建可视化系统界面的开发者参考。项目按标准Vue CLI结构组织包含组件、视图、路由、状态管理等核心模块可帮助读者理解从项目配置到页面渲染的完整链路。资源包共15个文件涵盖4个JavaScript逻辑文件、4个Vue单文件组件、2个JSON配置文件另有HTML入口、图标及说明文档等整体仅131KB结构紧凑便于逐文件分析。目前已有2040人学习下载适合用于练习Vue项目初始化、组件拆分、路由配置与生产构建等操作。通过学习该包可以获得一套可直接运行的视觉系统基础代码以及npm install、serve、build、lint等标准脚本配置为独立开发同类中后台界面提供参考。1. 视觉系统为什么值得自己搭一套从 Vue 生态说起做前端时间久了你会发现所谓“视觉系统”这个词在不同人口袋里装的东西完全不一样。有人拿它指机器视觉的算法链路有人拿它指工厂里的监控大屏。而我这篇文章要拆的是一套用 Vue 生态从零搭建的前端视觉系统——它负责把一堆离散的数据源汇成统一的可视化界面支撑实时监控、趋势分析和异常告警。选 Vue 的原因很直接响应式数据流天生适合“数据进来就刷新视图”这个场景而且组件化边界清晰多人协作时不容易把代码写成一坨。这套系统解决的是三个具体问题数据怎么接、视图怎么画、异常怎么定位。适合谁呢适合那些手里已经有一套后端接口、想快速搭出可视化前端的人也适合维护老项目时被各种硬编码图表逼疯的工程师。下面我从数据层、表现层到底层优化把整个链路拆开每一段都会落到可复现的代码。先说明白一件事——我讲的不是那种连个 demo 都要申请试用的商业产品而是你拉下来就能跑、改改配置就能接自己数据源的完整工程。2. 数据层设计从接口封装到状态管理的三层结构2.1 视觉系统的数据特征高频率、多源、乱序到达视觉系统的数据和普通后台管理页面有个显著差异——它面对的往往是高频写入。我拆过的一个产线监控项目里视觉检测设备每 200 毫秒推一条检测结果同时还有 PLC 信号、环境传感器数据一起涌进来。这种情况下如果每个页面各自去拉接口后端会被打垮前端也会因为响应式依赖混乱而频繁触发无效渲染。所以设计数据层时我一般遵循三层结构接入层负责与后端通信缓存层负责数据清洗和合并状态层负责把整理好的数据交给视图。接入层统一用封装好的 HTTP 和 WebSocket 客户端缓存层做时间窗口内的数据合并、去重和时间对齐状态层暴露给组件的永远是一个个明确的 getter而不是让组件自己去操作原始数组。这种分层带来的直接好处是替换成本低。我后来把一个项目的 REST 接口迁移成 WebSocket 推送时只改了接入层的几十行代码视图层和状态层完全没动。这比那种把axios直接写在组件里的写法省下了太多返工时间。2.2 统一接入层axios 与 WebSocket 双通道封装我先给出接入层的完整封装这段代码是所有可视化的数据基础。视觉系统的数据一般分两类一类是查询型频率低但对完整性要求高另一类是推送型频率高但每条数据量极小。双通道封装就是让两类数据各走各的路互不干扰。// 接入层visual-data-client.js import axios from axios import { EventEmitter } from ./emitter const HTTP_TIMEOUT 10000 // 查询型通道带超时和重试适合拉取历史数据 / 配置 export const queryClient axios.create({ timeout: HTTP_TIMEOUT, // 视觉系统通常部署在内网反向代理需要保留真实路径 baseURL: /visual-api }) queryClient.interceptors.response.use( response response.data, error { // 常见的 502/504 大多是后端视觉服务重启导致重试一次 if (error.code ECONNABORTED || error.response?.status 500) { return retry(error.config, 1) } return Promise.reject(error) } ) function retry(config, count) { if (count 0) return Promise.reject(new Error(visual service unavailable)) return new Promise(resolve { setTimeout(() resolve(queryClient(config)), 300) }) } // 推送型通道WebSocket 统一管理心跳与重连 export class PushChannel extends EventEmitter { constructor(url, options {}) { super() this.url url this.heartbeatInterval options.heartbeatInterval || 15000 this.reconnectDelay options.reconnectDelay || 2000 this._connect() } _connect() { this.ws new WebSocket(this.url) this.ws.onmessage (event) { let payload try { payload JSON.parse(event.data) } catch (e) { // 视觉设备直接吐了字符串按 raw 类型处理 payload { type: raw, value: event.data } } this.emit(payload.type, payload.value) } this.ws.onclose () { setTimeout(() this._connect(), this.reconnectDelay) } this._startHeartbeat() } _startHeartbeat() { this.timer setInterval(() { // 视觉系统服务端有些实现不支持标准 ping pong // 这里发一个自定义的 ping 请求 this.ws.send(JSON.stringify({ type: ping, ts: Date.now() })) }, this.heartbeatInterval) } }这段代码里有两个容易被忽略的点。第一是拦截器里对 5xx 状态码的重试逻辑——视觉系统后端经常因为检测任务排队而偶发超时我认为必须做一次重试而不是直接报错否则页面会频繁展示错误状态。第二是 WebSocket 的raw类型处理很多工业设备推送的是裸字符串不做这层兼容后续的 JSON 解析会把整个推送链路卡死。参数说明HTTP_TIMEOUT默认 10 秒查询型接口如果涉及从数据库读历史图片可能需要放宽到 30 秒。heartbeatInterval建议比服务端超时时间的一半要短不然连接会被服务端先掐断。我在实际项目里遇到过服务端 30 秒无消息断连、客户端 15 秒心跳这种正好的配置不会触发误判。2.3 缓存层时间窗口合并与乱序修正接入层只管收发真正体现视觉系统数据复杂性的在缓存层。以我拆的那个产线项目为例检测设备每 200ms 推一条结果但网络抖动会导致数据到达顺序与产生顺序不一致。如果前端不做乱序修正折线图会出现倒挂的尖刺操作员会误判为设备故障。// 缓存层time-window-cache.js export class TimeWindowCache { constructor(windowSize 100) { this.windowSize windowSize this.buffer [] this.waiting [] this.maxWaitMs 200 } push(item) { // 数据里必须带产生时间而不是接收时间 if (!item.ts) { console.warn([visual-cache] item missing ts, dropped) return } if (this.waiting.length 0 item.ts this.waiting[0].ts) { // 新来的数据比等待中的数据更旧说明发生了乱序 this._reorder(item) } else if (this.waiting.length 0) { this._flushWaiting() this._insertSorted(item) } else { this.waiting.push(item) this._scheduleFlush() } } _insertSorted(item) { // 二分查找插入位置保证 buffer 内按 ts 升序 let low 0 let high this.buffer.length while (low high) { const mid (low high) 1 if (this.buffer[mid].ts item.ts) low mid 1 else high mid } this.buffer.splice(low, 0, item) if (this.buffer.length this.windowSize) { this.buffer.shift() } } _scheduleFlush() { setTimeout(() this._flushWaiting(), this.maxWaitMs) } _flushWaiting() { if (this.waiting.length 0) return for (const item of this.waiting) { this._insertSorted(item) } this.waiting [] } _reorder(item) { this.waiting.push(item) this._flushWaiting() } getSorted() { if (this.waiting.length 0) this._flushWaiting() return this.buffer } }核心思想是把到达的数据先放进一个短暂的等待区超过 200ms 窗口或者来了更新的数据再合并进主缓冲。这样做的原理是视觉系统的数据乱序通常发生在几百毫秒内超过这个时间还乱序的概率极低不值得为极端情况牺牲实时性。这里有个参数权衡maxWaitMs设得太短乱序修正效果差设得太长折线图实时性受损。我测试下来的经验值是 200ms——在人眼感知连续动画的阈值以内又能覆盖大部分网络抖动。windowSize控制内存占用如果在跑 8 路相机同时检测这个值要适当调小我一般按 100 条/通道×通道数来算。2.4 状态层让 Vue 组件只认语义不认事件状态层是连接缓存层和视图的桥梁。我用的是 Pinia原因有两个一是 Vue 3 的 Composition API 和 Pinia 天然契合二是它对 TypeScript 的支持比 Vuex 更加完整视觉系统这种需要定义大量数据类型的场景类型提示能少写很多排查时间。状态模块的划分应该跟着业务域走而不是跟着组件走——我之前见过有人把defectList放在大屏组件内部定义结果另一个弹窗组件也要用同样的数据只能通过事件层层传递极其痛苦。举个状态层接口设计的例子// 状态层visual-store.ts import { defineStore } from pinia import { TimeWindowCache } from ./time-window-cache export const useVisualStore defineStore(visual, { state: () ({ cache: new TimeWindowCache(100), // 相机状态映射camId - Online/Offline/Warning camStatus: {}, // 当前选中的检测区域用于联动图表的过滤 activeRegion: ALL }), getters: { // 组件永远只认语义化的 getter不直接碰 buffer defectTrend: (state) { const sorted state.cache.getSorted() // 按时间聚合缺陷数 return sorted.reduce((acc, item) { const key formatTime(item.ts) acc[key] (acc[key] || 0) (item.defect ? 1 : 0) return acc }, {}) } }, actions: { handleIncoming(item) { this.cache.push(item) // 相机状态是低频数据直接覆盖 if (item.type status) { this.camStatus[item.camId] item.status } } } })组件侧只需要useVisualStore()然后声明依赖某个 getter完全不需要关心数据是从 HTTP 来的还是 WebSocket 来的也不需要关心缓存层怎么排序。这个解耦在视觉系统项目里特别重要——因为视觉的需求迭代很快今天只看缺陷数明天要看缺陷面积占比后天要按缺陷类型分组。只要状态层 getter 改了视图自动更新不需要动任何组件代码。我对这个三层结构最满意的不是性能而是排错效率。出问题时先用 Postman 直连接口确认接入层再写临时脚本消费缓存确认缓存层最后才去看组件。三层各查一遍绝大多数问题半小时内能定位。如果你现在的项目里数据流是乱的我强烈建议先按这个结构重组一遍再动可视化。3. 可视化实战从 Canvas 图层到事件穿透的完整方案3.1 为什么视觉系统的图表不用 ECharts 一把梭ECharts 很强大但视觉系统里有一部分场景我得说它不够用。你想象一下一个实时更新的缺陷分布热力图数据每秒刷新同时还能拖动、缩放还要叠加不同设备的轮廓图。ECharts 的配置项在这种复合场景下会膨胀到难以维护而且它的动画机制在数据高频变化时反而占用 CPU。我的选型原则是标准图表折线、柱状、饼用 ECharts因为它开箱即用且 API 稳定定制化渲染图层叠加、不规则区域、实时拖拽用 Canvas 手绘。混搭的前提是一套统一的事件绑定机制不能让 Canvas 和 ECharts 各玩各的交互。Canvas 手绘方案的核心是一个图层管理器——每个图层负责一块独立区域比如相机画面层、缺陷标注层、坐标辅助层。图层之间互不干扰绘制顺序由管理器统一控制。这种设计最大的好处是可以单独禁用某个图层来排查问题比如怀疑标注层遮挡了画面在调试工具里关掉它就能立刻确认。3.2 图层管理器实现帧循环与脏标记图层管理器的两个核心机制是帧循环和脏标记。帧循环用requestAnimationFrame驱动保证绘制跟屏幕刷新率同步脏标记则避免无意义的重绘——只有数据变化或交互发生时对应的图层才标记为dirty。// 图层管理器visual-layer-manager.js export class LayerManager { constructor(canvas, options {}) { this.ctx canvas.getContext(2d) this.layers new Map() this.dirtySet new Set() this.rafId null // 视觉系统的帧率一般不需要拉满15 FPS 够用还省电 this.fps options.fps || 15 this.frameInterval 1000 / this.fps this._lastFrame 0 } registerLayer(name, layer) { // layer 需要实现 draw(ctx) 和 hitTest(x, y) 两个方法 this.layers.set(name, layer) this.dirtySet.add(name) return this } markDirty(name) { this.dirtySet.add(name) } start() { const loop (ts) { // 按设定帧率节流避免高频数据导致 Canvas 重绘风暴 if (ts - this._lastFrame this.frameInterval) { this._render(ts) this._lastFrame ts } this.rafId requestAnimationFrame(loop) } this.rafId requestAnimationFrame(loop) } stop() { cancelAnimationFrame(this.rafId) } _render(ts) { for (const name of this.layers.keys()) { if (!this.dirtySet.has(name)) continue const layer this.layers.get(name) layer.draw(this.ctx, ts) this.dirtySet.delete(name) } } hitTest(x, y) { // 按注册顺序倒序检测保证最上层优先响应 const names Array.from(this.layers.keys()) for (let i names.length - 1; i 0; i--) { const layer this.layers.get(names[i]) const result layer.hitTest(x, y) if (result) return { name: names[i], payload: result } } return null } }fps参数这里我特意设置为 15 而不是 60是因为视觉系统的画面监测不需要丝滑动画操作员看的是数据趋势和异常点15 帧完全够用且能显著降低 CPU 占用。切换到 60 帧后 CPU 占用会从 8% 涨到 20% 多在旧工控机上容易跟后端服务抢资源。hitTest是 Canvas 交互的关键。Canvas 和 DOM 不一样没有天然的点击事件必须自己做坐标命中测试。我的做法是让每个图层实现自己的hitTest(x, y)方法比如缺陷标注层就是遍历所有缺陷点计算距离距离小于半径就视为命中。管理器从最上层往下找命中即停这个顺序保证了交互跟视觉呈现一致。3.3 事件穿透让 Canvas 与 ECharts 共享一套交互语言在一个视觉系统大屏上往往左边是 Canvas 自绘的相机画面和缺陷标注右边是 ECharts 的趋势图。操作员在左边画一个框选区域右边的趋势图要立刻只展示该区域的缺陷数据。这种跨框架交互的关键是把事件统一成一套系统内部的消息而不是各自监听浏览器事件后各做各的。// 交互协调器visual-interaction.ts import { EventEmitter } from ./emitter export class VisualInteraction extends EventEmitter { constructor(layerManager, chartInstances) { super() this.layerManager layerManager this.charts chartInstances // { trendChart: echartsInstance, heatmap: echartsInstance } // 统一事件协议 // - region-change: 框选区域变化携带 { regionId, bounds } // - point-hover: 指针悬停到缺陷点 // - zoom-change: 图表缩放需要同步 Canvas 视野 this.layerManager.on(region-change, ({ bounds }) { // 通知右侧图表更新数据范围 this.emit(region-change, bounds) this.charts.trendChart.dispatchAction({ type: dataZoom, start: bounds.percentStart, end: bounds.percentEnd }) }) // 反向联动右侧图表缩放时左侧 Canvas 只调整辅助坐标线 this.charts.trendChart.on(datazoom, (params) { this.emit(zoom-change, params) this.layerManager.getLayer(aux-grid).setZoom(params) this.layerManager.markDirty(aux-grid) }) } hoverOnCanvas(x, y) { const hit this.layerManager.hitTest(x, y) if (hit?.payload?.defectId) { // 让右侧图表高亮对应对缺陷点 this.charts.trendChart.dispatchAction({ type: highlight, seriesIndex: 0, dataIndex: hit.payload.dataIndex }) } } }这套事件穿透设计的核心思路是框架之间的交互只通过VisualInteraction转发不直接互相调用。这样做的好处是以后替换掉 ECharts 换成其他渲染库时只需要改协调器内部那几行代码事件协议不变Canvas 图层完全不用动。我在实际项目里就干过这事——客户从 ECharts 3 升到 ECharts 5Canvas 部分零改动。Canvas 自定义渲染最耗时间的是坐标变换。因为 Canvas 的坐标系原点是左上角而视觉系统的业务坐标往往是像素坐标或毫米坐标。我在这里的处理方式是加一个 transform 层统一在图层绘制时做坐标映射避免每个图层都自己处理一遍缩放和平移。具体做法是给 LayerManager 增加一个相机对象包含worldToScreen和screenToWorld两个方法所有图层的 draw 和 hitTest 都走这个相机换算。4. 排查与避坑视觉系统前端五类高频故障4.1 图片资源在动态路由里全部失效之前做一个基于 Vue Router 的视觉系统路由配置用了动态传参每个相机详情页的 URL 带相机 ID。结果发现所有从接口返回的图片路径都不能显示浏览器控制台全是 404。现象是图片请求的 URL 变成了/visual-api/visual-api/cameras/1/preview这种重复前缀。原因是接口返回的图片路径是相对路径visual-api/cameras/1/preview我在 axios 封装里已经设了baseURL: /visual-api而页面上的img src又直接用了接口返回的相对路径Vue Router 默认用相对路径拼接当前路由于是前缀重复。解决方法是做一个统一图片路径过滤函数export 前把所有相对路径转成绝对路径。具体代码是在响应拦截器里加一段normalizeMediaUrl(item)遍历对象里所有url或imageUrl字段如果以visual-api开头就自动在前面拼一个/。从那以后我每次封装接口都会顺手检查返回数据里的路径字段再也不敢信任后端给的路径可以直接用。4.2 Canvas 在设备 DPR 大于 1 时模糊视觉系统的大屏经常是 4K 屏用 Canvas 绘制缺陷标注后发现线条边缘发虚像是蒙了一层雾。现象是同一套 Canvas 代码在 1080p 屏上清晰在 4K 屏上模糊尤其是绘制细线条和文字标注时。原因是 Canvas 的物理像素和 CSS 像素没有做适配。我设的 canvas 宽高是 CSS 像素但设备像素比是 2 或 3Canvas 按 CSS 像素绘制后浏览器拉伸到物理像素自然就糊了。解决方法是初始化时校正 DPRfunction setupCanvas(canvas) { const dpr window.devicePixelRatio || 1 const rect canvas.getBoundingClientRect() canvas.width rect.width * dpr canvas.height rect.height * dpr const ctx canvas.getContext(2d) ctx.scale(dpr, dpr) // 所有后续绘制逻辑按 CSS 像素坐标走 return ctx }注意这个ctx.scale(dpr, dpr)只调用一次如果放在渲染循环里每次都调用会导致坐标越滚越大。正确的做法是在尺寸变化时比如窗口 resize重新 setup。4.3 高频数据下 Vue 响应式拖慢渲染视觉系统每秒推 50 条数据我直接把数据 push 进 reactive 数组然后界面上遍历显示。结果画面卡顿严重CPU 占用飙升。现象是页面有交互反应但数组越长越卡定位到是 Vue 的响应式代理在数组长度变大时性能劣化。原因是 Vue 3 的reactive会对数组内部元素做深度代理每秒 push 50 条数据意味着每秒创建 50 个 Proxy 对象而且数组越长后续任何响应式依赖重算的成本越高。视觉系统的数据往往只读不需要深度响应式。解决方法是把高频数据放进shallowRef或markRaw包裹的对象里Vue 只追踪 data 的引用变化不递归追踪内部属性。具体代码是把TimeWindowCache的实例用shallowRef存起来const cacheRef shallowRef(new TimeWindowCache(100)) // 更新时整体替换引用或者直接用 cacheRef.value.push() // 配合 getter 里的 .value 访问Vue 只会在引用变化时重算改完之后卡顿问题直接消失。从那以后我定了一个规矩视觉系统的原始数据流绝不直接进 reactive统一走 shallowRef 或普通 class 实例。4.4 组件复用时状态残留视觉系统里有个检测参数配置弹窗一个组件服务多个相机。切换相机后打开弹窗发现上一个相机的参数还留在表单里。现象是弹窗显示的数据是上一个相机的保存时把错误参数写回了错误的相机。原因是弹窗组件在v-if关闭时没有销毁v-show 或者 keep-alive或者组件实例被 Vue 复用created 钩子不再触发表单初始值没有重新加载。解决方法是监听组件的可见状态在打开时重新拉取数据初始化watch(() props.visible, (visible) { if (visible) { // 重新拉取当前相机的配置 loadConfig(props.cameraId) } })这是 Vue 组件复用的经典坑不只是视觉系统会碰到。我的排查经验是先把弹窗的v-if改成每次打开强制销毁再重建能快速确认是不是复用导致的确认后用上面的 watch 方案根治。4.5 事件协调器导致的内存泄漏视觉系统长时间挂着运行几天不刷新操作员反馈页面越来越卡最后浏览器直接崩溃。现象是性能面板里事件监听器的数量随运行时间线性增长内存占用也稳步攀升。原因是VisualInteraction里用了 EventEmitter而图表组件on了事件后组件销毁时没有对应的off。ECharts 实例在组件beforeUnmount时销毁了但它上面的监听器还留在 EventEmitter 里回调里引用了已销毁的组件GC 无法回收。解决方法是给 EventEmitter 增加一个dispose方法在协调器销毁时把所有注册的回调清掉class VisualInteraction extends EventEmitter { dispose() { // 先移除自己的监听再通知下层注销未移除的图表事件 this.removeAllListeners() for (const chart of Object.values(this.charts)) { chart.off() } } }然后组件里在onBeforeUnmount中调用interaction.dispose()。为了排查这件事我花了整整半天——事件不像 datalayer 的脏数据那么直观它没有报错只是慢慢耗尽内存。从那以后我给团队定了个硬规范任何 EventEmitter 的注册和注销必须出现在同一个生命周期函数里写代码时就想着对应的off写在哪。5. 把视觉系统的性能再往前推一步帧率监控与渲染瘦身5.1 用帧率监控脚本量化每次改动的收益视觉系统的优化不能凭感觉要有数字。我写了一段极简的帧率监控脚本挂在 LayerManager 的启动过程中实时统计渲染帧间隔// 帧率监控简易 FPS 统计 export function attachFpsMonitor(layerManager, onReport) { let frameCount 0 let startTime performance.now() // 利用 LayerManager 的钩子每绘制 100 帧统计一次 const originalRender layerManager._render layerManager._render function(...args) { originalRender.apply(this, args) frameCount if (frameCount % 100 0) { const elapsed (performance.now() - startTime) / 1000 const fps frameCount / elapsed onReport({ fps, avgRenderMs: elapsed / frameCount * 1000 }) frameCount 0 startTime performance.now() } } }我一般会在三种状态下分别记录无数据渲染空帧切换、低负载渲染 5 帧/秒数据、满载渲染50 帧/秒数据。对比这三个数能快速定位性能瓶颈是绘制复杂度、脏标记范围还是数据转换逻辑。有一次优化图层重绘区域后满载 fps 从 11 涨到 28而低负载基本没变这说明问题确实出在重绘范围过大而不是数据层。参数说明onReport每 100 帧回调一次在 15 FPS 设定下大约每 6.6 秒报告一次足够观察趋势而不会刷屏。如果你想看更精细的优化曲线把100改成30时间分辨率更高但报告频率也会变高。5.2 渲染瘦身四象限合并重绘视觉系统大屏上最常见的浪费是重绘整个 Canvas即使只新增了一个缺陷点。我基于前面图层管理器的脏标记机制做了更精细的四象限合并。把 Canvas 的逻辑区域分成四块每块标记独立脏状态一次数据更新只重绘受影响的象限。具体实现是在 LayerManager 内部维护一个dirtyQuads数组绘制时只调用对应象限的clipRect后再执行 draw。这种优化在相机画面和缺陷标注叠加的场景下效果极好因为有标注的象限往往集中在画面中心四周空白区域从不重绘。另外一个大头是离屏缓存。坐标辅助线网格、刻度是静态的但每次绘制都要重算一遍坐标并画线浪费明显。我把它画到一个独立的离屏 Canvas 上绘制完成后直接drawImage贴到主画布这个图层可以完全不参与帧循环。对比改造前后的性能数据网格图层从每帧 2.8ms 降到了 0.2ms 以内。5.3 一个验证视觉系统质量的小技巧用历史数据回放最后分享一个我比较受用的验证方法——历史数据回放。视觉系统的实时数据转瞬即逝出了问题不好重现。我在开发阶段会录制一份真实的数据流按时间戳存成 JSON 数组然后用一个脚本按原始时间间隔回放给整个前端链路。回放的好处很多第一新来的前端开发者可以模拟真实工况而不需要去产线旁边等数据第二你可以把timeScale参数调大比如 10 倍速快速检验缓存层在高压力下会不会丢数据或乱序第三当你修改了渲染代码用同一份录制数据跑回归能发现肉眼难以察觉的渲染差异。回放脚本的核心是一个带setTimeout的异步遍历async function replayData(recorded, timeScale 1) { let prevTs 0 for (const item of recorded) { if (prevTs) { const waitMs (item.ts - prevTs) / timeScale if (waitMs 0) await sleep(waitMs) } routeToVisualSystem(item) prevTs item.ts } }我养成了一个习惯每次拿到新的视觉数据源第一件事是先录制五分钟的真实数据存为 JSON之后所有开发调测都基于这份录制数据不直接接现场。好处是可复现、可回归、可单元测试。有一次后端改了数据结构我的缓存层因为字段名不匹配直接报错用回放脚本一分钟就定位了不用等现场复现。这个习惯我一直保持到了今天——希望这篇文章能帮你在搭视觉系统时少走点弯路也希望你能像我一样把每一份资源和数据都当成工程资产来对待。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联 返回资讯列表 →