博主介绍
👨💻 了解博主:波仔椿
📖 人生箴言:组件拆得够小,页面才装得下变化。
🧰 我的专栏:Vue3-实战派
文章内容
上个月接了个数据大屏的活,设计稿是 1920×1080,客户现场的屏幕却是 2560×1440 的带鱼屏,一打开页面元素全挤在左上角,右边一大片空白。改了一晚上适配才搞定。今天把 Vue3 项目里屏幕适配的那些坑和方案一次性拆给你看,从移动端到大屏全覆盖。
一、屏幕适配到底在解决什么问题
很多人一上来就问"用 rem 还是 vw",但连适配要解决什么都没搞清楚。先把问题盘清楚,方案选型才不会瞎。
1. CSS 像素和物理像素不是一回事
你在 CSS 里写width: 375px,在 iPhone 12 上实际渲染的物理像素是 1170px——因为它的设备像素比(DPR)是 3。说白了,CSS 像素是逻辑单位,物理像素才是屏幕上真正发光的点,DPR 就是两者之间的换算系数。
这就是为什么同样写1px的边框,在 Retina 屏上看起来比设计稿粗——CSS 的 1px 对应了 2 个甚至 3 个物理像素。
你发现没有:适配的本质不是"让 px 变小",而是让"逻辑尺寸"和"物理屏幕"之间建立一套可伸缩的映射关系。
2. 那些年踩过的适配坑
我把项目里最容易翻车的点列了张表,你可以对照排查:
| 问题 | 表现 | 根因 |
|---|---|---|
| 1px 边框变粗 | Retina 屏上线条发虚、偏粗 | CSS 1px 对应多个物理像素 |
| 刘海屏内容被挡 | 顶部按钮被灵动岛遮住 | 未处理安全区域(safe-area) |
| 图片模糊 | 高清屏上图片发虚 | 用了 1 倍图,没按 DPR 提供 2x/3x 图 |
| 横竖屏错乱 | 旋转后布局崩坏 | 未监听orientationchange |
| 字体忽大忽小 | 部分机型字体不跟随缩放 | iOS 文本自动调整(-webkit-text-size-adjust) |
| 行内样式没被转换 | 用了 postcss 插件但部分 px 没变 | 行内 style 不经过 PostCSS 处理 |
| 大屏元素错位 | 非标准分辨率下布局散架 | 固定 px 布局没有缩放层 |
这些问题不是某个方案能全部解决的,得组合着来。下面逐个方案讲清楚各自的适用场景。
二、方案一:rem + 动态根字号(经典移动端方案)
rem 的原理很简单:1rem等于根元素<html>的font-size。只要根据屏幕宽度动态改变根字号,所有用 rem 写的尺寸就会跟着等比缩放。
1. 用 Vue3 组合式函数实现 rem 适配
不用再引lib-flexible那种老库了,Vue3 里一个 composable 就能搞定:
// src/composables/useRem.jsimport{onMounted,onUnmounted}from'vue'/** * 移动端 rem 适配 * @param {number} designWidth 设计稿宽度,默认 375 * @param {number} maxWidth 最大限制宽度,防止 PC 端字号过大 */exportfunctionuseRem(designWidth=375,maxWidth=750){functionsetRem(){constclientWidth=document.documentElement.clientWidth||window.innerWidth// 限制最大宽度,PC 端打开不至于字大到离谱constwidth=Math.min(clientWidth,maxWidth)// 把设计稿分成 10 份,1rem = 设计稿宽度/10// 比如 375 设计稿 → 1rem = 37.5px,元素 75px 就写 2remconstfontSize=(width/designWidth)*(designWidth/10)document.documentElement.style.fontSize=fontSize+'px'}onMounted(()=>{setRem()window.addEventListener('resize',setRem)window.addEventListener('orientationchange',setRem)})onUnmounted(()=>{window.removeEventListener('resize',setRem)window.removeEventListener('orientationchange',setRem)})}在App.vue里调用一次就行:
<!-- src/App.vue --><scriptsetup>import{useRem}from'./composables/useRem'// 设计稿 375,PC 端最大限制到 750useRem(375,750)</script><template><divclass="app"><router-view/></div></template>2. 配合 postcss-pxtorem 自动换算
手写 rem 太费脑子,让 PostCSS 帮你把 px 自动转成 rem:
npminstallpostcss-pxtorem-D// postcss.config.jsexportdefault{plugins:{'postcss-pxtorem':{rootValue:37.5,// 设计稿宽度 / 10,和 useRem 里的基准保持一致propList:['*'],// 所有属性都转换selectorBlackList:['.ignore-px'],// 带这个类名的不转换minPixelValue:2,// 小于 2px 的不转,1px 边框保留原样mediaQuery:false// 媒体查询里的 px 不转换}}}这样你在样式里照常写width: 100px,构建后自动变成width: 2.66667rem。
说白了:rem 方案的核心就两步——JS 动态改根字号 + PostCSS 自动转单位。少了哪一步都得手算。
3. rem 方案的局限
- 依赖 JS:根字号要靠 JS 算,首屏如果 JS 加载慢,会有一瞬间字号不对导致布局闪烁
- 字体问题:rem 是等比缩放,但有些场景字体不希望跟着缩(比如正文可读性),得单独处理
- 大屏不适用:rem 是按宽度缩放的,大屏项目高度方向容易出问题
三、方案二:vw/vh + postcss-px-to-viewport(现代主流方案)
vw 是视口宽度的百分之一,100vw就是整个屏幕宽。它是纯 CSS 单位,不依赖 JS,浏览器原生支持,现在已经是移动端适配的首选。
1. Vite 中的配置
Vue3 + Vite 项目里直接在vite.config.js配置 PostCSS 插件:
npminstallpostcss-px-to-viewport-D// vite.config.jsimport{defineConfig}from'vite'importvuefrom'@vitejs/plugin-vue'importpostcssPxToViewportfrom'postcss-px-to-viewport'exportdefaultdefineConfig({plugins:[vue()],css:{postcss:{plugins:[postcssPxToViewport({unitToConvert:'px',// 要转换的单位viewportWidth:375,// 设计稿宽度unitPrecision:5,// 转换后保留的小数位propList:['*'],// 所有属性都转viewportUnit:'vw',// 转换成 vwfontViewportUnit:'vw',// 字体也转 vwselectorBlackList:['.ignore'],// 忽略的选择器minPixelValue:1,// 最小转换值mediaQuery:false,// 媒体查询不转换replace:true,// 直接替换而不是添加备用exclude:[/node_modules/]// 排除第三方库})]}}})配完之后,你写的width: 100px会被编译成width: 26.66667vw(100 / 375 × 100)。
2. 移动端和 PC 端双设计稿切换
如果项目同时要适配移动端(375)和 PC 端(1920),可以通过环境变量切换viewportWidth:
// vite.config.js(节选)postcssPxToViewport({viewportWidth:process.env.MODE==='mobile'?375:1920,// ...其他配置同上})3. vw 方案要注意的坑
坑一:行内样式不会被转换。PostCSS 只处理<style>里的样式,模板里的:style="{ width: '100px' }"不会被转。解决办法是把需要适配的尺寸写到 CSS 类里,或者用 CSS 变量做桥接:
<!-- src/components/Card.vue --><scriptsetup>// 动态尺寸用 CSS 变量传递,样式里用 calc 配合 vwconstcardWidth='26.667vw'// 等价于 100px @ 375 设计稿</script><template><divclass="card":style="{ '--card-w': cardWidth }">卡片内容</div></template><stylescoped>.card{width:var(--card-w);}</style>坑二:1px 边框不要转 vw。1px转成0.267vw在某些屏幕上可能渲染不出来。用minPixelValue: 1排除掉,或者给边框加.ignore类名。
坑三:100vw在移动端包含滚动条宽度,会导致横向溢出。用100%代替100vw做容器宽度,或者设置overflow-x: hidden。
有意思的地方就在这儿:vw 看起来比 rem 简单,但真正的坑都在"哪些 px 不该转"上——边框、行内样式、第三方组件,这些都得单独拎出来处理。
四、方案三:transform: scale 整体缩放(大屏 / 固定设计稿首选)
数据大屏、活动页这种设计稿固定(通常 1920×1080)、不希望布局被打散的场景,最稳的方案是把整个页面用transform: scale()包一层缩放。
1. Vue3 组合式函数实现等比缩放
// src/composables/useScaleScreen.jsimport{ref,onMounted,onUnmounted}from'vue'/** * 大屏等比缩放适配 * @param {number} designWidth 设计稿宽度,默认 1920 * @param {number} designHeight 设计稿高度,默认 1080 */exportfunctionuseScaleScreen(designWidth=1920,designHeight=1080){consttransform=ref('scale(1) translate(-50%, -50%)')functionupdateScale(){constscaleX=window.innerWidth/designWidthconstscaleY=window.innerHeight/designHeight// 取较小值,保证完整显示,不留白也不裁切constscale=Math.min(scaleX,scaleY)transform.value=`translate(-50%, -50%) scale(${scale})`}onMounted(()=>{updateScale()window.addEventListener('resize',updateScale)})onUnmounted(()=>{window.removeEventListener('resize',updateScale)})return{transform,designWidth,designHeight}}2. 在大屏页面中使用
<!-- src/views/Screen.vue --><scriptsetup>import{useScaleScreen}from'../composables/useScaleScreen'const{transform,designWidth,designHeight}=useScaleScreen(1920,1080)</script><template><!-- 外层占满整个屏幕,作为缩放的舞台 --><divclass="screen-stage"><!-- 内层固定设计稿尺寸,通过 transform 缩放居中 --><divclass="screen-content":style="{ width: designWidth + 'px', height: designHeight + 'px', transform }"><!-- 这里面所有元素都按 1920×1080 写死 px 就行 --><headerclass="screen-header">数据可视化大屏</header><mainclass="screen-main"><sectionclass="panel">面板一</section><sectionclass="panel">面板二</section></main></div></div></template><stylescoped>.screen-stage{width:100vw;height:100vh;overflow:hidden;background:#0a1929;}.screen-content{position:absolute;top:50%;left:50%;transform-origin:center center;}</style>3. scale 方案的坑和解法
坑一:ECharts 图表不跟着 resize。缩放后图表容器的 CSS 尺寸没变(还是 1920×1080),ECharts 内部不知道要重绘。需要在缩放后手动调用chart.resize(),或者监听window.resize事件统一触发:
// src/composables/useEchartsResize.js(节选)import{onMounted,onUnmounted}from'vue'exportfunctionuseEchartsResize(chartRef){functionresize(){chartRef.value&&chartRef.value.resize()}onMounted(()=>window.addEventListener('resize',resize))onUnmounted(()=>window.removeEventListener('resize',resize))}坑二:canvas 内容缩放后模糊。transform: scale缩放的是位图,canvas 画出来的东西放大后会发虚。解法是给 canvas 容器做反缩放补偿——外层 scale 放大了多少,canvas 内部就按 DPR 多绘制多少倍。
坑三:缩放后点击事件坐标偏移。如果有拖拽、绘制等交互,event.clientX是屏幕坐标,需要除以缩放比才能换算成设计稿坐标。
说白了:scale 方案最适合"设计稿钉死、不许乱"的大屏项目,但凡是有 canvas 图表、鼠标交互的地方,都得额外做一层坐标换算。
五、四种方案横向对比,到底该选哪个
| 维度 | rem + flexible | vw/vh | transform: scale | 媒体查询响应式 |
|---|---|---|---|---|
| 依赖 JS | 是(改根字号) | 否 | 是(算缩放比) | 否 |
| 适用场景 | 移动端 H5 | 移动端 H5 / 通用 | 大屏 / 活动页 | 官网 / 后台 |
| 等比缩放 | 宽度方向 | 宽高独立 | 整体等比 | 断点切换 |
| 1px 边框 | 需单独处理 | 需单独处理 | 天然解决 | 天然解决 |
| 首屏闪烁 | 可能(JS 慢时) | 无 | 可能(JS 慢时) | 无 |
| 第三方组件兼容 | 需排除 | 需排除 | 无需处理 | 无需处理 |
| 学习成本 | 低 | 低 | 中 | 中 |
我的选型建议:
- 普通移动端 H5:直接上 vw +
postcss-px-to-viewport,这是目前最省心的方案 - 需要兼容老版本安卓 WebView:rem 方案更稳,vw 在极低版本上有兼容问题
- 数据大屏 / 固定设计稿活动页:
transform: scale一把梭,内部全写 px - 企业官网 / 管理后台:媒体查询 + Flex/Grid 响应式布局,不要强行等比缩放
六、Vue3 项目中的工程化落地技巧
1. 全局 1px 边框方案
不管用 rem 还是 vw,1px 边框都得单独处理。写一个全局样式类,用伪元素 + transform 缩放实现真正的物理 1px:
/* src/styles/border.css *//* 下边框 1px */.border-bottom-1px{position:relative;}.border-bottom-1px::after{content:'';position:absolute;left:0;bottom:0;width:100%;height:1px;background:#e5e5e5;transform-origin:0 100%;}/* DPR = 2 时缩到 0.5px */@media(-webkit-min-device-pixel-ratio:2){.border-bottom-1px::after{transform:scaleY(0.5);}}/* DPR = 3 时缩到 0.333px */@media(-webkit-min-device-pixel-ratio:3){.border-bottom-1px::after{transform:scaleY(0.333);}}2. 刘海屏安全区域适配
iOS 刘海屏、灵动岛、底部 Home 条会遮挡内容,用env()函数处理:
<!-- index.html --><metaname="viewport"content="width=device-width, initial-scale=1.0, viewport-fit=cover">/* src/styles/global.css */.safe-area-top{padding-top:env(safe-area-inset-top);}.safe-area-bottom{padding-bottom:env(safe-area-inset-bottom);}/* 兼容 iOS 11 以下的 constant() */.safe-area-bottom{padding-bottom:constant(safe-area-inset-bottom);padding-bottom:env(safe-area-inset-bottom);}你想想:
viewport-fit=cover是让页面延伸到刘海区域的开关,不开这个,env()拿到的值全是 0,安全区域适配等于白做。
3. 禁止 iOS 字体自动缩放
iOS Safari 会在横屏时自动放大字体,加一行全局样式关掉:
/* src/styles/global.css */html{-webkit-text-size-adjust:100%;text-size-adjust:100%;}4. 高清屏图片适配
用srcset按 DPR 提供不同分辨率的图片,避免高清屏上图片模糊:
<imgsrc="/images/logo@1x.png"srcset="/images/logo@2x.png 2x, /images/logo@3x.png 3x"alt="Logo"/>或者用 CSS 的image-set():
.logo{background-image:image-set(url('/images/logo@1x.png')1x,url('/images/logo@2x.png')2x);}七、总结:适配不是选一个方案就完事
屏幕适配这件事,没有银弹。真实项目里往往是vw 做主体缩放 + 1px 边框单独处理 + 安全区域全局样式 + 媒体查询补断点的组合拳。
记住三个原则:
- 先搞清楚设计稿基准(375 还是 750?1920×1080 还是其他?),所有换算都围绕它来
- 哪些 px 不该转比"怎么转"更重要——边框、行内样式、第三方组件都得排除
- 上线前多拿几台真机测,尤其是刘海屏、折叠屏、高 DPR 设备,模拟器测不出来的问题太多了
最后说一句:适配的终极目标不是"在所有屏幕上一模一样",而是"在所有屏幕上都好用、都不难看"。该等比的等比,该自适应的自适应,别一根筋。
精彩推荐
🔎Vue3全屏实战:一键搞定数据大屏
🔎Vue3深色模式实现方案:一键切换零白屏
🔎Vue3组件封装:从混乱到高复用的实战指南
🔎Vue3国际化实战:vue-i18n避坑指南
🔎前端AI编辑器实现:一键润色与智能写作
本篇博客文章唯一版权归属©波仔椿