做了这么多年前端,最怕接到的需求之一就是大屏开发。不是技术难,而是烦。一个驾驶舱页面,图表十几个,布局要像素级对齐,还要适配各种分辨率的投屏终端,改一次调半天。传统做法就是开个 Vue 项目,装 ECharts,一个图表一个图表地写 option,写完再手动调布局,等项目上线,光 CSS 和自适应就耗费大半时间。
这期想分享一种最近项目中实际用下来的新开发方式:用一个开源的大屏开发设计器完成页面布局和组件样式配置,把导出的配置文件接入自己的前端工程,再在代码层面接管数据请求、交互逻辑和部署流程。说白了,就是拖拽生成界面,代码掌控逻辑。用下来最大的感受是,把大屏开发里最消耗精力的“排版”和“重复造图表”环节砍掉了,同时保留了程序员对数据和交互的完全控制权,不像商业平台那样被锁死在私有环境里。
这套方式比较适合被大屏项目反复折腾的前端开发者、可视化工程师,也适合想快速出效果但定制需求又多的中小团队。下面从整体思路、工具选型、实操细节、踩坑记录四个角度完整复盘一遍,希望能给你提供一条新的解决路径参考。
1. 旧路子的问题与新方案的总体思路
1.1 手写代码的累与旧式低代码的坑
先说传统方式。大屏开发最常见的做法是前端手写,技术栈一般是 Vue 或 React 加 ECharts,复杂一点的套用 DataV 组件库或 AntV G2。这种方式获得自由度最高,但有两个绕不开的问题。
第一是布局调整效率低。客户很少一次把需求说清楚,经常是“这个模块往左一点”“标题字号再大一点”“中间地图占比再高一些”这类微调。每次调整都要先在组件上改样式,再刷新整个页面去确认效果,一屏十几个图表模块,来回改一晚上是常有的事。如果用了栅格布局,牵一发动全身,靠 CSS 适配不同分辨率屏幕时更痛苦,经常出现一个屏幕正常另一个屏幕错位的尴尬。
第二是组件复用成本高。ECharts 虽然图表类型丰富,但每次开发都要重新写 option、配数据格式、处理 tooltip、处理 loading 态,本质上是在做重复劳动。哪怕封装了通用组件,遇到不同项目的 UI 风格差异,还是要改 props、改样式逻辑。
后来尝试过商业化的低代码大屏平台。这类平台开箱即用,内置大量模板,拖拽组件就能交付,确实快,但坑也不少。最典型的是数据必须经过平台方转发,很多企业对数据出域非常敏感,核心经营数据不能放在第三方平台。其次是交互自由度和代码可维护性差,平台生成的页面很难跟现有系统做单点登录、权限控制、深度交互联动,出了问题也没法自己排查,只能提交工单等平台方响应。
1.2 新思路:拖拽调样式,代码管数据
我在一次项目里尝试了一种介于“纯手写”和“商业低代码平台”之间的方案,核心思路是三个字:分离。把大屏开发拆成两段,视觉排版和样式配置放在可视化设计器里完成,数据对接、动态交互和工程化部署放在传统代码工程里完成。
具体执行路径是:先用开源的大屏设计器拖出一个页面,把占比、尺寸、图表类型、配色这些配置保存成一份 JSON 或者实现好的前端组件代码。然后在我的前端工程里引入设计器对应的渲染器,这张 JSON 配置文件交给渲染器解析并渲染到真实浏览器环境中。剩下的工作,比如请求后端真实数据、定时轮询、点击弹窗、钻取联动,全部通过代码注入到组件上。
这样做的好处非常直接。
- 调整布局不再需要改代码后刷新页面反复试,打开设计器拖拽几下,导出配置,刷新浏览器就完成了。
- 数据接口不再经过任何中间平台,前端工程直接请求自己的后端服务,域名、鉴权、数据格式全部自己说了算。
- 整个大屏页面本质上是自己工程里的一部分,路由守卫、权限控制、登录态这些机制天然兼容,可以无缝嵌入已有的后台管理系统。
- 不想用设计器默认的组件时,还可以自己封装一个可视化组件注册进去,预览时直接看到效果,自由度比商业平台高一大截。
1.3 这条路的适用范围和边界
这套方案并不是要替代所有场景,它最适合的,是那些视觉上“常规但有要求”、数据上“必须自主可控”、交付上“需要快速迭代”的大屏项目。比如企业内部运营监控大屏、智慧园区管理大屏、电商实时销售看板等,这类项目的共同点是图表以常见类型为主(折线图、柱状图、饼图、地图、滚动列表),但布局和配色需要反复打磨,部署环境要求私有化。
如果项目需要高度定制的 3D 地球、复杂物理粒子效果、完全自定义的交互动画,那还是老老实实写代码更合适。设计器能覆盖的是通用市场的大约八成需求,剩下的两成定制场景,要么通过自定义组件扩展,要么直接切回手写模式。我的建议是把这套方案当作一个“加速器”,而不是万能药,合适就用,不合适别硬凑。
2. 工具选型与环境搭建
2.1 大屏设计器怎么选
走这条路的第一步,是选一个靠谱的大屏设计器。市面上可选的方案不少,有 Vue 系也有 React 系,我在选型时重点看五个维度。
第一,组件生态是否够用。内置图表至少要覆盖常用的折线、柱状、饼图、雷达图、地图、滚动表格、数字翻牌器。不然拖到一半发现缺图表,还得自定义组件救场,就很被动。第二,是否支持自定义组件。这个直接决定工具的天花板。第三,导出能力。有的设计器只支持导出 JSON 配置,有的支持生成完整前端代码,有的两者都行。第四,社区活跃度。开源项目最怕的就是作者弃坑,遇到 bug 连问的地方都没有。第五,私有化部署和数据安全能力。纯前端工具型产品优先,数据绑定在代码层完成,设计器本身不碰数据的最安全。
综合比较之后,我选择的是一个基于 Vue3 + TypeScript 的开源大屏设计器。选择它有几点考量:底层渲染依赖 ECharts,图表类型不用愁;设计器支持自定义组件开发,而且提供了一套标准的注册接口;它导出的配置是标准 JSON,解析和二次修改都很方便;社区相对活跃,issue 响应速度快,遇到问题能查到相关资料。
这里说句题外话,也有人会直接选商业平台的免费版或者试用版,但我个人还是建议优先考虑开源方案。大屏开发本质上是把数据和样式输出到屏幕上,这个过程如果受制于人,后期维护会很难受。开源工具至少能保证项目在自己手里可控,哪怕是代码有问题,也能自己修。
2.2 前端项目骨架的搭建
既然核心思路是“渲染器 + 配置文件”跑在自有工程里,那么工程基础还是常规的前端项目骨架。我这次用的是 Vite + Vue3 + TypeScript,模板选的是 Vue3 + TS 的组合,组件库按需引入 element-plus,图表库用 ECharts 5。如果项目是 React 技术栈,思路完全一样,只需要找对应技术栈的设计器和渲染器。
初始化的步骤,从创建工程到依赖安装完整走一遍。
# 使用 Vite 创建 Vue3 + TypeScript 项目 npm create vite@latest my-big-screen -- --template vue-ts # 进入项目目录 cd my-big-screen # 安装基础依赖 npm install # 安装路由和状态管理 npm install vue-router@4 pinia # 安装 UI 组件库 npm install element-plus @element-plus/icons-vue # 安装 ECharts npm install echarts # 安装设计器对应的运行时渲染器 npm install @big-screen/runtime项目基本目录结构按下面这样划分,目的很明确:把设计器生成的配置文件、自定义扩展组件、页面入口三者分开,互不污染。
src/ ├── api/ # 接口请求模块 │ └── dashboard.ts # 大屏数据相关接口 ├── components/ │ └── big-screen/ │ ├── config/ # 设计器导出的 JSON 配置 │ ├── custom/ # 自定义组件列表 │ └── index.vue # 大屏渲染容器 ├── views/ │ └── screen/ │ └── index.vue # 大屏页面入口 ├── router/ │ └── index.ts └── main.ts2.3 设计器与项目的三种对接方式
设计器选好了、工程搭起来了,接下来要解决一个关键问题:设计器里做的内容,怎么进到自己的工程里?根据选型不同,大致有三种方式。
第一种是完整代码导出。设计器直接生成一个独立的 Vue 项目,代码全部生成好,你可以直接拿到本地运行。这个方式最省事,但生成代码的可读性和可维护性一般,后续想要深度改造成本高。适合要求不高的交付型项目,不适合复杂系统。
第二种是配置 JSON 解析渲染。设计器只导出页面配置的 JSON 文件,工程里通过官方的 runtime 运行时组件解析并渲染。这个方式的优点是页面的任何细节都在 JSON 里清晰可见,改布局可以直接改 JSON,也可以在浏览器里借助调试面板动态修改。缺点是页面复杂度高时 JSON 会很长,初次上手需要理解配置结构。
第三种是运行时动态接入。设计器和前端应用共用一份配置源,比如设计器把配置保存到后端数据库,前端应用通过接口动态拉取配置并渲染。这种方式最灵活,甚至可以实现运营人员在设计器里维护页面、开发人员在工程里保障数据和功能的协作模式。适合有管理后台支撑的中大型项目。
我这次采用的是第二种和第三种结合的方式。设计器导出的 JSON 作为每块看板的静态配置随前端工程打包,真实数据通过接口注入到组件的 data 属性中。这样既保留了 JSON 配置的直观性,又实现了数据和样式的完全解耦。后面如果要做页面管理后台,只需要把这套静态 JSON 改造成后台下发即可,改动成本可控。
3. 核心实操:从空白画布到跑起来的大屏
3.1 设计器里的高频操作
当设计器服务和前端工程都准备好后,核心工作就从“写代码”变成了“在画布上搭页面”。这一步看似简单,但里面有很多影响最终产出的细节。
打开设计器,第一件事是设定画布尺寸。不要随手填 1920x1080 就完事,要先想清楚目标投放屏幕的分辨率。对于会议室常见的 1080P 屏幕,画布尺寸设 1920x1080 没问题。但如果投屏目标是竖屏一体机,就得设为 1080x1920。如果不知道目标分辨率,我建议直接用 1920x1080,这是最通用的分辨率,后面适配时相对好处理。
接下来拖拽组件。设计器左侧通常是组件列表,中间是画布,右侧是属性面板。拖动组件到画布后,调整位置和大小的方式跟 PPT 很像,选中组件后可以拖动边缘缩放,也可以直接改右侧面板里的 x、y、width、height 数值。这里有个实操技巧:布局时先用网格对齐把每个模块的大致位置定下来,精确数值最后统一在右侧面板里调。网格对齐适合前期快速规划,数值微调适合后期精确控制,两者结合比单用鼠标拖得快得多。
每个组件的属性配置也要花点心思。以折线图为例,设计器提供的属性包括图表标题、图例位置、坐标系样式、数据系列名称、线条颜色、是否平滑、是否显示数据标签等。这些配置项其实就是在帮我们生成 ECharts option,所以如果你熟悉 ECharts,配置起来会非常顺手。我的习惯是先在设计器里把配色和数据模式选好,导出后到代码里再针对特殊需求做二次调整。
3.2 数据接入:从静态数据到真实接口
设计器里刚拖出来的组件,默认绑定的都是静态数据,也就是写死的一组示例数据。这在预览时没问题,但真实项目中数据永远来自后端接口。所以下一步的关键动作是数据接入。
这里要说清楚一个概念:设计器只管配置展示形式,数据绑定是在代码层完成的。渲染器解析 JSON 配置后,会生成对应的图表实例,图表实例的数据来源由一个统一的 data 属性注入。我的做法是,在大屏页面组件里写一个 loadData 方法,调用后端接口获取数据,然后把返回的数据映射成图表需要的结构,赋值给渲染器配置。
// 大屏页面入口处的一段数据加载逻辑 import { ref, onMounted } from 'vue' import { fetchDashboardData } from '@/api/dashboard' const screenConfig = ref(import('@/components/big-screen/config/main-screen.json')) const chartData = ref({}) const loadData = async () => { const res = await fetchDashboardData({ range: 'today', region: 'all' }) // 后端返回的数据结构,映射成图表组件需要的结构 chartData.value = { salesTrend: res.trendData, // 折线图数据 categoryRatio: res.categoryList, // 饼图数据 rankingList: res.ranking, // 排名列表数据 totalRevenue: res.totalAmount, // 翻牌器数据 } } onMounted(() => { loadData() })这样做的意义是,设计器里导出的 JSON 只控制界面长什么样,而数据是活的。后端接口调整字段、新增维度、改变数据粒度,前端只需要修改 loadData 里的映射关系,页面布局完全不用动。
还有一个细节值得提一下。处理数据映射时,先把后端接口返回的原始结构梳理清楚,再对照设计器组件要求的数据结构做转换。很多时候组件渲染不出来,不是配置错了,而是数据结构对不上。比如后端返回一个对象数组,图表却要求 key-value 格式,不转换就空白。
3.3 数据接入后的刷新机制
大屏项目跟普通后台页面不一样,它是“挂墙展示”的,不会有人去刷新浏览器。所以数据必须自己刷新,这个机制要在代码层做。
常见的做法是轮询,也就是用 setInterval 定期调用 loadData 方法。轮询时间的设置要综合考虑数据变更频率和接口压力。实时性要求高的销售大屏,可以设 5 秒或 10 秒一次;一般的数据看板,30 秒到 1 分钟足够了。我建议不要低于 5 秒,否则接口压力大,页面动画也会因为频繁更新而卡顿。
轮询的实现还要注意一个问题:避免上一次请求还没返回,下一次请求又发出了。React 和 Vue 项目里常见的解法是加一个 loading 标志位,或者用 AbortController 取消上一次请求。更稳妥的做法是使用 setTimeout 递归调用,而不是 setInterval 固定间隔,这样可以保证上一次请求完成后再开始计时。
let timer: ReturnType<typeof setTimeout> | null = null const startPolling = (interval = 30000) => { stopPolling() const tick = async () => { await loadData() timer = setTimeout(tick, interval) } tick() } const stopPolling = () => { if (timer) { clearTimeout(timer) timer = null } }页面销毁的时候记得调用 stopPolling,防止组件卸载后定时器还在跑,造成内存泄漏。这个坑很容易被忽略,尤其在大屏嵌入后台路由的场景里,路由切走再切回来,定时器叠加,界面会明显卡顿。
3.4 自定义组件:把一个列表组件装进画布
设计器自带的组件再丰富,也免不了遇到“这个效果我非要自己做”的场景。这时候自定义组件扩展就派上用场了。设计器如果支持自定义组件,通常会提供一个标准的注册接口,我把这个过程的路径走通后,以后任何大屏项目都可以复用。
这次的项目里需要做一个公告滚动列表,设计要求是文字单行滚动、悬停暂停、点击可跳转详情。这种组件在设计器内置的滚动列表里做不到,所以我自己封装了一个轮播列表组件,再注册到设计器里。
组件本身不多解释,就是一个标准的 Vue 单文件组件,接收 data 属性作为数据源,内部用 CSS3 transform 做滚动动画。核心在于注册过程。设计器会在画布中渲染自定义组件时,把组件放入一个预览容器里,这里边需要做两件事:声明组件暴露出来的数据配置结构,以及在设计器预览面板里渲染真实效果。
// 自定义组件注册的简化写法 import CarouselList from './CarouselList.vue' export default { name: 'CarouselList', component: CarouselList, // 声明组件的可配置项,设计器右侧属性面板由此生成 props: { data: { type: Array, default: () => [] }, speed: { type: Number, default: 2, description: '滚动速度' } }, // 预览时用到的默认数据 defaultData: [ { title: '示例公告标题', date: '2024-06-01' } ] }注册完成后,设计器组件列表里就会出现 CarouselList,拖进画布就能预览,右侧属性面板自动生成 speed 配置项。真实大屏运行时,这个组件的 data 值在 loadData 方法中通过接口数据注入,滚动速度和点击跳转逻辑则写在组件内部。
自定义组件的扩展使得这套方案不再局限于“内置图表拼凑”,而是可以逐步积累自己团队的专属组件库。每做完一个项目,把抽象出来的组件注册进设计器,下次做类似大屏只需要拖拽配置,效率会越来越高。
3.5 屏幕适配:这次我用的是等比缩放
大屏开发绕不开的一个难题是分辨率适配。投屏终端可能是 1920x1080,可能是 2560x1440,甚至可能是 3840x2160 的 4K 屏,如果方案不做适配,页面就会出现留白、溢出、文字模糊等问题。
常见适配方案有两种。一种是 vw/vh 单位适配,让每个组件的宽高和位置都按视口尺寸计算,优点是不同分辨率下内容始终铺满屏幕,缺点是当大屏在 16:9 比例之外的屏幕上展示时,图形会被拉伸变形。另一种是等比缩放,把整个页面当作一块固定尺寸的画布,通过 CSS transform scale 整体缩放适配屏幕。优点是页面在任何尺寸下都维持设计稿比例,不会变形,缺点是在超大屏上可能会留出黑边。
这次项目的目标屏幕有 1080P 和 4K 两种,且都是 16:9 比例,我采用了等比缩放方案。实现起来非常直接,页面容器固定为 1920x1080,然后根据浏览器实际视口宽高计算缩放比例。
const screenScale = () => { const designWidth = 1920 const designHeight = 1080 const scaleX = window.innerWidth / designWidth const scaleY = window.innerHeight / designHeight const scale = Math.min(scaleX, scaleY) const screen = document.getElementById('screen-container') if (screen) { screen.style.transform = `scale(${scale})` screen.style.transformOrigin = 'left top' } } window.addEventListener('resize', screenScale)注意 transformOrigin 必须设为 left top,否则缩放时页面会从中心缩放,导致左上角偏移。另一个细节是,如果目标屏幕存在系统 UI 工具栏,占用了部分屏幕高度,要先用 window.availHeight 而不是 window.innerHeight 计算可用区域,否则页面底部会被遮挡。
4. 常见问题与排查技巧实录
4.1 导出配置与本地项目依赖不一致的兼容处理
初次接入这套方案时最容易遇到的一个问题:设计器版本和本地引入的 runtime 渲染器版本不一致,导致导出的 JSON 里某些属性字段没有被渲染器识别,页面局部空白或组件报错。
我遇到过的情况是设计器里看起来正常的地图组件,在本地工程里渲染不出来了。排查时发现设计器端地图组件已经升级到新版数据格式,而本地安装的 runtime 版本依赖的还是旧版解析逻辑。这种问题解决方式很简单,查看文档确认两者版本对应关系,把本地依赖升级到匹配版本即可。
这里有一个经验:接入项目的一开始就要把设计器和 runtime 的版本号固定住,并在项目的 package.json 里显式锁定版本,不要随便升级。开箱即用的大屏设计器普遍迭代快,版本兼容性偶尔会有问题,与其升级后花时间排查,不如保证开发环境一致。
4.2 大屏刷新后配置丢失与状态恢复
有个现象容易被忽略:设计器导出的配置 JSON 是静态文件,但大屏页面通常包含一些运行时才产生的状态,比如用户切换了大屏的 Tab、筛选了某个时间维度、展开了某个隐藏面板。如果在页面刷新前没有保存这些状态,刷新后就会回到配置文件的初始状态。
有两种处理思路。一是简单方案,把状态保存在 URL query 参数中,每次状态变更时更新路由,刷新后从路由参数恢复。适合状态量少、结构简单的场景。二是复杂方案,把状态放入浏览器 localStorage 或 sessionStorage,刷新时优先从缓存恢复。适合状态量多、嵌套深的场景。
大屏项目我一般推荐简单方案优先。大屏本身是展示型页面,交互状态不应该太复杂,让每个 Tab、每个筛选条件都在 URL 上体现,方便分享和排错。状态恢复要写在路由守卫里,确保前端路由跳转出去再返回时,页面能回到上次浏览的视图。
4.3 自适应下文字模糊和拖影的排查
做了等比缩放以后,页面缩放在 4K 大屏幕上的效果比预想中差一点:文字边缘发虚,高速滚动的列表有拖影。这个问题折磨了我一个下午,最后定位到两个因素。
文字模糊的直接原因是缩放比例不是整数。1920 宽度缩放到 3840 宽度时,比例是 2,这是理想情况,字体不会模糊。但很多时候屏幕宽度是 2560,缩放比例是 1.3333,浏览器对非整数倍缩放的处理会降低渲染清晰度。解决思路是:页面内尽量使用矢量图形(ECharts 本身就是 Canvas 或 SVG 渲染,不受影响),少量需要高清展示的文字模块,可以让设计器输出更高分辨率的底图,或者干脆用图片替代部分装饰文字。
拖影问题通常是 CSS 动画在缩放后的兼容性问题。滚动列表的 transform 属性在缩放容器内频繁变化时,部分浏览器会开启 GPU 加速,合成动画出现拖影。解决方法是为滚动元素加一层样式,强制开启硬件加速,同时给列表项设置 backface-visibility 属性。
.scroll-list-item { transform: translateZ(0); backface-visibility: hidden; will-change: transform; }这类问题在不同内核浏览器上表现差异很大。给客户的部署机建议固定使用同一版本的主流浏览器,避免同一套页面在不同浏览器上出现观感差异。
4.4 数据轮询与动画的性能管控
数据轮询频繁加上图表动画过度,很容易造成页面卡顿。尤其是同时展示十几个图表组件的大屏,每秒都有数据更新,CPU 占用率飙升。
性能优化的几个关键动作要提前做。第一,关闭非必要动画。ECharts 的默认入场动画和更新动画在大屏场景中吸引力有限,却占用不低的渲染资源,对实时性要求高的模块,在配置里关掉动画。第二,按需渲染。大屏未被激活的 Tab 页面延迟加载,不要一次性把所有图表都渲染出来。第三,控制更新频率。轮询周期尽量不要低于 10 秒,如果后端数据源本身是分钟级更新,前端完全没必要 5 秒一轮。第四,针对大数据量图表启用 sampling,比如折线图只绘制降采样后的数据点,大幅减少 Canvas 绘制压力。
有一个比较容易忽略的细节:ECharts 实例在组件销毁时必须 dispose。大屏页面反复开关时,如果旧实例没有释放,内存会持续上涨,最终页面掉帧甚至白屏。在 Vue 组件卸载钩子里对这一块做清理,属于常规操作,但非常重要。
4.5 常见故障速查表
最后把这次项目中遇到的问题整理成一个速查表,方便你排查同类问题时快速定位。
| 现象 | 可能原因 | 处理方式 |
|---|---|---|
| 配置引入后页面白屏 | JSON 文件格式错误或渲染器版本过低 | 检查 JSON 是否合法;确认 runtime 与设计器版本匹配 |
| 某个图表不渲染 | 数据字段名与组件要求不一致 | 在 loadData 中把接口数据映射为组件要求的 key |
| 地图区域显示空白 | 地图组件数据源缺失或 geoJSON 加载失败 | 确认地图数据文件已部署,优先使用内网资源 |
| 双击全屏后布局偏移 | 忽略了全屏 API 造成的视口尺寸变化 | 在全屏状态变化事件里重新调用缩放计算函数 |
| 缩放后文字模糊 | 非整数倍缩放 | 用矢量图表或图片替代关键文字模块 |
| 数据更新后图表闪一下 | 动态更新数据时重建了实例 | 使用 setOption 而不是先销毁再创建实例 |
| 页面切走再返回后不再刷新 | 定时器在组件销毁时未被清理 | 在 onUnmounted 钩子中调用 stopPolling |
| 渲染器报错找不到组件 | 自定义组件未注册 | 检查自定义组件是否在设计器配置中正确引入 |
我在实际使用这套方案时最深的体会是,它真正把大屏开发的重心从“调整样式”转移到了“数据逻辑”。以前改一个坐标轴颜色都要改代码刷新页面,现在在画布上点两下就行,时间省下来的同时,整个项目的可维护性也提升了。如果你手头正有被反复折腾的大屏项目,不妨试一下“设计器拖界面 + 代码接数据”这个组合,感受会很不一样。最后的建议是,别被工具束缚,设计器解决它擅长的问题,剩下的交给代码掌控,两者配合才能让大屏开发又快又稳。