移动端rem适配方案实战踩坑总结

南宫东芳 移动 阅读 927
赞 14 收藏
二维码
手机扫码查看
反馈

核心代码就这么几行

先上核心代码,这个rem适配方案我已经用了好几年,亲测有效。整个方案的核心就是这几十行JavaScript代码:

移动端rem适配方案实战踩坑总结

(function() {
    var docEl = document.documentElement;
    var clientWidth = docEl.clientWidth;
    
    if (clientWidth >= 750) {
        // 设计稿宽度750px,按照iPhone X的标准来计算
        docEl.style.fontSize = '100px';
    } else {
        // 动态计算字体大小
        docEl.style.fontSize = 100 * (clientWidth / 750) + 'px';
    }
    
    // 监听屏幕旋转和resize事件
    window.addEventListener('resize', function() {
        var newClientWidth = docEl.clientWidth;
        if (newClientWidth >= 750) {
            docEl.style.fontSize = '100px';
        } else {
            docEl.style.fontSize = 100 * (newClientWidth / 750) + 'px';
        }
    });
    
    // 针对移动端设备做额外处理
    window.addEventListener('orientationchange', function() {
        setTimeout(function() {
            var newClientWidth = docEl.clientWidth;
            if (newClientWidth >= 750) {
                docEl.style.fontSize = '100px';
            } else {
                docEl.style.fontSize = 100 * (newClientWidth / 750) + 'px';
            }
        }, 100); // 延迟执行,确保屏幕尺寸已更新
    });
})();

这段代码的作用是动态设置html根元素的font-size,让1rem始终等于设计稿的1/7.5。比如设计稿是750px宽,那1rem就等于100px。这样在写CSS的时候,设计师量出来的像素值除以100就能得到对应的rem值。

这个场景最好用

rem方案特别适合移动端H5页面开发,尤其是需要适配各种尺寸手机的那种。我之前做过一个电商活动页,从iPhone SE到iPhone 14 Pro Max都能完美显示,完全不用关心具体的像素值。

CSS代码长这样:

/* 基于750px设计稿 */
.container {
    width: 7.5rem; /* 750px */
    height: 1.33rem; /* 133px */
    margin: 0.2rem auto; /* 20px */
    padding: 0.3rem; /* 30px */
}

.header {
    height: 1.2rem; /* 120px */
    line-height: 1.2rem; /* 120px */
    font-size: 0.36rem; /* 36px */
    background-color: #ff6b35;
}

.content {
    padding: 0.24rem; /* 24px */
    font-size: 0.28rem; /* 28px */
}

你看,这里的数值都是直接从设计稿量出来除以100的结果。如果设计稿是375px宽,那就除以37.5,以此类推。不过我个人还是推荐750px的设计稿尺寸,因为现在高分辨率手机太多了,750px能保证足够的精度。

踩坑提醒:这三点一定注意

用rem方案最容易踩的坑就是viewport设置。很多人直接复制网上的meta标签,结果导致适配出问题:

<!-- 错误做法 -->
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">

<!-- 正确做法 -->
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">

记住,initial-scale一定要设置为1,不能让浏览器自动缩放。我之前就是因为这个参数没设置对,导致在某些Android机型上显示异常,折腾了整整一天才发现问题。

第二个坑是dpr处理。有些高端机dpr是3,如果不处理的话可能会出现字体模糊的问题:

// 改进版的rem设置,考虑了dpr因素
(function() {
    var dpr = window.devicePixelRatio || 1;
    var scale = 1 / dpr;
    
    var metaEl = document.querySelector('meta[name="viewport"]');
    if (!metaEl) {
        metaEl = document.createElement('meta');
        metaEl.setAttribute('name', 'viewport');
        document.head.appendChild(metaEl);
    }
    
    metaEl.setAttribute('content', 
        'width=device-width,' +
        'initial-scale=' + scale + ',' +
        'maximum-scale=' + scale + ',' +
        'minimum-scale=' + scale + ',' +
        'user-scalable=no'
    );
    
    // 设置data-dpr属性,方便CSS中针对不同dpr做特殊处理
    document.documentElement.setAttribute('data-dpr', dpr);
    
    var docEl = document.documentElement;
    var clientWidth = docEl.clientWidth;
    
    if (clientWidth >= 750) {
        docEl.style.fontSize = 100 * dpr + 'px';
    } else {
        docEl.style.fontSize = 100 * dpr * (clientWidth / 750) + 'px';
    }
    
    window.addEventListener('resize', setRem);
    window.addEventListener('orientationchange', function() {
        setTimeout(setRem, 100);
    });
    
    function setRem() {
        var clientWidth = docEl.clientWidth;
        if (clientWidth >= 750) {
            docEl.style.fontSize = 100 * dpr + 'px';
        } else {
            docEl.style.fontSize = 100 * dpr * (clientWidth / 750) + 'px';
        }
    }
})();

