之前接了一个数据大屏需求周期压得很紧我当时就跟自己说这次绝不能再按老一套来了。所谓老一套大家应该都有画面感——一个巨大的前端项目里面塞满了写死的像素值、各种适配代码、一堆图表组件互相传参改一个布局能把人折磨到凌晨三点。这次我用了一种很新的大屏开发方式CSS Grid做骨架Web Components封装图表卡片整个页面由一份JSON配置驱动。不是自研低代码平台也不是付费大屏工具就是一套轻量的、能落到代码里的工程化思路。如果你也经常被大屏项目搞得头疼或者想绕过“复制上一版项目再改”的恶性循环这篇文章应该对你有用。我会把核心原理、完整实现、踩过的坑和适用边界都记录下来。1. 老一套大屏方案到底卡在哪了1.1 像素级适配的恶性循环先说说我之前用传统方式开发大屏时遇到的真实情况。设计师给一张1920x1080的设计稿然后我需要在各种分辨率的屏幕上让它都“看起来正常”。常见的做法是rem、vw/vh、再加一堆media查询。rem方案计算根字体大小所有尺寸都要除以基准值图表内部字体、间距都得跟着换算每写一个组件都在做小学除法。vw/vh方案1920宽度下一切正常换到2560宽的屏幕上所有模块被横向拉伸圆角变椭圆线图变粗视觉上非常难受。多重媒体查询一屏适配写完项目里多了几百行media query没人敢动一动就崩。这些方案的共同问题在于大屏不是页面它本质上是“在一个固定场景下投放的信息面板”受众不会拿手机缩放看也没有滚动条。用适配普通网页的思路去做大屏从一开始就在跟像素较劲。1.2 组件复用成了空话传统框架写大屏组件复用也只是看上去很美。一个“销售概览组件”从上一个项目复制出来要改props、改接口字段、改CSS变量、改父组件传参改完发现样式还是跟当前主题不一致。时间久了没人愿意复用新项目直接复制整页再改——相当于每一块大屏都是“一次性开发”成本高质量还不稳定。图表组件普遍犯的一个毛病是把数据请求、轮询逻辑、布局样式、主题配色全部耦合在一起。一旦换了一个数据源或者换了一款图表整个组件都要推倒重来。而大屏项目恰恰是多变的今天用折线图明天要换柱状图后天领导说加一个环形图这些变化都需要在组件层面被低成本承接。1.3 改版成本高到劝退还有更让人崩溃的改版。一屏改三个图、加两个模块牵扯到布局、图表实例生命周期、请求逻辑、自适应逻辑四五个地方同步动。有时候只是改一个数据源地址也要重新跑一遍构建、重新联调。实际上大屏的需求变更频率比业务系统高得多领导今天要看A指标明天要换成B维度后天说面板位置要对调。如果没有一套把“布局和组件”剥离开的机制每一次变更都是在叠技术债直到项目变成谁都不敢碰的屎山。我当时就在想大屏开发能不能反过来页面结构用配置声明图表能力用原生组件封装布局交给Grid适配交给Transform。这样改版只需要动配置加图表只需要加配置项整体复杂度会大幅降下来。2. 新方案的整体设计配置驱动 原生组件 Grid 骨架2.1 核心架构一句话这套方案的核心可以用三句话概括页面结构由一份 dashboard.json 描述页面骨架由 CSS Grid 实现图表能力由 Web Components 封装。数据请求和刷新逻辑全部内聚在组件内部页面本身只负责“读配置、逐个实例化组件、把配置丢给组件”。组件之间如果需要联动通过 CustomEvent 解耦不需要全局状态管理。用这种架构写大屏最直观的感受是页面代码几乎没有业务逻辑它只是一个“渲染器”。真正的内容全部沉淀在 JSON 配置里。布局改了改 JSON 里的 gridArea。数据源换了改 JSON 里的 dataSource。主题调了改一套 CSS 变量。这相当于把大屏开发的复杂度从“程序逻辑”转移到了“数据声明”上而数据声明是可以被业务方理解和修改的。2.2 为什么选 Web Components 而不是 React/Vue并不是 React/Vue 不好而是这套场景里我更看重“跨项目复用”和“框架无关”。用 Web Components 封装图表卡片有四个实实在在的好处原生自定义元素不依赖任何框架可以直接在一个纯静态页面里用也可以嵌到 React/Vue 项目里。Shadow DOM 天然隔离样式图表主题、卡片背景、加载状态不会跟宿主页面打架避免“样式越写越乱”的经典难题。组件有明确的生命周期connectedCallback 里初始化disconnectedCallback 里销毁非常适合图表这种需要管理实例的场景。没有构建层面的强约束不搞一套复杂的打包链路Vite 随手就能跑起来。如果你的团队全是 React 选手沿用 React 组件加 JSON 配置驱动也完全可以核心思路是一致的配置驱动、布局分离。Web Components 只是让这套方案更“通用”换项目不用迁移组件库。我第一次迁移这套组件时直接把整个 components 目录复制过去一行没改就能用这个体验在传统框架里很难实现。2.3 为什么布局选 CSS Grid transform scale这里我详细解释一下布局层的动机。大屏设计稿绝大多数是 16:9常见的是 1920x1080。用 vw/vh 方案在 1920 上正好但换成 2560 宽或者带鱼屏比例变了所有东西被横向拉伸用 rem 方案字体、边距、图表内边距都很难精确控制。我的选择是最外层容器固定设置 width: 1920px; height: 1080px再用 transform: scale() 整体缩放。内部所有尺寸都按 px 写死设计稿量出来是多少就写多少不需要任何换算。CSS Grid 负责把页面划分为若干区域每个卡片模块通过 grid-row / grid-column 来定位而不是绝对定位。这样设计带来的直接好处设计稿上的每一个像素都能精确落地还原度极高。模块位置调整只需要改 CSS 类或者 JSON 里的行列区间不需要动 DOM 顺序。整体缩放只会产生一次浏览器重绘不会像 rem 方案那样每个元素都在计算。用 transform scale 需要处理一个关键点缩放后容器在页面中的居中。容器要设置 transform-origin: top left然后根据窗口尺寸计算缩放比例和偏移量。这块下面会给出完整代码。我做过一张对比表可以直观看出几种方案的差别方案还原精度不同比例屏幕表现实现复杂度适用场景rem中比例稳定但换算繁琐中移动端适配习惯的团队vw/vh低拉伸变形严重低固定分辨率项目媒体查询高需要逐个断点调高页面级自适应transform scale高等比缩放留边居中低16:9 大屏首选3. 落地实现从零搭一个配置化大屏骨架3.1 项目初始化与目录结构我用 Vite 加原生 JS 初始化项目目录结构非常清晰dashboard/ ├── index.html ├── src/ │ ├── main.js │ ├── styles/ │ │ ├── theme.css │ │ └── grid.css │ ├── components/ │ │ ├── chart-card.js │ │ └── title-bar.js │ ├── config/ │ │ └── dashboard.json │ └── core/ │ ├── scale.js │ └── loader.jsconfig 只管数据声明core 管流程控制components 管表现逻辑。模块之间没有循环依赖每个人接手都能快速定位。我建议不管项目多小都要保持这个分层否则配置驱动的优势会很快被混乱的文件结构抵消。3.2 页面骨架Grid 布局与缩放适配HTML 骨架是一个三层的结构最外层 stage 是视口容器中间 screen 是固定 1920x1080 的设计稿画布里面的卡片由 loader 根据 dashboard.json 动态创建。div idstage div idscreen/div /div关键 CSS 是这些#stage { width: 100vw; height: 100vh; overflow: hidden; background: #0b1a2f; display: flex; align-items: center; justify-content: center; } #screen { width: 1920px; height: 1080px; transform-origin: top left; display: grid; grid-template-columns: repeat(12, 1fr); grid-template-rows: repeat(8, 1fr); gap: 16px; padding: 16px; box-sizing: border-box; }缩放适配的核心逻辑在这里const stage document.getElementById(stage); const screen document.getElementById(screen); function resize() { const sw stage.clientWidth / 1920; const sh stage.clientHeight / 1080; const scale Math.min(sw, sh); screen.style.transform scale(${scale}); const left (stage.clientWidth - 1920 * scale) / 2; const top (stage.clientHeight - 1080 * scale) / 2; screen.style.left ${left}px; screen.style.top ${top}px; } window.addEventListener(resize, resize); resize();这里我用了 Math.min 实现等比缩放而非 scaleX 和 scaleY 分别填满。等比缩放会让屏幕上下或左右留黑边但保证了图表不变形。大屏上的数据展示变形比留边更让人难以接受所以我会坚持等比缩放。等宽屏普及、设计稿改成 21:9 时只需要改 screen 的宽高和 resize 里的基数。3.3 字体与间距用CSS变量统一主题大屏基本都是暗色主题不需要搞复杂的多主题切换但是颜色管理不能乱。我在 theme.css 里定义了一套设计变量:root { --color-bg: #0b1a2f; --color-panel: rgba(16, 42, 84, 0.65); --color-border: #1e6b9c; --color-text: #d7f3ff; --color-accent: #00e4ff; --font-main: Roboto, PingFang SC, sans-serif; --radius: 8px; --gap: 16px; }所有组件内部都引用这些变量而不是写死颜色值。这样做的好处是当甲方说“这个蓝色太亮了整体压暗一点”的时候我只需要改两个变量所有组件同步生效。这个在传统开发里要全局搜索替换半天在 Web Components 里依然有效因为 CSS 变量可以穿透 Shadow DOM 边界。3.4 数据层统一请求与自动刷新每个 chart-card 组件自己接收 dataSource 配置比如{ type: line, dataSource: { url: /api/trend, interval: 5000 } }组件内部在 connectedCallback 里执行首次加载然后 setInterval 定时刷新disconnectedCallback 里 clearInterval。这样任何一个图表的刷新逻辑跟其他图表完全隔离不会因为一处接口挂了影响其他图。请求层用一个简单的 fetch 封装包含错误重试和请求幂等async function fetchData(url, { retry 3, delay 1000 } {}) { for (let i 0; i retry; i) { try { const res await fetch(url); if (!res.ok) throw new Error(HTTP ${res.status}); return await res.json(); } catch (e) { if (i retry - 1) throw e; await new Promise(r setTimeout(r, delay * (i 1))); } } }重试逻辑很关键大屏启动的时候往往服务端还没完全就绪如果没有容错一启动就是一片加载失败的红叉。我一般会在首次加载失败时显示一个友好的重试按钮而不是干等。4. 核心把图表封装成自由配置的卡片组件4.1 自定义元素的生命周期设计一个 chart-card 标签在页面里的使用方式非常干净chart-card config{...}/chart-card更推荐的做法是让 loader 统一读取 dashboard.json然后通过 property 注入配置。组件内部结构我选择用 Shadow DOM 隔离构造函数里 attachShadow然后所有 DOM 操作都在 shadowRoot 内完成。组件生命周期设计是这样的class ChartCard extends HTMLElement { static get observedAttributes() { return [config]; } constructor() { super(); this.attachShadow({ mode: open }); this._timer null; this._chart null; } connectedCallback() { this.render(); this.initChart(); this.bindResize(); this.loadData(); } disconnectedCallback() { if (this._timer) clearInterval(this._timer); if (this._chart) this._chart.dispose(); } }这里有两个容易被忽略的细节。第一connectedCallback 里不要直接对外部 DOM 做 querySelector因为组件可能被移动到新的容器里会导致重复绑定第二disconnectedCallback 里必须把 ECharts 实例 dispose 掉否则 canvas 会一直驻留内存大屏多图表场景下很快就卡。4.2 JSON配置如何驱动ECharts渲染完整的配置示例{ id: trend, component: chart-card, title: 近七日访问趋势, gridArea: 1 / 1 / 5 / 7, chart: { type: line, option: { color: [#00e4ff, #ff9f43], xAxis: { type: category }, yAxis: { type: value }, series: [{ type: line, smooth: true }] } }, dataSource: { url: /api/trend, interval: 10000 } }组件内部把 option 和数据源合并先拿到基础 option 结构再根据接口返回的数据填充 series 数据然后 chart.setOption(fullOption, true)。这里要注意第二个参数代表是否 notMerge决定了是整体替换还是增量合并。做数据刷新时我倾向于用 true 整体替换避免上一次的旧 series 残留。数据加载的核心方法是这样的async loadData() { const { url, interval } this._config.dataSource; const data await fetchData(url); this.updateChart(data); if (interval !this._timer) { this._timer setInterval(async () { const fresh await fetchData(url); this.updateChart(fresh); }, interval); } } updateChart(data) { const option this._buildOption(data); this._chart.setOption(option, true); }_buildOption 会根据接口返回的数据结构动态生成 series 数据这样图表类型、颜色、坐标轴这些结构配置与实时数据就完全分开了。4.3 图表自适应与联动刷新自适应这块千万不要再用 window.resize 了因为组件可能是后来动态创建的而且缩放容器本身会变化。更稳的是用 ResizeObserverbindResize() { const observer new ResizeObserver(() { if (this._chart) this._chart.resize(); }); observer.observe(this); }ResizeObserver 会监听宿主元素的尺寸变化图表自动重绘不会因为整体 scale 导致图表内部糊成一团。联动刷新是这套架构里最有价值的部分。组件点击事件里可以 dispatch 一个 CustomEventthis._chart.on(click, (params) { this.dispatchEvent(new CustomEvent(chart-click, { detail: { seriesName: params.seriesName, value: params.value }, bubbles: true, composed: true })); });宿主页面监听这个事件再更新其他组件的 config 属性或调用组件的方法。这里 composed: true 非常关键它决定了事件能不能穿过 Shadow DOM 的边界。没有这行配置事件会被 Shadow DOM 挡住外部永远收不到。我第一次写的时候漏了这个属性花了半天排查。5. 实测记录这套方案坑在哪里怎么绕过5.1 Shadow DOM里写ECharts样式踩到的坑ECharts 的 tooltip 默认是渲染到 body 的如果图表被包在 Shadow DOM 里tooltip 的样式就会掉到外部无法继承 Shadow 内部设置的主题甚至会因为 z-index 层级被其他元素盖住。对策是通过 ECharts 的 tooltip 配置里的 confine: true同时配合 renderMode 设置。更彻底的办法是在组件里把 tooltip 的 DOM 节点手动移入 shadowRoot。我实测下来配置 appendToBody 为 false不同版本字段名略有差异最省事但要注意容器的 overflow 不能是 hidden否则 tooltip 会被截断。如果你一定要用 overflow: hidden 裁掉圆角溢出那就只能在外部用全局样式定制 tooltip。我的建议是图表卡片的容器不要设 overflow: hidden圆角裁切用 border-radius 加 background 配合实现反而更灵活。5.2 scale缩放后字体和地图模糊的问题整体 scale 之后如果浏览器窗口缩放比例不是整数字体和 SVG 元素会产生轻微的模糊。这个坑在普通网页上几乎发现不了但大屏投到大尺寸显示器上会非常明显。对策有三个给 ECharts 实例设置 devicePixelRatio让 canvas 按实际像素渲染。当缩放比例是 1.5 时默认的 devicePixelRatio 会导致 canvas 被拉伸字体发虚。字体尽量使用 font-weight: 500 而不是 300粗一点的字体在缩放后模糊感会小很多。地图类的 SVG 最明显如果是 ECharts 的地图让它在 canvas 渲染模式下工作而不是 SVG 渲染模式如果只是背景网格直接用 CSS 重复渐变不要用图片。这个模糊问题不是 absolution 能解决的实测下来只是“可接受”和“很难看”的区别。对于追求极致像素的人来说还是得回到 rem 方案但那就得接受更高的开发成本。5.3 数据更新时setOption的合并策略这块值得展开因为很多人在这里翻车。第一次渲染用 chart.setOption(option, true)之后每次接口返回数据如果直接 setOption({ series: [{ data: newData }] }, true)你会发现图例变了旧数据还在甚至 series 数量越来越多。原因是在 notMerge 模式下ECharts 会整体替换配置但如果你传入的 series 里没有完整的 name、type 字段它不会智能识别为“更新原 series”而是会生成新的配置覆盖。我的经验是结构变化时用 true 全量替换保证干净。只有数据变化且结构稳定时用默认 merge 模式但需要把无用的属性置空。数据刷新场景中最简单可靠的是用 Object.assign 把基础 option 和动态 option 合并后再 setOption(merged, true)同时保证 series 的 name 和 type 与初始配置一致。另外刷新数据时如果用户正 hover 在图表上tooltip 会闪断非常影响体验。处理方式是在合并后的 option 里保留 tooltip 的 show 属性并且刷新频率不要低于 5 秒低于 5 秒人眼会持续感知闪烁。5.4 性能多图表页面卡顿的优化手段一个大屏动辄十几个图表每个都有定时器、都有 canvas如果不做性能控制CPU 占用会迅速拉满。我实测的优化手段有四个定时器只在页面可见时运行监听 visibilitychange页面隐藏在后台时停止所有组件刷新回到前台再立即恢复。后台大屏不需要实时数据这个优化能省大量资源。图表不可见区域不渲染配合 IntersectionObserver等卡片真正进入可视区再 initChart而不是页面加载时一次性初始化所有图表。减少重复请求多个图表如果共用同一个接口数据在请求层做一个简单的 Map 订阅相同 URL 的请求只发一次多个组件共享结果避免同一个接口被请求七八次。及时 dispose 无用实例切换配置卡片时旧实例必须 dispose否则 canvas 会一直挂在 DOM 里。这几个手段叠加之后效果很明显。我之前某个项目里 12 个图、每个 5 秒刷新用这些手段之后 CPU 占用明显降了下来风扇不转了演示的时候不再尴尬。具体的数字我就不放了每个项目差异很大但方向是通用的。6. 什么项目适合用这套方案什么情况别硬上6.1 最合适的落地场景这套方案最适合三类场景展示型大屏、需求变更频繁的大屏、多个团队各自维护一块屏的场景。运营监控大屏指标多、数据源多、刷新频率不同配置驱动天然适合。数据汇报大屏领导今天要看这个指标明天换那个维度改 JSON 即可不需要开发介入。多团队共用底座不同业务线各自维护自己的 dashboard.json通过组件库共享互不干扰。它不需要高度交互、不需要复杂路由页面结构完全可以通过配置变更来调整。如果符合这些特征这套方案的投入产出比会非常高。6.2 不要为了新而新方案的边界也得说点大实话不然就是误导。这套方案不是银弹以下情况别硬上页面有大量表格、复杂表单交互、多页面跳转时Web Components 加配置驱动反而会变复杂不如老老实实用框架。团队没人熟悉自定义元素看到 attachShadow 就头晕学习成本也要算进去不要低估。屏幕比例千奇百怪除了 16:9 还有 5:4、1:1 的scale 方案的居中留边会浪费大量显示面积。比如一个 5:4 的竖屏等比缩放后两侧留黑边信息密度就上不去了。图表极其复杂比如世界地图叠加飞线、3D 地球这种还是建议用成熟的大屏产品或专门的 3D 可视化方案。ECharts 的普通图表封装成配置没问题但 3D 场景下 Web Components 的自绘逻辑会变成一座大山。我知道有些人看到“配置驱动”就联想到低代码然后说这不适合复杂场景。我的观点是工具要放在合适的场景里才会发挥价值大屏本身就是一种相对轻量的展示页把复杂度前置到配置层收益远大于成本。6.3 后续可以继续扩展的方向这套架构的扩展空间很大我自己已经列了几个方向在计划里把 dashboard.json 放到服务端做成在线配置下发业务方改配置不用发版运维大屏上线效率会提高一个量级。把通用图表组件沉淀成 npm 包跨项目复用慢慢积累出一个内部的“可视化组件市场”。增加模板能力同一个配置在不同尺寸屏幕上用不同的 gridArea 自动重排解决多比例屏的适配问题。接入低代码编辑器的拖拽改 JSON可视化调整模块位置让业务方通过图形界面配置大屏。这些方向并不需要推翻现有架构都是基于当前这套骨架做增量扩展。这也是我选择 Web Components 的一个重要原因它在扩张过程中不锁死任何技术栈。最后说点我个人的体会。把大屏开发从“写页面”变成“写配置”之后最大的变化不是代码量少了而是心不累了。以前改一块大屏我得前后端一起伺候现在我把 dashboard.json 扔给后端同学他都能自己加一个图表。如果你手头正好要接大屏项目不妨从这篇文章里的最小骨架开始改一版试试。第一遍会慢一点第二遍开始你就会觉得回不去了。
阅读完成 · 觉得有帮助?