ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Vue3大屏自适应方案详解:scale/rem/vw与ECharts适配实践

Vue3大屏自适应方案详解:scale/rem/vw与ECharts适配实践 做可视化大屏有一段时间了从早期的PC端图表到后来的Vue3大屏项目踩过的适配坑不算少。大屏自适应这个问题几乎每个接到可视化项目的同学都会遇到——设计稿是1920x1080客户现场的屏幕可能是2560x1440也可能是1366x768还有各种比例的拼接屏。如果按照固定像素写死换一台设备就崩如果用百分比或flex图表字体和间距又很难精确还原设计稿。这篇文章我把Vue3项目里常用的几种大屏自适应方案整理出来包括每种方案的原理、核心代码、适用场景和已知坑点供正在做类似项目的朋友参考。1. 大屏自适应的核心矛盾与设计思路1.1 为什么大屏自适应这么烦先理清楚问题到底出在哪。大屏项目不同于普通后台管理系统它有几个先天性的硬约束第一设计稿是固定尺寸。绝大多数大屏UI设计稿是按照1920x1080出的有些可能是2560x1080这种超宽屏。设计师用固定像素标注你要做的就是让这套固定尺寸的稿子在各种屏幕上尽量“一模一样”。第二大屏项目的容器可能是拼接屏、LED屏、普通显示器它们的物理分辨率和浏览器视口比例都可能不同。普通后台项目只需要“能用”大屏项目要求“还原”这两个目标的实现路径完全不一样。第三图表类组件ECharts、AntV G2等大多数是基于Canvas绘制的Canvas元素本身只认像素尺寸不认CSS的rem、vw这些相对单位。这就是为什么很多同学发现我用了postcss-pxtoremECharts的字体和图形却不跟着变——因为那些px被写在了ECharts的option配置里根本没经过CSS处理。所以大屏自适应的本质不是“让页面在不同宽度下重新布局”而是“让一套固定设计稿在不同尺寸的屏幕上等比呈现”。想通了这一点方案选择就清晰了。1.2 四条技术路线的总体对比目前主流的适配方案可以归为四类scale缩放、rempostcss-pxtorem、vw/vh视口单位、CSS3媒体查询flex栅格。它们解决的是不同层次的问题scale缩放整体等比缩放简单粗暴适合内容铺满全屏、不需要滚动的展示型大屏。rem方案通过修改html根字号让以rem为单位的元素随屏幕宽度变化适合内容较多、允许纵向滚动的页面。vw/vh方案用视口单位直接描述元素尺寸纯CSS实现适合布局结构简单、追求代码纯净的项目。媒体查询flex按断点切换布局适合交互型页面或管理后台型的“伪大屏”。我在实际项目中通常不会只用一种。比较常见的组合是外层容器用scale或rem做整体缩放内部的图表组件用监听resize重新计算尺寸来适配。这样既能保证设计稿还原度又能让图表这类Canvas内容跟着容器尺寸变化。2. 方案一scale缩放适配应用最广泛的做法2.1 核心原理scale方案的思路来自一个很朴素的逻辑既然设计稿是1920x1080那我就在页面上固定一个1920x1080的容器把所有的内容都画在这个容器里然后用CSS的transform: scale()把整个容器缩放到当前屏幕的尺寸。这么做有几个显而易见的好处开发者的心智负担极低。开发时完全按照1920x1080的尺寸写px不需要换算不需要考虑rem甚至不需要考虑flex如何布局。页面所有元素包括ECharts的Canvas画布都会等比缩放不会出现图表字体突兀的情况。不会出现换行、错位、间距不均的问题因为整体缩放是按比例进行的元素之间的相对关系完全不变。2.2 Vue3中的完整实现我封装了一个useScale的组合式函数代码不长但包含了几个关键细节。// hooks/useScreenScale.ts import { ref, onMounted, onBeforeUnmount } from vue export function useScreenScale(designWidth 1920, designHeight 1080) { const scale ref(1) const screenRef refHTMLElement() const updateScale () { const windowWidth document.documentElement.clientWidth const windowHeight document.documentElement.clientHeight // 按宽度比例和高度比例分别计算取较小值 const scaleX windowWidth / designWidth const scaleY windowHeight / designHeight scale.value Math.min(scaleX, scaleY) } const setTransform () { if (!screenRef.value) return screenRef.value.style.transform scale(${scale.value}) screenRef.value.style.transformOrigin left top } onMounted(() { updateScale() setTransform() window.addEventListener(resize, updateScale) }) onBeforeUnmount(() { window.removeEventListener(resize, updateScale) }) return { scale, screenRef } }模板部分这样写template div classscreen-container div refscreenRef classscreen-box :style{ width: 1920px, height: 1080px } !-- 大屏内容 -- /div /div /template script setup langts import { useScreenScale } from /hooks/useScreenScale const { screenRef } useScreenScale() /script style scoped .screen-container { width: 100vw; height: 100vh; overflow: hidden; display: flex; align-items: center; justify-content: center; } .screen-box { flex-shrink: 0; background: #0b1a2e; } /style这里有一个很关键的细节是transformOrigin要设置为left top也就是缩放的中心点放在容器的左上角。如果不设置transform的默认缩放中心是元素中心center center缩放后容器会产生偏移导致内容不居中。配合外层flex居中就能实现在非16:9屏幕上上下居中显示两侧留白。2.3 注意事项与扩展技巧scale方案看起来简单实际用起来有几个地方必须注意第一个坑是模糊问题。当屏幕实际尺寸大于设计稿尺寸时比如设计稿1920屏幕2560scale值会是1.33容器里的所有位图都会被放大图片和文字边缘会变虚。这个没法完全避免只能说尽量在开发时用SVG或者矢量图标位图素材的分辨率要按目标大屏中最大的尺寸来准备。我一般会让设计直接给出2倍图防止在4K屏上糊掉。第二个坑是页面里有弹窗、悬浮层或者用户交互组件的情况。scale缩放会把所有的缩放效果应用到这些组件上如果弹窗的定位是在body下而不是screen-box里弹窗不会跟着缩放就会出现弹窗位置和触发元素对不上的问题。解决方案是所有的弹窗、tooltip、下拉框都要放在screen-box容器内并且使用absolute定位而不是fixed定位。ECharts自带的tooltip因为是canvas内部绘制的不受这个影响。第三个坑是transform对position: fixed的影响。在Chrome和Safari中一旦父元素设置了transform子元素的fixed定位就不再相对于浏览器视口而是相对于这个transformed父元素。这个特性在做大屏时其实是好用的因为大屏页面本身不应该有滚动条所有的浮层都应该围绕屏幕盒子来定位。但如果你在screen-box外面还有fixed的元素那就需要单独处理。第四个经验如果需要兼容keyboard事件或鼠标滚轮需要重新计算坐标。因为整个页面被缩放了事件触发的clientX、clientY是相对于缩放后视口的而如果你要根据鼠标位置高亮某个图表数据点需要把坐标除以scale值还原到1920坐标系里。我有一次在地理位置标记类的大屏上就踩过这个坑计算偏移量时忘记除以缩放比例鼠标跟可视化标记点总是对不上。3. 方案二rem postcss-pxtorem 适配方案3.1 rem方案在大屏场景下的独特价值rem方案是移动端适配的经典做法但在大屏场景里同样适用而且有一些scale方案不具备的优势。它的核心思路是浏览器默认字号是16px如果我们在html元素上设置font-size 屏幕宽度 / 某个基准值那么所有以rem为单位的CSS属性都会随屏幕宽度变化。比如设计稿是1920我们定基准值是192那么1920宽屏下1rem等于10px1920/19210也就是font-size: 10px在3840宽的4K屏下font-size自动变成20px所有使用rem的元素宽度、字体、间距都自动放大两倍。和scale方案对比rem方案最大的优势是交互性更好。scale方案虽然整体等比缩放了但是点击区域也被缩放实际点击坐标和视觉坐标存在偏差rem方案是真实的重排不存在这个偏差。而且rem方案可以允许页面纵向滚动——很多大屏项目实际上是数据大屏管理看板混合形态内容超出了1080的高度限制scale方案就难办了rem方案完全不受影响。3.2 Vue3 Vite环境下的配置步骤如果你用Vue3 Vite开发rem方案的落地分两步。第一步写一个设置根字号的小工具// utils/rem.ts export function initRem(designWidth 1920) { const setHtmlFontSize () { const clientWidth document.documentElement.clientWidth if (clientWidth 0) return // 设计稿1920下1rem 10px所以基准值是192 document.documentElement.style.fontSize (clientWidth / 192) px } setHtmlFontSize() window.addEventListener(resize, setHtmlFontSize) }在main.ts里引入执行import { initRem } from ./utils/rem initRem()第二步配置postcss-pxtorem让CSS中的px自动转为remnpm install postcss-pxtorem -D在项目根目录创建postcss.config.jsmodule.exports { plugins: { postcss-pxtorem: { rootValue: 192, // 设计稿宽度/10 propList: [*], // 所有属性都转换 exclude: /node_modules/i } } }这样开发时所有样式仍然写px编译后会转换成rem。比如font-size: 32px会编译成font-size: 0.1667rem。页面宽度从1920变成3840时根字号翻倍这个0.1667rem对应的实际像素也随之翻倍。3.3 为什么pxtorem对ECharts不生效这个问题在相关热词里反复出现“pxtorem对echarts没起到效果”。其实原因前面已经提到了——pxtorem只对CSS样式里的px生效ECharts的配置项是JavaScript对象它最终通过Canvas绘制不走CSS渲染流程。ECharts里所有涉及尺寸的参数包括fontSize、width、height、symbolSize、itemSize等都是以数字形式写在option里的。对这些值做rem处理需要自己在代码中写一个转换函数// utils/rem.ts 增加转换方法 export const px2rem (px: number) { const clientWidth document.documentElement.clientWidth return px * (clientWidth / 1920) } // ECharts option中使用 const option { title: { text: 销售趋势, textStyle: { fontSize: px2rem(24) } }, tooltip: { textStyle: { fontSize: px2rem(14) } } }所以我通常在混合方案中这样分配凡是用CSS能解决的间距、边框、布局尺寸交给postcss-pxtorem自动转凡是写在JavaScript配置里的图表参数用一个统一的坐标换算函数手动处理。这样虽然多了点代码量但图表字体可以跟随屏幕变化总比写死强。3.4 边界情况处理rem方案有一个需要注意的边界情况当屏幕宽高比与设计稿差异较大时垂直方向的适配会失真。比如设计稿是1920x1080客户现场是1920x1200的屏幕rxRem方案只根据宽度计算根字号内容高度可能超出视口或者纵向空间利用不满。处理方式有两种一是限制大屏容器的最大高度为视口高度超出部分给容器加overflow-y: auto二是根字号的计算同时考虑宽高取视口宽度和视口高度除以设计稿宽高的较小值const setHtmlFontSize () { const scale Math.min( document.documentElement.clientWidth / 1920, document.documentElement.clientHeight / 1080 ) document.documentElement.style.fontSize 192 * scale px }这里用Math.min还是Math.max取决于具体业务。Math.min保证内容完整显示不留滚动条Math.max则保证铺满屏幕但可能出现内容被裁剪的情况。我一般用Math.min因为大屏是展示用的信息不能丢。4. 方案三vw/vh视口单位方案4.1 从px到vw/vh的思维转换vw和vh是CSS3的视口单位1vw等于视口宽度的1%1vh等于视口高度的1%。如果设计稿宽度是1920那么1vw等于19.2px要把一个40px的间距换算成vw就是40 / 19.2 2.0833vw。这个方案最大的优势是纯CSS实现不需要JavaScript参与不需要监听resize事件不需要额外的转换插件。页面渲染时浏览器自动按视口尺寸计算最终像素值性能开销最低。在Vue3项目中我推荐的做法是写一个scss函数把vw换算封装起来// styles/vw.scss $designWidth: 1920; $designHeight: 1080; function vw($px) { return ($px / $designWidth) * 100vw; } function vh($px) { return ($px / $designHeight) * 100vh; }使用的时候.title { font-size: vw(28); // 相当于 1.4583vw margin-top: vh(16); // 相当于 1.4815vh }4.2 解决vw单边适配的问题单纯的vw方案有个先天缺陷宽度适配了高度不一定适配。一个1920x1080的设计稿在3840x1080的超宽屏上宽度翻倍但高度不变那么所有使用vw的元素都放大了高度就容易溢出。我实际开发中用的是一套宽度和高度分离的策略布局结构、宽度类属性width、padding-left、padding-right、font-size用vw。高度类属性height、margin-top、margin-bottom、line-height用vh。这样当屏幕比例变化时元素的宽高会按照各自方向的比例伸缩不会出现整体被拉变形的问题。还有一个更省事的做法是直接用CSS的min()函数做组合值.box { width: min(calc(1920px * 100vw / 1920), 100%); }不过这样写可读性较差生产环境代码不易维护。我建议用scss函数封装好之后团队按统一规范来写。4.3 与ECharts配合的尺寸获取vw方案在处理ECharts时也有自己的方式。通常我们会让图表容器用vw/vh设置宽高这样图表容器本身会跟随屏幕变化。但是ECharts在初始化时会读取容器尺寸来创建Canvas画布如果容器尺寸变化了图表内容不会自动跟着变需要调用resize方法。Vue3中比较稳妥的做法是用ResizeObserver监听容器尺寸变化import * as echarts from echarts import { onMounted, onBeforeUnmount, ref } from vue const chartRef refHTMLDivElement() let chart: echarts.ECharts | null null let observer: ResizeObserver | null null onMounted(() { chart echarts.init(chartRef.value) observer new ResizeObserver(() { chart?.resize() }) observer.observe(chartRef.value!) }) onBeforeUnmount(() { observer?.disconnect() chart?.dispose() })这里用ResizeObserver而不是window的resize事件好处是如果大屏页面里有可折叠的侧边栏或者可拖拽调整宽度的面板图表也能跟着容器尺寸变化而不是傻等浏览器窗口变化。4.4 字体到底是vw还是rem很多同学纠结一个点字体大小用vw还是rem好。我的实践结论是在纯展示的大屏项目中字体用vw更直接。因为大屏项目的所有元素比例都是统一的字体跟随视口宽度缩放效果和scale方案几乎一致。但字体有一个特殊问题就是中文字体在缩放后可能会出现笔画重叠或者发虚的现象尤其是font-weight较大的标题文字。如果遇到这种情况可以把标题改用SVG文字或者图片或者使用letter-spacing微调。我踩过的一个典型坑是大屏上一个24px的宋体标题scale放大1.5倍到36px后字符间距显得特别拥挤最后是通过给文字加letter-spacing: 2px才解决。5. 方案四媒体查询 Flex栅格布局过渡方案5.1 什么时候才需要媒体查询媒体查询这种方案在大屏项目中往往被误解有很多人一上来就问“大屏自适应是不是就是写一堆media”。其实不是。大屏的数据可视化页面设计稿是固定的几乎所有元素都是等比例关系用媒体查询去一个个断点调样式工作量大且效果差。媒体查询真正适合的场景是大屏和后台管理功能混在一个项目里比如一个页面同时包含可视化图表区域和可操作的表格、表单。这种情况下页面不仅要在不同屏幕上“看得见”还要“能用”。查询条件是分辨率变化、浏览器窗口缩放、甚至是打印。还有一类场景是大屏本身包含明显不同密度级的布局比如1366px以下用单列布局1366到1920用双列1920以上用三列。这种属于结构性变化媒体查询配合flex栅格是合理的。5.2 在Vue3中组织媒体查询的策略媒体查询的写法和Vue2没有本质区别但在Vue3项目中我建议把所有断点变量化管理写在scss的变量文件里避免到处魔法数字// styles/breakpoints.scss $screen-md: 1366px; $screen-lg: 1920px; $screen-xl: 2560px; mixin respond-to($breakpoint) { if $breakpoint md { media (min-width: $screen-md) and (max-width: $screen-lg - 1) { content; } } else if $breakpoint lg { media (min-width: $screen-lg) and (max-width: $screen-xl - 1) { content; } } else if $breakpoint xl { media (min-width: $screen-xl) { content; } } }在具体组件中这样用.data-card { display: flex; __item { flex: 1; include respond-to(md) { flex-basis: 50%; } include respond-to(xl) { flex-basis: 25%; } } }5.3 防抖与resize事件的最优写法不管用哪种方案只要有JavaScript参与尺寸计算就离不开resize事件监听。在大屏项目中这个监听还会引发一些性能问题。Vue3中常见的写法是直接在setup里window.addEventListener(resize, handler)。但这有个问题如果handler里做了昂贵的操作比如重新计算scale、重新初始化图表频繁触发会导致掉帧。我建议用requestAnimationFrame做节流import { onMounted, onBeforeUnmount, ref } from vue const scale ref(1) let rafId 0 const handleResize () { cancelAnimationFrame(rafId) rafId requestAnimationFrame(() { const windowWidth document.documentElement.clientWidth scale.value windowWidth / 1920 }) } onMounted(() { window.addEventListener(resize, handleResize) }) onBeforeUnmount(() { window.removeEventListener(resize, handleResize) cancelAnimationFrame(rafId) })requestAnimationFrame会把同一帧内的多次resize回调合并成一次计算配合cancelAnimationFrame可以避免连续触发时无效计算。还有一个经验不要在Vue的watch里直接执行ECharts的resize。因为ECharts的resize是同步操作会触发Canvas重绘如果watch回调里还有其他状态更新容易造成重复渲染。比较稳妥的做法是把resize封装成一个方法手动调用。6. 图表类组件的专项适配6.1 ECharts初始化时机与容器尺寸大屏项目里图表是最核心的内容也是适配最容易出问题的部分。除了前面说的字体问题还有几个专门针对ECharts的坑。第一个坑是初始化时机不对导致图表尺寸为0。当图表所在的容器使用v-if控制显示或者容器宽高是通过异步请求的数据计算出来的ECharts可能在容器还没有真实宽高的时候就init了结果整个图表渲染不出来或者只渲染了左上角的一小块。排查这个问题的标志是console里报“Cant get DOM width or height”。解决方案是在容器挂载并且真实尺寸确定后再初始化图表。如果图表依赖异步数据可以用v-if确保数据加载后再渲染容器或者在nextTick之后执行init。第二个坑是图表容器不设宽高。很多同学给图表容器写CSS时没有显式设置width和height而是依赖父级flex撑开。这在普通页面可能没问题但ECharts要求容器有具体的像素尺寸。大屏中一定要显式设置图表容器和宽的宽高或者设置flex: 1加上min-width: 0这样的约束否则Chrome的flex布局会把图表容器压缩成0宽。6.2 图表尺寸联动屏幕的方案封装针对大屏图表我写了一个相对完整的useEchartsScreen组合式函数同时处理了初始化、尺寸获取、自适应缩放和销毁// hooks/useChart.ts import * as echarts from echarts import { onMounted, onBeforeUnmount, ref, type Ref } from vue export function useChart(elRef: RefHTMLElement | undefined, options: Recordstring, any) { let chart: echarts.ECharts | null null let resizeObserver: ResizeObserver | null null const setOption (opt: Recordstring, any) { chart?.setOption(opt) } const resize () { chart?.resize() } onMounted(() { if (!elRef.value) return chart echarts.init(elRef.value) chart.setOption(options) resizeObserver new ResizeObserver(() { chart?.resize() }) resizeObserver.observe(elRef.value) }) onBeforeUnmount(() { resizeObserver?.disconnect() chart?.dispose() chart null }) return { setOption, resize } }用的时候在组件里把容器ref传进去template div refchartRef classchart/div /template script setup langts import { ref } from vue import { useChart } from /hooks/useChart const chartRef refHTMLElement() const { setOption } useChart(chartRef, { xAxis: { type: category, data: [] }, yAxis: { type: value }, series: [{ type: line, data: [] }] }) // 拿到数据后 const update (data: number[]) { setOption({ xAxis: { data: data.map((_, i) 第${i 1}月) }, series: [{ data }] }) } /script style scoped .chart { width: 100%; height: 100%; min-width: 0; /* 防止flex压缩 */ } /style6.3 大屏中Canvas多实例的性能优化大屏项目一多就会遇到性能问题。一个页面上可能有十几个图表实例每个ECharts实例都是一个独立的Canvas上下文内存和GPU开销非常大。再加上自适应的事件监听和重绘低配电脑上经常出现页面卡顿、风扇狂转的情况。三个优化经验第一能用line图尽量不用散点图或大数据量的柱状图。ECharts在数据量超过几千条时canvas的渲染性能会急剧下降。如果必须展示大量数据可以采用降采样或者聚合的方式比如把一分钟的数据聚合成一个点。第二关闭不需要的动画。大屏默认是所有动画都会播放但多个图表同时动画时会导致窗口卡顿。初始化时设置animation: false或者把effect.animation配置项关闭视觉效果影响不大性能提升明显。第三合理使用dispose优化隐藏页签。如果大屏有tab切换不要在v-if切换时反复create和dispose图表实例最好用v-show保留DOM节点切换时只更新数据。反复创建销毁Canvas的开销远大于保留常驻实例。7. 常见问题与排查技巧实践7.1 高频问题速查表把我在Vue3大屏项目中实际遇到过的高频问题整理成了表格供遇到类似情况时快速排查。问题现象根本原因解决方案大屏在4K屏上整体偏小或偏大scale或rem的基准值没匹配目标分辨率检查designWidth是否与设计稿一致必要时以最大目标屏为基准ECharts图表字体不随屏幕变化option里的fontSize是固定px不经过CSS转换用封装函数按屏幕宽度换算字体图表监听容器resize页面出现双滚动条scale方案中容器高度超过视口外层overflow没有隐藏外层容器设置overflow: hiddeninner容器宽高使用100vw/100vh图表第一次加载不出数据v-if渲染的容器还没挂载完就初始化了图表使用nextTick等待DOM就绪或图表绑定ref后延迟初始化弹窗位置偏移弹窗使用fixed定位受祖先元素transform影响导致定位基准变化弹窗放进scale容器内使用absolute定位坐标同时除以缩放比例文字模糊有锯齿位图被scale放大分辨率不足字体抗锯齿性能问题用矢量图代替位图按最大屏幕准备2倍图素材文字避免缩放后小字号多个图表resize时闪烁每个图表独立监听window resize触发顺序不一致导致重绘错乱统一使用ResizeObserver监听容器批量raf节流后再重绘7.2 一个真实项目的选型复盘最后分享一个我最近做的实际项目案例。这是个城市交通运行监测大屏客户现场的屏幕是五块46寸液晶拼接屏总分辨率约5760x1080但由于拼接缝隙和显卡限制浏览器实际可视区域大约是5760x1080而设计稿是按照1920x1080给的一屏内容。这种情况下用scale方案最合适。我在项目里固定设计稿尺寸1920x1080计算scale 5760/1920 3。所有图表、组件都按1920设计稿布局整体放大三倍后拼接屏效果刚好铺满。开发过程中没有为5760分辨率写任何特殊样式甚至本地用1920的窗口F12模拟也可以直接调试。这个项目踩到的两个特殊问题值得记录下来第一个是地图瓦片分辨率。项目用Mapbox GL加载城市路网在scale3的容器里地图上的文字和道路标签被放大后会变模糊。这里的正确做法不是提高Canvas分辨率而是使用mapbox提供的setPixelRatio方法提高地图render的devicePixelRatio。这个操作与容器缩放是独立的需要单独适配。第二个是多屏拼接的物理边框补偿。拼接屏的每块屏之间有几厘米的物理边框在浏览器中映射为一部分不可见的“裁切区域”。如果设计稿的重要内容正好落在拼接缝上会被挡住。处理方式是让设计在左右边缘预留安全边距同时调整容器尺寸让内容整体往中间收缩5%左右。这类问题在单屏开发时完全无法预知必须到现场联调。7.3 我个人的选型建议做了这么多大屏项目我的习惯是如果页面内容不能超过一屏优先用scale方案开发效率最高还原度最好如果页面内容可能超出一屏优先用rem方案允许滚动且交互准确如果项目是纯展示没有任何用户操作且开发团队对CSS比较熟vw/vh方案也可以但图表部分仍然需要单独做resize处理。千万别试图用一套方案解决所有问题。大屏自适应没有一个银弹正确的方式是掌握多种方案根据项目实际情况灵活组合。最重要的是在设计稿阶段就明确目标屏幕的分辨率和比例以及是否需要交互、是否允许滚动这些决策越早做后面踩的坑越少。如果你正在做一个Vue3的大屏项目建议从方案一scale开始先把整体效果铺出来再逐步加图表和交互细节。遇到具体的尺寸适配问题再回头看这篇文章里的表格大概率能帮你省下半天排查时间。
返回列表