第三个坑是边界值处理。有时候屏幕刚好是750px宽,但是由于四舍五入或者小数点精度问题,可能会出现微小的差异,所以要做边界判断。上面的代码里都有体现。

配合Sass/Less提高效率

手动换算px到rem确实挺烦人的,我们可以用预处理器来简化这个过程。我平时用Sass比较多,写了个mixin:

// 定义设计稿基准宽度
$design-width: 750px;

// px转rem函数
@function px2rem($px) {
    @return $px / ($design-width / 7.5) * 1rem;
}

// 使用示例
.container {
    width: px2rem(750px); // 输出: 7.5rem
    height: px2rem(133px); // 输出: 1.33rem
    margin: px2rem(20px); // 输出: 0.2rem
    padding: px2rem(30px); // 输出: 0.3rem
}

// 如果想一次性设置多个值,可以写个mixin
@mixin size($width, $height: $width) {
    width: px2rem($width);
    height: px2rem($height);
}

.card {
    @include size(375px, 200px);
}

这样写起来就方便多了,直接用px值,编译后自动转换成rem。不过要注意,在涉及到border、阴影等效果时,可能需要保持1px的物理像素,这时候就不能用rem了:

.border-hairline {
    border: 1px solid #ddd;
}

// 在高DPR设备上,可以用媒体查询处理
@media (-webkit-min-device-pixel-ratio: 2) {
    .border-hairline {
        border-width: 0.5px;
    }
}

@media (-webkit-min-device-pixel-ratio: 3) {
    .border-hairline {
        border-width: 0.33px;
    }
}

线上验证和调试

在实际项目中,我还写了一个简单的调试工具,方便查看当前页面的rem计算情况:

// 调试工具,生产环境记得注释掉
if (process.env.NODE_ENV === 'development') {
    var debugInfo = document.createElement('div');
    debugInfo.innerHTML = 
        &lt;div style=&quot;position: fixed; top: 10px; left: 10px; z-index: 9999; 
                    background: rgba(0,0,0,0.7); color: white; 
                    padding: 10px; font-size: 12px; border-radius: 5px;&quot;&gt;
            &lt;div&gt;Screen: ${screen.width} × ${screen.height}&lt;/div&gt;
            &lt;div&gt;Client: ${document.documentElement.clientWidth} × ${document.documentElement.clientHeight}&lt;/div&gt;
            &lt;div&gt;DPR: ${window.devicePixelRatio}&lt;/div&gt;
            &lt;div&gt;FontSize: ${document.documentElement.style.fontSize}&lt;/div&gt;
            &lt;div&gt;1rem = ${parseFloat(document.documentElement.style.fontSize)}px&lt;/div&gt;
        &lt;/div&gt;
    ;
    document.body.appendChild(debugInfo);
}

这样可以在开发时实时看到各种参数,遇到适配问题能快速定位。不过记得上线前要把这个调试信息注释掉。

兼容性和性能考虑

rem方案在现代浏览器中兼容性还不错,iOS Safari 6+,Android Browser 4.4+都支持。不过在一些老版本Android浏览器上可能会有问题,可以通过Feature Detection来做降级处理:

// 检测是否支持rem
function isRemSupported() {
    var testElement = document.createElement('div');
    testElement.style.cssText = 'width: 1rem;';
    document.body.appendChild(testElement);
    var isSupported = testElement.style.width === '1rem';
    document.body.removeChild(testElement);
    return isSupported;
}

if (!isRemSupported()) {
    // fallback到px或者其他方案
    console.warn('REM is not supported, using fallback...');
    // 这里可以加载备用的适配方案
}

性能方面,频繁的resize事件监听确实会影响性能。我的建议是在resize事件上加个防抖,减少不必要的计算:

function debounce(func, wait) {
    let timeout;
    return function executedFunction(...args) {
        const later = () => {
            clearTimeout(timeout);
            func(...args);
        };
        clearTimeout(timeout);
        timeout = setTimeout(later, wait);
    };
}

// 应用防抖
window.addEventListener('resize', debounce(setRem, 100));

以上是我个人对这个rem适配方案的完整讲解,有更优的实现方式欢迎评论区交流。这个技巧的拓展用法还有很多,后续会继续分享这类博客。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论