最近有个朋友问我大屏开发是不是就是“写个 Vue 页面放大字号再把图表铺满屏幕”。我愣了一下因为一年前的我也会这么回答。上个月我们刚把一套车间实时产线大屏全部重构完用的是一种以前没尝试过的方式不再为每个大屏单独写页面而是先写一张描述大屏的 JSON 配置再由一个轻量渲染内核把这个配置实时变成屏幕上的组件。这套方式刚开始被组里同事吐槽多此一举但等三块大屏跑完大家都觉得回不去了。我把具体方案、关键代码和踩过的坑完整记录在这里给正在纠结要不要换一种大屏开发方式的朋友做个参考。1. 先说我为什么放弃“每个大屏写一个页面”的老路子很多团队做大屏开发第一反应都是新建一个项目按设计稿画 div引 ECharts写 axios最后铺满 1920x1080 的容器。这套流程本身没毛病做单块展示屏完全够用。但我遇到的挑战是要在同一个时间段维护 6 块不同主题的大屏三块在办公楼两块在车间一块在园区门口数据源不一样、刷新频率不一样、视觉风格只有细微差别。如果每块屏都是一个独立页面项目工作量直接翻六倍。1.1 一个典型大屏需求背后的隐形工作量我随便拿一块“生产看板”举例。你以为的需求只是“左边放产值趋势图右边放设备状态中间滚动报警记录”。把一个页面搭出来之后还有这些事等着你下午 3 点提测时发现报警表格要 5 秒轮询一次第二天数据部门说要加一个“工单达成率”的卡片第三天大屏挂在墙上现场反馈说分辨率和测试电脑不一样整体把画面拉伸了。页面代码被改了一遍又一遍最后变成了一个只有原作者敢动的“定制化烂摊子”。大屏开发真正的成本从来不在首屏搭建而在后续反复的维护和微调。传统做法里每个改动都要进组件里定位代码、改逻辑、重新发版。如果同时有六块屏就是一个改动要重复做六次。1.2 老思路的三个死穴重复、联调、改需求重复很容易理解nav 栏、边框、标题、时间组件、状态圆点每块屏都要写一遍。联调是更头疼的环节很多大屏的数据接口并不在同一个后端服务里有的在 BI 系统有的在设备采集网关有的干脆只能提供一个 CSV 文件让前端自己轮询。每块屏的前端代码都要单独处理这些来源数据格式稍有不同解析逻辑就得跟着变。改需求则是压垮我自己的最后一根稻草。某次领导看完一块驾驶舱大屏提了一个听起来很小的意见“把左下角这个环形图的颜色跟右上角那个柱状图统一一下。”我打开代码找了半天才发现两个图用了不同组件实例配置还散落在不同初始化函数里。改起来不难但我在想这种改法根本没有沉淀。下一次换个人来改还是同样找不到重点。1.3 我从低代码平台里捡回来的一个关键念头当时我们评估过市场上几款大屏低代码工具能拖拽、能配置、能发布效果确实不错。但问题在于业务侧需要和内部系统深度集成很多组件要共用我们的登录鉴权、自定义接口协议甚至要嵌入到已有的运营后台里。把整块大屏完全托管给外部平台后续扩展会很被动。不过低代码产品给了我一个启发大屏本质上可以看作“一颗组件树”。如果我用一种统一的数据格式描述这颗组件树的位置、类型、数据来源、交互行为然后写一个通用渲染器去解析它那么维护成本就不应该随屏幕数量线性增长。这个念头最后演变成了我们沿用至今的方案我管它叫 Schema 驱动的大屏开发方式。2. 新方式的核心用一张 Schema 把大屏描述出来从“写页面”变成“写配置”不是简单的把 JSX 换成 JSON而是整个开发心智的变化。你需要先定义一套 schema 规范再把所有页面元素抽象成节点。有了这个抽象之后大屏就不再是“一个项目”而是“一份清单”。2.1 一个最小可运行的 schema 长什么样先给你看一个我们实际生产环境里精简过的配置能让没有接触过这个方案的人最快理解结构{ name: 车间产线看板, width: 1920, height: 1080, backgroundColor: #0a1628, components: [ { id: main-title, type: Text, layout: { x: 0, y: 0, w: 1920, h: 80 }, props: { content: 华东一车间实时产线监控, fontSize: 42, color: #ffffff } }, { id: output-trend, type: LineChart, layout: { x: 60, y: 140, w: 880, h: 420 }, props: { title: 近 24 小时产值走势, showLegend: true }, dataSource: { mode: polling, url: /api/v1/workshop/output-trend, interval: 10000, transform: mapOutputTrend } }, { id: alert-scroll, type: ScrollList, layout: { x: 1420, y: 600, w: 440, h: 400 }, props: { rowHeight: 40, speed: 30 }, dataSource: { mode: polling, url: /api/v1/workshop/alerts, interval: 5000 } } ] }这段配置描述了三样东西一块大屏的全局属性一个文本标题两个图表组件。每个组件都有唯一 id、类型、布局位置和 props。布局用绝对坐标表示这里我直接按 1920x1080 设计稿写方便视觉还原。数据源是独立字段它决定了组件的数据从哪来、多久刷新一次、结果要做什么转换。2.2 渲染器如何把 schema 变成真实组件光有配置还不够你还需要一个能把它跑起来的解释器。这个解释器在我们的项目里是一个基于 Vue 3 的渲染内核核心逻辑就是一个递归组件。每次拿到 schema 后渲染器会遍历 components 数组根据 type 字段去一个注册表里找到对应组件实现再用 layout 和 props 完成挂载。核心伪代码思路如下// 组件注册表 const registry { Text: defineComponent({ ... }), LineChart: defineComponent({ ... }), ScrollList: defineComponent({ ... }), BarChart: defineComponent({ ... }), ... }; // 渲染入口 function renderComponent(node: SchemaNode) { const comp registry[node.type]; if (!comp) { console.warn([render] 未注册的组件类型: ${node.type}); return null; } const style { position: absolute, left: node.layout.x px, top: node.layout.y px, width: node.layout.w px, height: node.layout.h px, }; return h(comp, { key: node.id, style, props: node.props, dataSource: node.dataSource, }); }在真正的 Vue 模板里我只需要一个component :isresolvedComponent v-bindnodeProps /即可。组件本身是纯展示组件它不关心自己被放在哪块屏幕也不关心数据是来自轮询还是来自手动 mock。它只接收 props 和 dataSource内部负责渲染。这套设计让我可以把“大屏页面”彻底变成一个数据产物。新增大屏时我不会再去src/views下面新建页面而是往配置仓库里提交一份 JSON。组件还是那些组件但组合方式可以千变万化。2.3 为什么选 JSON 而不是直接写 Vue 模板有人会问用 Vue 模板直接写组件树也能做到复用为什么非要绕一圈变成 JSON我的回答是JSON 是一种数据数据可以被校验、被存储、被后端返回甚至可以被可视化编辑器生成。模板是代码代码需要构建、需要发布、需要人来 review。JSON 让大屏的表达和运行彻底分离这是它最大的价值。我们项目里经常出现需求方上午说标题改两个字下午说图表位置互换这种改动如果改 JSON只需要找到对应字段修改然后提交整个流程不超过十分钟。最关键的是这个 JSON 可以存在数据库里业务运营人员通过后台界面对大屏做微调时修改完保存前端刷新就生效。这在老方案里是不可想象的。2.4 schema 版本化让大屏配置成为可回滚资产既然 JSON 是一份数据自然可以做版本管理。我们团队为每个大屏配置建了一份 git 仓库每次改动都会提交一个版本。线上出问题时能快速对比某个组件的 props 在哪个版本被改过也能一键回滚到上一版。这块在老式的“代码即页面”流程里很难做到因为组件代码的改动影响范围往往是全局的而 schema 的改动可以精确到单个组件。多说一句schema 一定要做校验。我们自己写了一个基于 JSON Schema 的校验器在 CI 阶段检查每个大屏配置是否满足基本规范组件 type 是否存在于注册表layout 是否超出画布边界dataSource.url 是否为空。这些检查虽然简单但能挡住绝大多数低级错误。3. 适配和动效大屏最容易翻车的两件事我这样处理大屏和普通后台页面的最大区别在于终端环境。后台页面大多数人在 14 寸笔记本上看大屏可能是 55 寸拼接屏、75 寸一体机、甚至三块屏拼接。分辨率、缩放比例、浏览器内核都不可控。动效则是另一个坑领导常要求“数据要滚动起来”“数字要闪烁一下”如果这些逻辑写死在组件内部复用性会大打折扣。3.1 全屏适配scale 缩放方案与 rem 方案的取舍刚开始我们试着用 rem 做适配把设计稿宽度 1920 分成 100 份所有尺寸都用 rem 表示。结果发现图表内部文字会被放大或缩小得很难看因为 ECharts 默认在容器宽度变化时只重算 canvas 尺寸字体大小不会跟着等比缩放。后来我们换成了整体缩放方案。渲染器在最外层容器里根据实际屏幕尺寸与设计稿宽高的比例计算出一个 scalefunction calcScale(designWidth: number, designHeight: number) { const clientWidth window.innerWidth; const clientHeight window.innerHeight; const scaleX clientWidth / designWidth; const scaleY clientHeight / designHeight; return Math.min(scaleX, scaleY); }把这个 scale 应用到最外层容器的transform: scale()上内部组件全部按 1920x1080 设计稿的绝对坐标摆放视觉上始终是等比缩放不会出现文字和图表比例失调的问题。这个方案唯一的缺点是两侧可能会留下白边但用背景色填充后就无伤大雅。3.2 组件级响应式用 ResizeObserver 感知容器尺寸整体缩放只解决了画面比例问题但组件内部还是要感知真实渲染大小。因为 ECharts、表格这类内容组件在初始化时需要拿到容器的实际宽度高度才能画出一个不模糊、不溢出的图。我们在封装图表组件时统一用 ResizeObserver 监听外层容器尺寸变化并在回调里调用chart.resize()。一个容易被忽略的坑是当外层容器使用transform: scale()缩小之后元素自身的 offsetWidth 仍然是设计稿尺寸但 canvas 绘制出来的位图会因为缩放产生模糊感。处理办法是绘图时把 canvas 的物理像素提高一倍或者干脆把容器宽高理解为视觉尺寸由缩放层去适配。实际项目里我们选择在 ECharts 初始化时传入一个pixelRatio参数保证在 4K 大屏上线条也足够锐利。3.3 轮播、滚动、闪烁把动效写进 schema 而不是写死在组件里最早的组件里“列表轮播”和“数字闪烁”都是写死的逻辑导致同样一个 ScrollList 组件在某块屏上要轮播在另一块屏上却要静止只能靠开发者不断复制代码去微调。换成 schema 驱动后我们在每个节点里增加了一个animation字段把动效当作一种可配置行为{ id: alert-scroll, type: ScrollList, layout: { x: 1420, y: 600, w: 440, h: 400 }, props: { rowHeight: 40 }, animation: { type: loop-scroll, speed: 30, autoStart: true } }渲染器会在组件挂载后统一处理 animation 配置比如对 loop-scroll就启动 setInterval 定时把内部列表滚动一位对 flash就周期性给数字加高亮类。组件的核心逻辑保持纯粹动画只是外挂行为。这样一来产品经理说“这个图表不滚了”“那个列表加个暂停”都只需要改配置不用动代码。3.4 我在生产环境遇到的一个 1px 模糊问题不得不提一个我花掉半天时间的怪问题某块大屏显示在 4K 分辨率下整体画面总有一种灰蒙蒙的不锐利感。排查到最后发现是外层容器设置了transform: scale(1.125)而内部某些图表容器宽度换算出来是奇数比如 879.35px。浏览器在缩放时会做一次亚像素插值导致边缘线发虚。解决方法很简单在计算布局时对所有容器的 width、height 做向下取整。另外尽量避免在内部组件上再次使用transform尤其不要和外部缩放叠加很容易出现抖动或模糊。这是大屏开发里最容易忽略的视觉细节再漂亮的配色也经不住毛边的侵蚀。4. 数据接入让大屏从“静态展示”变成“实时驾驶舱”大屏如果只是一个静态页面价值会大打折扣。真正让用户愿意抬头看屏幕的原因是上面有实时变化的数据。如何管理这些数据流是我这套方案里仅次于 schema 的核心内容。4.1 数据源描述轮询、推送、本地 Mock 三种模式我们在 schema 的 dataSource 字段里定义了三种模式polling 轮询、websocket 推送、mock 静态数据。轮询是最常见的适合变化不频繁、接口也不支持推送的场景我们直接在字段里配置interval就好。WebSocket 适合秒级刷新的大屏比如设备电压、温度这类生产数据推送过来之后直接更新组件。mock 则用于开发和演示环境不依赖后端也能把界面跑起来。每种模式都遵循同一个抽象接口返回一个统一的DataSourceResult对象包含data和error两个字段。这样组件层不关心数据到底是怎么来的它只负责渲染data。{ dataSource: { mode: polling, url: /api/v1/equipment/status, interval: 3000, headers: { Authorization: Bearer {{token}} } } }4.2 渲染器里的数据管线请求、转换、注入光有配置还不够渲染器需要真正发起请求并管理生命周期。这段逻辑我们抽象成一个useDataSource的组合式函数function useDataSource(source: DataSource | undefined) { const data ref(null); const error ref(null); let timer: any null; const load async () { if (!source) return; try { const raw await fetchData(source); data.value applyTransform(raw, source.transform); } catch (e) { error.value e; } }; onMounted(() { load(); if (source?.mode polling) { timer setInterval(load, source.interval); } }); onUnmounted(() clearInterval(timer)); return { data, error, reload: load }; }这里比较需要注意的是transform字段。很多后端接口的返回结构并不完全匹配组件 props我们允许在 schema 里配置一个转换函数名然后在渲染器中维护一个转换函数注册表。比如后端返回{ code: 0, result: { list: [] } }前端只需配置transform: unwrapResultList数据就会自动变成组件需要的数组避免每个组件里写重复的解析逻辑。4.3 断网与抖动比前端框架更怕的是数据源的稳定性数据接入这件事单纯从代码层面看并不复杂真正的复杂度在异常场景。大屏挂在墙上网络偶尔会抖动如果轮询请求失败后不处理组件会一直停在旧数据上屏幕看起来“死”了。我们的处理策略是失败时保留上一次成功的数据只在大屏的角落显示一个微弱的状态提示而不是清空画面。还有一个容易翻车的地方是请求并发。某些大屏轮询接口响应时间超过 interval比如 5 秒轮询但接口要 8 秒才返回前端可能会出现请求堆积。所以我们给每个数据源加了一个简单的重入判断上一次请求没结束这一次轮询就跳过保证不会越积越多。另外我们统一给轮询请求设置了一个超时时间默认 5 秒。这个值看起来很小但大屏场景下数据其实是“宁可晚一点也不能显示一个断裂的错误页”。定时器越简单越好核心数据链路不要承载太重的逻辑。5. 本地开发到上线这套流程怎么跑通配置驱动大屏开发听起来美好真正要把团队工作流搬过去还需要解决工具链和协作问题。我们大概花了三周才跑顺这条路下面说几个关键节点。5.1 一个 schema 仓库 一个渲染内核 所有大屏我们把所有大屏配置放在同一个仓库里目录结构很简单screens/ workshop-line-a/ schema.json command-center/ schema.json energy-monitor/ schema.json shared/ components/ Text.vue LineChart.vue ScrollList.vue renderer/ Renderer.vue useDataSource.ts渲染内核是独立部署的前端应用它根据 URL 参数加载对应大屏的 schema。比如访问/preview?screenworkshop-line-a就加载screens/workshop-line-a/schema.json。大屏幕上永远跑同一个构建产物只是加载的配置不同。这样新增大屏不需要重新构建前端只需要提交一份配置。这套模式最大的好处是减少了构建链路的干预。以前大屏修改要重新打包、上传静态资源、刷新浏览器现在只需要把 JSON 上传到 CDN或者直接在配置中心改一条记录刷新就能看到效果。5.2 可视化编辑器与手写 schema 怎么共存有人可能会问是不是必须开发一个拖拽编辑器才能用这个方案其实不一定。我们在最初一个月完全手写 schema也没有太痛苦因为 schema 字段足够少、足够直观。后来需求方希望能自己调布局位置我们才在现有 schema 基础上加了一个简易的编辑器。编辑器的本质是读取 schema渲染一个可视化的拖拽层用户拖完组件后把 layout 写回 schema最后点击保存生成新的 JSON。这个编辑器没有直接操作 DOM 来实现布局而是全程操作数据。因为 schema 本身就是开源的描述文件编辑器只是它的另一种可视化表达。给团队的协作建议是开发者继续用代码仓库维护 schema运营人员使用编辑器微调样式。两边共用同一个 schema 规范不会出现“编辑器改了一个字段开发又用代码覆盖回来”的冲突。文件版本管理能够保留每一次改动痕迹。5.3 多环境部署与贴屏调试的实用姿势大屏开发有一个特殊环节贴屏调试。开发时效果很好到了现场 55 寸大屏上却颜色发暗、字体偏小。我们的办法是把渲染内核的调试模式做成一个独立入口打开调试面板后可以模拟不同分辨率和缩放比例并覆盖当前大屏的 schema。这样在办公室就能做大量预演减少现场反复调试次数。另一个实用姿势是自动刷新。调试模式下渲染器会每隔三秒检查一次 schema 文件的时间戳有变化就自动重载。配合本地开发服务器你能实现“改 JSON 保存大屏立刻更新”的体验大大缩短反馈循环。这个功能虽然只有十几行代码却是我们团队效率提升最明显的工具。6. 这套方式的实际收益与适用边界任何技术方案都有适用边界。我需要实话实说这套 schema 驱动的方式不适合所有场景。写这篇文章不是要吹捧一个万能银弹而是记录它在什么条件下特别顺手以及哪些情况下你还得老老实实写代码。6.1 三个已上线大屏的量化对比我们上线最早的三块大屏分别是车间产线看板、能耗监测大屏、指挥调度中心。传统开发方式估算一块屏平均要 3 天完成首版加上后续需求变更的时间全生命周期大概 8 天。用 schema 驱动后首版配置时间缩短到 0.5 到 1 天后续每次需求变更基本在 0.5 小时内完成前提是组件库不需要新增组件类型。效率提升的主要来源并不是“写代码变快了”而是“大多数需求根本不需要写代码”。追加一张卡片、调整一个数据源、改一个刷新频率全部通过配置完成。它带来的收益不只是节省工时而是把大屏维护从“开发排期”中解放出来很多小改动当天就能上线。6.2 什么样的团队/项目不建议用这套方式如果你只有一块大屏做完就不再动它完全没必要引入 schema 和渲染器直接写页面更简单。如果大屏内组件类型极其复杂有大量自定义 3D 场景、复杂的手势交互、定制化物理引擎那你也很难用统一 schema 描述清楚。另外如果团队没有比较强的前端基础维护这套渲染器也会成为新的负担。schema 驱动更适合多屏复用、需求频繁变化、数据源多样、且有团队成员愿意维护组件库的场景。它把复杂度从“每一块屏的业务代码”转移到了“组件库和渲染器”上属于一次性的前期投入。团队人数不多但大屏数量很多时这种投入非常划算。6.3 我踩过的几个坑和最终的取舍记录最后分享一个让我印象深刻的坑。刚开始我们为了“灵活”允许 schema 的 props 里直接写 JavaScript 表达式比如fontSize: props.windowWidth 1920 ? 48 : 32。这样做确实很强但调试起来非常痛苦JSON 里出现大量嵌套模板字符串很多新手根本看不懂。后来我们把这个能力收掉只允许在 schema 中引用预定义的变量比如$screenWidthKey这种映射由渲染器统一解析。灵活和克制之间的平衡需要不断用实际教训换回来。还有一个取舍是我们没有把 schema 做得过于庞大。市面上有些配置平台会定义几十种组件类型、几百个属性字段学习成本很高。我们的 schema 只保留布局、样式、数据源、动画四类字段组件的特殊配置都收敛在 props 对象里由组件自己声明。保持 schema 小而美是它能够长期维护下去的关键。在实际项目中从第一块大屏切换到现在的完整方案前后花了大半年。最开始大家都不太习惯因为以前改页面是“打开代码找到文件”现在是“打开 schema 找到节点”。但一旦习惯了你会发现大屏开发本身已经变成了一种有趣的数据编排工作而不是重复的样板代码搬运。如果你也正在被多块大屏的维护成本困扰可以先挑一块简单的屏幕做试点把 schema 跑通再逐步扩大范围。很多事的价值只有真正用过之后才会明白。
阅读完成 · 觉得有帮助?