拓冰建站拓冰建站
首页 / 资讯中心 / 正文

uniapp多端样式适配实战:rpx、条件编译与穿透避坑指南

做了这么多年 uniapp 项目我一直觉得“样式”这件事是最能体现细节功力的地方。功能逻辑写得再顺样式一端崩溃、一端变样用户第一时间就会觉得产品不靠谱。uniapp 的样式体系本质上还是 CSS但因为它要同时跑小程序、H5、App 三端很多在普通网页里随手写的样式到了小程序里就是另一副面孔。rpx、条件编译、组件穿透、安全区这些名词基本是每个 uniapp 开发者绕不过去的坎。这篇文章我不打算科普 CSS 基础而是结合我实际开发中反复踩过的坑把 uniapp 样式相关的选型思路、常用方案、平台差异和排查方法都过一遍适合正在做多端项目、或者刚准备把 vue 项目迁到 uniapp 的朋友。你把它当一份经验备忘也好当排查手册也行至少能帮你少走几次弯路。1. 先聊清楚uniapp 的“样式”到底难在哪1.1 一套代码三端跑的底层规则很多人第一次写 uniapp 的时候会下意识把它理解成“写一套 CSS 然后自动适配三端”这个想法不能算错但忽略了最关键的一点三端的样式渲染引擎并不是同一个东西。编译到微信小程序时你的 style 标签和外部样式文件会被转换成 wxss由小程序自身的样式系统去解释编译到 H5 时走的是标准浏览器 CSS编译到 App 端时又要再分两种情况——vue 页面走 webview 渲染和 H5 行为基本一致但 nvue 页面走的是原生渲染样式支持能力和 CSS 差别很大很多属性会失效。这意味着你写样式的时候脑子里始终要有“平台优先级”的概念。比如小程序端对*通配符选择器的支持就很差性能也会有明显损耗在 H5 上能用得很欢的body选择器在小程序里根本不存在得换成page。再比如小程序自定义组件里的样式隔离规则比 H5 更严格你想从页面外侧去改一个第三方组件内部的 class大概率改不动必须借助穿透选择器。这套规则不搞清楚后面排查起问题来会非常痛苦。1.2 设计稿怎么换算成 rpxrpx 是 uniapp 里最核心的样式单位它的设计逻辑很简单屏幕总宽度永远是 750rpx。也就是说屏幕越宽1rpx 对应的物理像素就越多这样同一套尺寸就能在不同设备上等比缩放。最常用的换算规则就两条设计稿宽度是 750px量出来是多少像素直接写多少 rpx不需要换算。设计稿宽度是 375px所有尺寸乘以 2比如设计稿上一个宽度为 100px 的按钮代码里写 200rpx。举个例子设计稿给了一个 120px 宽的图片在 375 宽设计稿场景下你写width: 240rpx在 iPhone 的 375px 屏上实际渲染 120px在宽度 414px 的机型上会自动放大到约 132px。这个等比缩放的效果是普通 px 给不了的。我自己习惯做项目的时候会先用表格把几个常用单位的关系理清楚避免团队里各写各的单位特点适用场景rpx750rpx 等于屏宽自动缩放uniapp 跨端项目的首选px固定物理像素不缩放1px 边框、固定宽高小元素rem相对根节点字号H5 端做整体缩放vw/vh相对视口宽高H5 端布局、全屏弹层补充一个场景如果你只做 H5rpx 也可以直接用uniapp 编译时会自动转成视口单位。但要注意在 iPad 这类超宽屏幕上rpx 会把元素放得很大遇到这种情况可以在 manifest.json 的 H5 配置里调整rpxCalcMaxDeviceWidth限制 rpx 计算的最大基准宽度否则整个页面会显得非常“傻大”。2. 项目里最常用的样式组织方案2.1 全局样式、uni.scss 与 App.vue 的分工先说一个最容易混淆的点uni.scss并不是普通样式文件。它是由编译器注入到每个 scss 文件里的变量文件里面适合放$uni-color-primary、$base-font-size这类主题变量所有页面和组件的style langscss都能直接引用不需要手动 import。但千万别往里面写选择器因为它会被重复注入到每一个 scss 编译单元里写多了代码体积和编译时间都会上涨。那全局样式应该放哪答案是App.vue的style标签里。这个 style 不带 scoped编译后是真正的全局样式适合放 reset 和通用类/* App.vue */ style langscss page { background-color: #f7f8fa; color: #333; font-size: 28rpx; line-height: 1.6; } /styleH5 端没有page选择器如果需要给 H5 单独设置 html、body 的基础样式建议用条件编译分隔开不要直接写在普通 CSS 里不然小程序端解析body会直接报错或干脆不生效。还有一类“样式”其实轮不到 CSS 管那就是导航栏、tabBar 的背景色和文字颜色。这些在pages.json里通过globalStyle、tabBar配置就能搞定。尤其是微信小程序端原生导航栏的渲染性能和体验都比自定义导航栏好能用配置解决的事情尽量不要动用 CSS。2.2 条件编译平台差异化样式的正经写法uniapp 的条件编译是我用得最多的一个功能它允许你在代码里标注“这段代码只在哪些平台保留”。样式里同样支持写法是注释形式/* #ifdef H5 */ .hover-btn { cursor: pointer; transition: all 0.2s; } /* #endif */ /* #ifndef MP-WEIXIN */ .line { border-left: 1px solid #eee; } /* #endif */#ifdef表示“仅在这些平台编译”#ifndef表示“除了这些平台都要编译”。常用的平台标识有H5、MP-WEIXIN、MP-ALIPAY、APP-PLUS、APP-VUE、APP-NVUE等。典型场景是“小手样式”。网页端用户习惯了鼠标移上去变手型但小程序和 App 端根本不存在鼠标写了也是白写所以我会用#ifdef H5把cursor: pointer单独圈起来。小程序端要做点击反馈应该用hover-class或者自己在 js 里控制 class 切换效果才自然。不过条件编译也是一把双刃剑平台分支写多了代码会非常难维护。我的原则是先想办法用同一套样式解决问题只有实在绕不开差异的时候才开分支并且每个分支都要写清楚注释说明“为什么这里必须单独处理”。2.3 UI 库选了谁样式覆盖怎么做uniapp 生态里 UI 库挺多我接触过的有官方 uni-ui、uView 2.0、uv-ui、还有 tmui 等。uni-ui 胜在官方维护、包体小但组件数量少部分组件样式比较基础uView 2.0 组件全、上手舒服但它底层还是 Vue2Vue3 项目里要用得换 uv-ui。选库这件事没有绝对答案但有一个建议团队如果同时有 Vue2 和 Vue3 的老项目优先选两套都能覆盖的库能少踩很多“这个组件在另一个版本里不存在”的坑。组件库买回来的第一件事是学会改它的样式。我的经验是覆盖顺序要按照下面这样来先看组件文档有没有暴露customStyle、styles这类 props能用官方入口改的绝不硬来。没有入口再用 scoped 穿透选择器去改内部类名。最后才考虑加!important而且要用注释说明为什么非加不可。以 Vue3 项目为例在一个带 scoped 的组件里改 uview 的 toast 样式可以这样写style langscss scoped .customer-wrap { :deep(.u-toast) { border-radius: 16rpx; background: rgba(0, 0, 0, 0.8); } } /styleVue2 里对应的写法是::v-deep或/deep/。经常有人问为什么自己在 Vue3 项目里写::v-deep没效果因为 Vue3 已经明确弃用它统一用:deep()了。如果代码在 vue2 转 vue3 的过程中报样式相关警告最常见的也是这个选择器语法问题。3. 高频组件样式改造实录3.1 dialog / 弹出层穿透、动画与滚动锁定“dialog 样式”这个词在搜索里出现频率很高我估计大家都是被同一个问题逼来的第三方弹窗组件内部样式改不动。不管是uni-popup还是 uView 的u-dialog它们内部结构往往嵌套很多层你直接给组件传 class 是没用的必须配合穿透选择器。如果你用uni-popup做对话框更推荐的做法是只用它来提供遮罩层和显示隐藏逻辑内容区域完全自己写uni-popup refdialog typedialog view classdialog-box text classdialog-title提示/text text classdialog-content确定要删除这条记录吗/text view classdialog-footer view classbtn cancel clickcloseDialog取消/view view classbtn confirm clickconfirmDelete确认/view /view /view /uni-popup.dialog-box是自己写的宽度、圆角、阴影想怎么定都行.dialog-box { width: 600rpx; border-radius: 24rpx; background: #ffffff; box-shadow: 0 8rpx 40rpx rgba(0, 0, 0, 0.12); padding: 40rpx 32rpx; }弹窗还有一个比样式更隐蔽的问题——滚动穿透。就是弹窗打开后手指在遮罩层上滑动底下的页面也跟着滚。解决思路有两个方向一是给遮罩和内容区域都加上touchmove.stop.prevent阻止事件冒泡二是在打开弹窗时记录当前滚动位置、给页面加overflow: hidden关闭后再恢复。对于小程序端touchmove.stop.prevent效果比较直接我用得最多。3.2 tabbar 导航栏角标、红点与自定义图标tabBar 的样式分两层配置层和代码层。配置层在pages.json的tabBar节点里可以设置选中色、未选中色、背景色以及每个 tab 的图标路径。如果只是换换颜色、换换图标到这里就结束了。但很多时候产品要的是“消息数量角标”或者“红点提示”这种动态内容就得用 API// 设置角标 uni.setTabBarBadge({ index: 1, text: 5 }) // 设置红点 uni.setTabBarRedDot({ index: 2 }) // 清除角标 uni.removeTabBarBadge({ index: 1 })角标样式是原生渲染的在微信小程序端你用 CSS 改不了它的大小和颜色只能控制它出不出、显示什么文本。真要完全自定义角标位置和样式就得把 tabBar 的custom配置项打开自己实现整个 tabBar。这个方案可以做出中间凸起按钮、渐变背景、动态角标等所有效果但代价是要自己处理选中态、路由切换、安全区高度工作量不小。顺带说一个监听技巧页面生命周期里有onTabItemTap回调可以拿到e.index、e.pagePath、e.text想拦截 tab 点击事件之前做埋点或判断用户登录态在这里处理比在 tabBar 自定义组件里处理更轻量。3.3 自定义导航栏与安全区适配有些页面需要沉浸式头部或者导航栏中间放自定义搜索框原生导航栏满足不了就得在pages.json里给对应页面设置navigationStyle: custom然后自己写导航栏区域。自己写导航栏第一件事是算状态栏高度。安卓和 iOS 的状态栏高度不一样全面屏和普通屏也不一样。不要硬编码运行时去取const systemInfo uni.getSystemInfoSync() this.statusBarHeight systemInfo.statusBarHeight然后在模板里当作 padding-top 用view classcustom-nav :style{ paddingTop: statusBarHeight px } view classnav-title个人中心/view /view底部安全区同样重要尤其是 iPhone 全面屏的 home indicator。给固定在底部的按钮或输入框加上安全区适配.safe-bottom { padding-bottom: constant(safe-area-inset-bottom); padding-bottom: env(safe-area-inset-bottom); }先写constant再写env是为了兼容 iOS 11.0 到 11.2 的旧版本。顺序反了的话旧版 iPhone 上底部 padding 会直接失效。4. 平台差异与打包引发的“假样式问题”4.1 小米打包后没麦克风权限真不是样式问题有人搜索“uniapp 小米手机打包 app 之后为啥没有麦克风权限”我猜最初的排查方向很可能在弹窗样式、授权提示样式这些地方绕了一圈。这里可以直接给结论这大概率不是样式问题而是权限声明问题。uniapp 打包成 App 时权限声明是在manifest.json里配置的。如果你没有在“App 模块配置”或“Android 权限配置”里勾选录音权限真机运行时系统压根不会弹授权框拿不到麦克风数据。小米对权限的管束尤其严格很多国产 ROM 还会在应用内二次检查权限是否真的被授予。处理步骤一般是这样的场景原因操作打包后无麦克风权限弹出manifest 未声明 RECORD_AUDIO在 manifest 检查并添加android.permission.RECORD_AUDIO在线打包后权限不起作用权限模块未勾选App 权限配置里勾选“麦克风/录音”相关模块离线打包后权限不起作用原生工程 AndroidManifest 配置缺失在离线打包工程的 manifest 中补声明并检查插件含 UTS 插件的权限配置6.0 以上系统不弹框未做运行时权限申请使用plus.android.requestPermissions动态申请后再调用录音功能如果你用了离线打包或者集成了 UTS 插件权限声明还要检查原生工程的和插件的 manifest不只是在 uniapp 里勾选一遍就完事。遇到这类问题先按表格顺序查不要在 CSS 上浪费一秒钟。4.2 webview 返回行为与常规页面不一致的处理web-view 组件在 uniapp 里是个很特殊的存在它渲染的是完全独立于页面体系的 web 内容。所以你会遇到一个怪现象进入 webview 页面后点了几层网页链接再按导航栏返回按钮它不会按照网页历史一层层退回而是直接退出了整个 webview 页面。很多产品会要求“先退网页历史再退页面”这就得自己接管返回逻辑。我的处理思路是在网页内部和 uniapp 页面之间建立一套消息约定。网页需要返回的时候通过uni.postMessage告诉外层页面当前状态外层页面根据状态决定是执行uni.navigateBack还是让网页内部自行回退。核心代码大致长这样在嵌入的网页里script document.addEventListener(UniAppJSBridgeReady, function () { document.querySelector(#backBtn).addEventListener(click, function () { uni.postMessage({ data: { type: back-request } }); }); }); /script在 uniapp 页面里onLoad() { // #ifdef APP-PLUS const currentWebview this.$scope.$getAppWebview().children()[0] currentWebview.addEventListener(popGesture, (res) { // 在这里拦截右滑返回处理网页历史回退 }) // #endif } handleMessage(e) { const data e.detail.data[0] if (data.type back-request) { // 判断是否还有网页历史有就让网页 history.back()没有就 navigateBack } }不同端的处理细节还有差别比如微信小程序里 web-view 的层级和返回行为跟 App 端不一样所以这段逻辑也建议用条件编译包起来单独维护。4.3 vue2 升 vue3 后样式跟着变了的几个场景从 vue2 转到 vue3最贴近“样式”的坑就是穿透选择器语法。vue2 时代很多人写/deep/或::v-deep到了 vue3 里这两个写法虽然有时还能跑但会有警告某些版本直接编译报错。标准做法统一改成:deep()。还有一类问题是 scss 本身的升级。vue2 项目常用旧版 sass除法还能直接写width: 100/3这种升级到 dart-sass 之后这种写法要么编译失败要么结果不对。遇到页面里某个带小数的宽度样式在升级后突然变了先去看 scss 表达式有没有用到已废弃的除法语法/* 旧写法 */ width: (750rpx / 3); /* 新写法 */ width: calc(750rpx / 3);最后不要忽略 Vue3 对响应式 proxy 的改动带来的副作用。有些组件库的样式切换依赖对 props 的监听升级后 props 监听时序变了导致 class 切换看起来“慢半拍”或闪一下这种问题排查起来很像样式 bug但根因在逻辑层别只盯着 css 文件。5. 常见样式问题排查与避坑速查5.1 样式不生效先从这三个方向查我在群里帮人看代码发现样式问题翻来覆去就那几类。这里整理成一张速查表遇到“改不动”“不生效”先对照它走一遍比自己瞎试快得多现象怀疑方向检查重点第三方组件内部样式改不动scoped 作用域与选择器权重是否用了:deep()组件是否支持customStyle小程序端完全没有样式H5 正常选择器兼容性是否用了*、body、标签选择器等受限写法样式只在一部分平台生效条件编译#ifdef/#ifndef的平台标识是否写对样式被全局污染样式覆盖顺序看 App.vue 全局样式和后引入的库样式谁先谁后修改后页面无变化缓存微信开发者工具清缓存、App 端重新编译尤其要注意*通配符。它确实能写出一行 reset但在小程序端性能损耗很大而且容易被各种组件库的样式再覆盖掉属于“看着省事、实际坑多”的写法。日常 reset 尽量收敛到几个最常用的标签选择器并且配合box-sizing: border-box一起写view, text, image, button, input { box-sizing: border-box; }5.2 几条真正好用的黑科技样式技巧下面这几个技巧是我项目里反复在用的能解决不少“看起来很高级”的需求代码量还不大。1px 细线。在 DPR 为 2 或 3 的屏幕上直接用height: 1px会显得很粗要得到视觉上的细线用伪元素加scaleY(0.5)缩放.hairline-bottom { position: relative; } .hairline-bottom::after { content: ; position: absolute; left: 0; bottom: 0; width: 100%; height: 1px; background: #ebedf0; transform: scaleY(0.5); transform-origin: 0 0; }文本多行省略。单行省略谁都懂多行省略经常会忘了-webkit-box-orient: vertical导致在部分浏览器里不生效.ellipsis-2 { overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }渐变文字。想让标题字色从橙到粉渐变不需要图片几行 CSS 就能搞定.gradient-text { background: linear-gradient(90deg, #ff9900, #ff2c55); -webkit-background-clip: text; background-clip: text; color: transparent; }这类技巧在小程序和 H5 上基本都能跑通但 nvue 原生渲染下不一定支持如果项目里混合使用了 nvue 页面记得先确认渲染模式再下手。5.3 我踩过的一些实用经验总结最后分享几条攒了很多年的经验。第一条是别神化 rpx。它确实方便但在横屏、iPad 这类非常规屏幕上会等比放大导致元素大得离谱。该用 px 固定尺寸的时候一定要用 px不能偷懒。第二条弹层类组件的滚动穿透问题一定要在联调阶段就测。很多项目是开发时好好的真机一测发现背景乱滚那是被手势时机骗了。给遮罩和内容区都加上touchmove拦截同时记录打开前的页面滚动位置关闭后手动恢复线上会稳很多。第三条能配置的不要写样式。导航栏颜色、tabBar 图标、页面背景色这些东西pages.json里能配置的尽量别用自定义 CSS 硬做。原生配置的性能和稳定性都好过 DOM 模拟尤其是在小程序端。第四条!important要克制。它确实是排查样式失效时最省事的办法但一旦用多了后面所有组件库升级、主题改造都寸步难行。能用嵌套选择器提升权重解决的就别上!important写注释说明原因更是好习惯。说到底uniapp 样式没有想象中神秘它还是那套 CSS 基本功只是多了一堆“平台分支”和“编译规则”。把 rpx 的换算、条件编译、穿透选择器和安全区适配这几件事吃透大部分项目的样式问题就解决了。剩下那些边角料问题就靠多真机、多平台去磨吧。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门