
上个月帮朋友看一个跑了两年多的微信小程序功能上线一切正常结果后台用户反馈里冒出一堆 iPhone SE 和 iPad 的截图小屏上三个按钮挤成一坨、副标题直接被截断iPad 上整页被放大得像老年机模式一屏只显示三行内容右半边一条大空白。微信小程序屏幕适配这件事做过的人都清楚它不难难在两端拉扯——小屏要塞得下iPad 大屏又不能空得慌。这篇文章把我在几个小程序项目里踩过的适配坑整理一遍从 rpx 的换算机制讲到 iPad 的分栏重构从导航栏高度怎么一次算准讲到真机上怎么排查。不管你是刚接手一个小程序的新手还是被 iPad 投诉折腾过的老手应该都能从里面捞到能直接抄的东西。1. 适配这件事到底难在哪先把问题拆开很多人对适配的理解停留在多写几个媒体查询结果改了半个月小屏还是挤、iPad 还是丑。根子在于没有把问题拆成独立的层单位是一层断点是另一层布局约束又是第三层。三层混在一起改改完 A 屏幕 B 屏幕崩最后只能靠不断叠加!important硬撑。我的习惯是拿到一个适配需求先问三个问题这个元素在小屏上能不能被压缩在大屏上它应该变宽还是保持原样它和屏幕边缘之间有没有必须保留的安全距离把这三个问题回答清楚代码怎么写基本就定下来了。1.1 rpx 的等比放大机制与它的边界微信小程序的 rpx 是一条很讨巧的设计不管实际屏幕多宽一律把屏幕逻辑宽度虚拟成 750rpx。于是在 iPhone 6逻辑宽 375pt上1rpx 等于 0.5px换成 iPhone SE320pt就变成 0.427px到了 iPad Pro 12.9 竖屏逻辑宽约 1024pt1rpx 直接变成 1.365px。这个换算关系意味着你写 32rpx 的正文字号在 iPhone 6 上是 16px在 iPad 上就变成约 43.7px——翻了将近 2.7 倍。问题的本质是rpx 保证的是比例一致而不是视觉一致。这对图标和文字的相对大小卡片的内边距占屏宽的比例这类需求非常友好因为比例本身就是设计意图。但它对必须保持物理尺寸的东西就是灾难比如 1px 的分割线、不少于 44px 的点击热区、圆角半径、图标的最小可辨识尺寸。这里有个特别经典的坑用1rpx画分割线。在 iPhone 6 上 1rpx 只有 0.5px浏览器的亚像素渲染策略会把它渲染成 1px 或者直接吞掉导致同一份代码在不同机型上线粗细不一。稳妥的做法是用伪元素加缩放.hairline { position: relative; } .hairline::after { content: ; position: absolute; left: 0; right: 0; bottom: 0; height: 1px; background: #e5e5e5; transform: scaleY(0.5); transform-origin: 0 100%; }所以 rpx 的边界很清楚随屏宽等比变化的尺寸交给 rpx需要锁定物理尺寸的交给 px。这条线划不清后面所有适配都是在原地打转。1.2 小屏和大屏各自暴露的真实症状适配问题在两端的表现完全不一样我把实际收到过的用户反馈整理成一张表对照着看会更容易定位。屏幕类型典型症状根因iPhone SE / mini 类小屏按钮文案换行成两截、卡片被压扁固定 px 宽度 内容不可压缩iPhone 全面屏底部操作条被 home 条遮挡、点不中没有处理安全区域Android 小屏系统字体调大后整页错位、文字溢出未限制字号放大、行高写死iPad 竖屏全页等比放大、字体巨大、一屏三行全量使用 rpx 且没有上限约束iPad 横屏 / 分屏布局被拉宽、内容被裁切单列布局 写死的固定宽度iPad 自由窗口内容区空得发慌或左右溢出缺少断点与最大宽度约束小屏的核心矛盾是空间不够解决思路是做减法能省的文案省掉能竖排的不要横排能用省略号的不要换行。大屏的核心矛盾是空间太多解决思路是做加法和做约束多列排布、限制最大宽度、增大留白和点击区域。1.3 三层适配策略的选型逻辑我总结下来一套靠谱的适配方案就三层层层递进第一层是单位层。把 rpx 和 px 的使用边界划定清楚这一步做完80% 的等比放大问题就消失了。第二层是断点层。用媒体查询或者运行时判断针对小屏、普通屏、大屏分别给出不同的布局规则。断点不要太多三档足够。第三层是约束层。给内容区加最大宽度、给字号加上限、给安全区留出距离。这一层是防止极端屏幕下布局彻底失控的兜底。这里我要明确反对一种做法用 JS 在onLoad里拿到屏幕宽度再用setData去改一堆元素的尺寸。原因是首屏渲染时setData还没回来页面会先按默认值渲染一次然后再跳一下用户看到的是闪烁和位移。适配应该尽量落在 CSS 层JS 只在必须知道精确数值的地方用比如自定义导航栏高度、scroll-view 的高度计算。2. 单位体系与断点设计地基打不对后面全是返工2.1 px、rpx、vw、rem 四种单位的实际使用边界小程序里能用到的单位其实就四种rem 基本可以排除因为小程序没有可以统一设置的根字号用 rem 反而会引入不确定性。剩下三种的分工我整理如下场景推荐单位原因卡片宽度、内边距、圆角rpx需要跟随屏宽等比缩放正文字号rpx 上限覆盖小屏保证可读大屏不能失控1px 分割线、边框px transform 缩放保持物理细线避免亚像素吞线按钮最小高度、图标热区px触控热区不能随屏幕缩水全屏背景、轮播高度vh / vw需要贴合视口比例弹窗、内容区最大宽度px大屏上不能被拉满vw 和 vh 在小程序里对应的是窗口宽高切换横竖屏时会重新计算这一点对 iPad 特别有用。比如一个全屏的引导页背景用height: 100vh比用height: 1334rpx稳得多因为后者在 iPad 横屏下会撑不满。关于字号我一般会这样处理基础字号用 rpx 写然后在媒体查询里针对大屏把关键文本的字号用 px 覆盖掉。.title { font-size: 34rpx; line-height: 1.4; } /* 进入平板档位后锁定字号避免被等比放大 */ media screen and (min-width: 768px) { .title { font-size: 20px; line-height: 28px; } }2.2 断点怎么切把无限多的屏幕装进三四个抽屉市面上的手机和平板屏幕尺寸是连续变化的你不可能每个都适配。我的做法是按逻辑宽度切三档最多四档小屏档宽度小于等于 360px覆盖 iPhone SE、部分老安卓机标准档361px 到 767px覆盖绝大多数手机平板档768px 及以上覆盖 iPad 全系和安卓平板WXSS 的媒体查询写起来是这样/* 小屏档收紧内边距缩小次要字号 */ media screen and (max-width: 360px) { .card { padding: 16rpx 20rpx; } .card .subtitle { font-size: 22rpx; } } /* 平板档切换多列布局锁字号 */ media screen and (min-width: 768px) { .card-list { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 24px; } .card { padding: 20px 24px; max-width: 480px; } }注意小程序媒体查询里的 px 是逻辑像素和wx.getWindowInfo()返回的windowWidth单位一致不用担心像素比换算。但断点一定要用 px 写用 rpx 写会因为基准是 750 而在所有屏幕上得到相同结果等于没写。2.3 安全区域、刘海与圆角的处理细节全面屏 iPhone 的底部有一条 home 指示条iPad 四角有圆角这些区域如果放了可点击元素用户是点不准的。小程序提供了env(safe-area-inset-*)系列变量来处理。.bottom-bar { position: fixed; left: 0; right: 0; bottom: 0; padding-bottom: constant(safe-area-inset-bottom); padding-bottom: env(safe-area-inset-bottom); background: #fff; }两个padding-bottom是刻意写两遍的constant()是为了兼容老版本 iOSenv()是新标准浏览器遇到不认识的值会自动忽略所以写两遍是安全的。滚动容器的高度计算也要把安全区算进去否则最后一个列表项会被 home 条盖住.scroll-container { height: calc(100vh - 88rpx - env(safe-area-inset-bottom)); }另外提一个容易被忽略的点wx.getWindowInfo()返回的safeArea对象里有top、bottom、left、right四个值bottom是安全区底边的 y 坐标用screenHeight - safeArea.bottom就能算出底部需要留出的高度。这个值在 iPad 横屏下也会变化如果页面上有悬浮按钮最好在onResize里重新读一次。3. 小屏幕适配实操每一像素都要抠3.1 自定义导航栏高度的一次算准自定义导航栏是适配翻车的重灾区。很多人直接写死height: 88rpx加padding-top: 44px在自己手机上看着挺好到了别的机型标题就偏上或偏下。原因很简单Android 各家厂商的状态栏高度不一致硬编码一定会翻车。正确的做法是让胶囊按钮反推导航栏高度。因为胶囊按钮的垂直位置是微信自己算好的它和状态栏、导航栏之间的间距在各端保持一致所以用它当参照物最准。// utils/navbar.js function getNavBarInfo() { const windowInfo wx.getWindowInfo(); const statusBarHeight windowInfo.statusBarHeight || 20; let menuButton null; try { menuButton wx.getMenuButtonBoundingClientRect(); } catch (e) { menuButton null; } // 兜底部分低版本基础库或异常场景下拿不到胶囊信息 if (!menuButton || !menuButton.height) { return { statusBarHeight, navBarHeight: 44, totalHeight: statusBarHeight 44 }; } const navBarHeight (menuButton.top - statusBarHeight) * 2 menuButton.height; return { statusBarHeight, navBarHeight, totalHeight: statusBarHeight navBarHeight }; } module.exports { getNavBarInfo };这个公式的推导逻辑是胶囊按钮上边缘到状态栏下边缘的距离等于胶囊下边缘到导航栏下边缘的距离微信在两端留的间距是对称的所以导航栏内容高度等于这个间距乘以 2 再加上胶囊自身高度。注意wx.getMenuButtonBoundingClientRect()在开发者工具的某些版本里会返回全 0所以兜底分支一定要写。另外这个方法只能在页面渲染后调用不要在app.js的onLaunch里调用。拿到totalHeight之后页面内容区要整体下移同时用占位元素把空间撑起来避免内容被导航栏盖住view classnav-placeholder styleheight: {{totalHeight}}px;/view view classcustom-nav styleheight: {{totalHeight}}px; padding-top: {{statusBarHeight}}px; view classnav-title订单详情/view /view3.2 小屏下的布局压缩与信息取舍小屏适配的核心动作是做减法但减法不是随便删而是按优先级删。我的顺序一般是先删装饰性图标再收缩间距再考虑隐藏次要文案最后才动主要内容的结构。收缩间距用媒体查询覆盖是最直接的.list-item { padding: 28rpx 32rpx; } media screen and (max-width: 360px) { .list-item { padding: 20rpx 24rpx; } }文本溢出处理是小屏上最高频的需求单行和多行的写法不一样.ellipsis { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .ellipsis-2 { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden; }这里有一个 flex 布局里的隐藏坑我踩过不止一次flex 子元素默认的min-width是auto意思是它不会小于内容宽度。所以即使你给子元素写了width: 0或者flex: 1只要里面有一段长文本它依然会把父容器撑破省略号根本出不来。解决办法就是显式写min-width: 0。.row { display: flex; align-items: center; } .row .left { flex: 1; min-width: 0; /* 关键不加这行省略号永远不生效 */ } .row .right { flex-shrink: 0; margin-left: 16rpx; }3.3 滚动、长列表与触控热区scroll-view 的高度必须是一个确定值不能靠内容撑。小屏上因为内容多很容易出现滚动区域高度算错导致滚不动的问题。我一般用视口高度减去固定头尾来算.scroll-area { height: calc(100vh - 88rpx - 120rpx - env(safe-area-inset-bottom)); }如果头部高度是动态的就把头部高度写进 data用内联样式传给 scroll-view。关于苹果手机上小程序不能滑动滚动这个高频问题我遇到的成因基本就四类按排查顺序列一下一是祖先元素上有overflow: hidden把滚动链路截断了二是某个节点上绑了catchtouchmove或者catch:touchmove事件被吞掉了三是有个全屏 fixed 遮罩层关闭后没有移除透明但依然吃事件四是 scroll-view 自身高度算成了 0。前三个是事件和层叠问题第四个是尺寸问题定位手段也不一样。触控热区这块小屏上尤其要注意。iOS 人机界面指南建议最小可点击区域是 44x44pt很多小程序的关闭按钮只有 20px 见方小屏上手指一按就点偏。我的做法是视觉尺寸保持小巧但用伪元素把热区撑开.close-btn { position: relative; width: 32rpx; height: 32rpx; } .close-btn::after { content: ; position: absolute; top: -20px; right: -20px; bottom: -20px; left: -20px; }4. iPad 大屏适配从能用到好用4.1 resizable 配置与多窗口、台前调度小程序在 iPad 上的默认行为是固定尺寸运行整页按手机宽度渲染居中显示左右留出大片黑边。这个模式开发省事但用户看到的是一块手机屏贴在 iPad 中间体验很割裂。要让小程序真正铺满 iPad需要在页面配置里开启可调整尺寸{ navigationStyle: custom, resizable: true, pageOrientation: auto }resizable控制窗口是否可以改变尺寸pageOrientation设为auto之后支持横屏。这两个开关打开用户旋转 iPad 或者用分屏、台前调度改变窗口大小时页面就会跟着重排。窗口尺寸变化时页面会触发onResize生命周期在这里更新布局需要的宽度值Page({ data: { windowWidth: 375, isTablet: false }, onLoad() { const info wx.getWindowInfo(); this.applyWindowInfo(info.windowWidth); }, onResize(res) { // res.size.windowWidth / res.size.windowHeight this.applyWindowInfo(res.size.windowWidth); }, applyWindowInfo(width) { this.setData({ windowWidth: width, isTablet: width 768 }); } });台前调度下的窗口是可以被拖成任意尺寸的这就意味着布局必须写成流式的任何依赖屏幕只有两种宽度的假设都会失效。这也是我建议用auto-fill minmax网格而不是写死断点数量的原因后面会细讲。4.2 单列转分栏布局重构的三种套路从手机单列切到 iPad 多列我常用三种套路按复杂度递增。第一种是弹性网格适合卡片列表。用repeat(auto-fill, minmax(...))让浏览器自己决定一行放几张卡片一行代码解决从 1 列到 4 列的过渡完全不需要写断点.card-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 20px; padding: 20px; } media screen and (max-width: 767px) { .card-list { grid-template-columns: 1fr; gap: 12px; padding: 12px; } }minmax(280px, 1fr)的意思是每列最小 280px最大按比例平分剩余空间。窗口宽度 1024 时一行自然排三张宽度 800 时排两张不需要任何额外的判断。第二种是左右分栏适合列表加详情的结构。iPad 横屏下左边放列表右边放内容这种结构最接近 iPad 原生应用的体验用户不用来回跳页。实现上给容器加 flex 方向切换.layout { display: flex; flex-direction: column; height: 100vh; } media screen and (min-width: 768px) { .layout { flex-direction: row; } .layout .sidebar { width: 320px; flex-shrink: 0; border-right: 1px solid #eee; } .layout .content { flex: 1; min-width: 0; overflow-y: auto; } }第三种是主辅双区适合表单类页面。主表单占左侧右侧放帮助说明或者实时预览。这种改动量大只有当产品明确要求iPad 上要像桌面端时才值得做。4.3 内容宽度、字号与留白的三条硬规则分栏做完只是第一步大屏上最容易翻车的是内容被拉得太宽。一段文字横跨 1024px用户读第二行时眼睛要横跳很远阅读体验极差。规则一内容区必须有最大宽度约束。我的经验值是 750px 左右超过就居中留白。.content-wrap { width: 100%; max-width: 750px; margin: 0 auto; box-sizing: border-box; }规则二字号要锁上限。用 rpx 写的字号到了 iPad 上会被放大 2 倍多正文必须用媒体查询覆盖成固定值。.body-text { font-size: 28rpx; line-height: 1.6; } media screen and (min-width: 768px) { .body-text { font-size: 16px; line-height: 26px; } }规则三留白和热区要随屏增大而不是等比拉伸。小屏上卡片内边距 24rpx 是合适的iPad 上如果还是按 rpx 走会得到约 33px看着还行但整体偏紧。我一般在大屏档位把内边距改成 24px 到 32px 的固定值视觉上更从容。另外还有一个特别容易忽略的点iPad 横屏时页面两侧的圆角。iPad 是有圆角的如果全屏背景铺满四角会被切掉一块。如果不希望圆角处露出白底可以在根节点加一个同色的背景容器或者干脆接受这个裁切。这个取舍看设计稿但从我的角度纯色背景铺满时的圆角裁切通常不影响观感不需要专门处理。5. 一套可复用的适配代码底座5.1 全局屏幕信息注入与封装每个页面都写一遍getWindowInfo太啰嗦我一般封装成一个全局模块加一个 Behavior页面引入之后自动拿到尺寸信息还能响应尺寸变化。// utils/device.js const win wx.getWindowInfo(); const device { windowWidth: win.windowWidth, windowHeight: win.windowHeight, statusBarHeight: win.statusBarHeight, // 底部安全区高度 safeBottom: win.screenHeight - (win.safeArea ? win.safeArea.bottom : win.screenHeight), isTablet: win.windowWidth 768, isSmallScreen: win.windowWidth 360 }; module.exports device;Behavior 版本// behaviors/adaptive.js const win wx.getWindowInfo(); module.exports Behavior({ data: { windowWidth: win.windowWidth, isTablet: win.windowWidth 768 }, methods: { // px 转 rpx用于少数必须动态计算的场景 px2rpx(px) { return px * 750 / this.data.windowWidth; } } });页面里用behaviors: [require(../../behaviors/adaptive)]引入this.data.isTablet直接就能用来做条件渲染。需要提醒的是条件渲染只适合结构性差异比如iPad 上多显示一个侧边栏纯样式差异还是交给媒体查询否则每次尺寸变化都要setData代价不小。5.2 WXSS 响应式样式模板我把经常复用的一套模板整理在这里直接贴到项目里改改就能用/* 基础容器限制最大宽度居中 */ .page-container { width: 100%; max-width: 750px; margin: 0 auto; box-sizing: border-box; } /* 卡片列表手机单列平板多列 */ .grid-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 24rpx; padding: 24rpx; } /* 固定底栏留出安全区 */ .fixed-footer { position: fixed; left: 0; right: 0; bottom: 0; padding-bottom: constant(safe-area-inset-bottom); padding-bottom: env(safe-area-inset-bottom); background: #ffffff; z-index: 100; } /* 小屏收紧 */ media screen and (max-width: 360px) { .grid-list { gap: 16rpx; padding: 16rpx; } } /* 平板档位锁字号、放宽间距 */ media screen and (min-width: 768px) { .grid-list { gap: 20px; padding: 24px; } }5.3 uni-app / Taro 跨框架的适配差异如果你不是原生小程序而是用 uni-app 或 Taro 开发适配的坑会多一层因为多了一层编译转换。uni-app的 rpx 基准同样是 750但它在 H5 端会把 rpx 转成 vw在小程序端保持 rpx。这意味着同一份样式在两个端的表现不完全一致特别是你用了calc()混合单位的时候。我建议在 uni-app 里需要跨端一致的尺寸用 px 或者uni.upx2px()转换后使用纯小程序端再用 rpx。另外 uni-app 在 iPad 上默认也不做特殊处理resizable需要在pages.json里配置{ pages: [ { path: pages/index/index, style: { navigationStyle: custom, resizable: true, pageOrientation: auto } } ] }Taro的转换规则不太一样。它用postcss-pxtransform把设计稿尺寸按 750 基准转换你写 px 它会转成 rpx但有两个例外大写PX不转换加了/* postcss-pxtransform disable */注释的行不转换。.a { width: 100px; } /* 会被转成 rpx */ .b { width: 100PX; } /* 保持 px 不变 */ /* postcss-pxtransform disable */ .c { width: 100px; } /* postcss-pxtransform disable end */这两个例外是 Taro 里处理我就想要固定 px场景的官方手段比在媒体查询里反复覆盖干净得多。但要注意postcss-pxtransform的配置项里有个platform参数不同平台的转换比例可能不同接项目的时候先看一眼config/index.js里是怎么配的别想当然。6. 排查实录适配问题速查与真机验证6.1 常见问题速查表适配问题的表现五花八门但成因其实高度集中在几类。这张表是我攒了两年的排查经验遇到问题先在里面找对应的行。现象可能原因定位方法解决方向iPad 上字号巨大全量使用 rpx 且无上限对比手机与 iPad 截图大屏档位用 px 覆盖字号小屏内容溢出横向滚动固定 px 宽度或 min-width 未归零给容器加临时边框看边界改 rpxflex 子元素加 min-width: 0底部按钮被 home 条遮挡未处理安全区在全面屏机型上查看补 env(safe-area-inset-bottom)自定义导航栏标题偏移硬编码状态栏和导航栏高度不同安卓机型对比用胶囊按钮反推高度分割线粗细不一用 rpx 画 1px 线多机型对比截图伪元素 transform scaleY(0.5)苹果上页面滑不动overflow hidden / catchtouchmove / 透明遮罩逐层注释排查逐个移除事件拦截和遮挡层弹窗在 iPad 上铺满全屏未设最大宽度大屏上打开弹窗加 max-width 并居中横屏后布局错乱未监听尺寸变化旋转 iPad 复现实现 onResize 并重置状态长按拖拽与页面滚动冲突事件未做区分长按列表项复现用 movable-view 或长按后锁滚动图片在大屏上过于模糊只出了一倍图大屏上放大查看按倍数准备多套或使用矢量图6.2 真机验证与调试方法开发者工具的设备模拟能覆盖大部分布局问题但有三件事必须在真机上验证安全区、字体渲染、滚动性能。工具里的安全区是模拟的和真机有偏差。我的验证流程是这样的先在开发者工具里打开设备模拟把常见机型过一遍重点看小屏和 iPad 两档然后用真机预览至少覆盖一台小屏 iPhone、一台普通安卓、一台 iPad最后用真机调试看控制台有没有尺寸相关的报错。iPad 上的验证有一个容易漏的场景分屏。把小程序和另一个应用同时打开窗口宽度会变成约 500px 到 600px这个宽度既不属于小屏档也不属于平板档如果你的断点是按 768 切的此时会落到标准档布局需要能正常显示。我一般会在 500px 到 767px 这个区间额外看一眼确保网格不会出现一行两张但每张都变形的情况。还有一个细节wx.getWindowInfo()返回的windowHeight不包含状态栏和导航栏而screenHeight包含。算固定元素高度时一定要想清楚用的是哪个值用错了就会出现底部差一点点或多出一截的现象。6.3 上线前的自检清单每次发版前我会过一遍这个清单能挡掉大部分适配相关的线上问题用 iPhone SE 尺寸查看所有主要页面确认没有横向溢出和文案截断用 iPad 竖屏和横屏各看一遍确认字号没有被放大到离谱检查所有固定底部元素是否留了安全区检查自定义导航栏是否用胶囊反推且写了兜底分支检查 scroll-view 高度是否有固定值最后一项能否完整显示检查所有 flex 子元素的长文本区域是否加了min-width: 0在台前调度下把窗口拖到最窄和最宽确认布局不崩检查弹窗、抽屉类组件在大屏上是否有最大宽度限制检查所有图片在小屏和大屏上的显示模式是否合适最后分享一个我个人一直在用的判断标准把小程序分别放在一台 iPhone SE 和一台 iPad 上如果两次看下来你没有产生这是一版没做完的设计的感觉那适配算是过关了。适配不是为了追求像素级完美而是为了让用户不觉得别扭。我在最近一个项目里把 iPad 的卡片列表从单列改成了自动网格只改了十几行 CSS客诉里关于大屏的问题直接归零投入产出比相当高。