尴尬而不失优雅地实现移动端响应式布局

菜鸡工程师  |  2019. 07. 16   |  阅读 839 次

单位选择: vw 还是 rem ?

我们选择了 rem 作为像素单位。因为本次开发的项目包含 ipad 与手机端,使用 rem 单位应对的根元素字体可以根据设备动态设置。因此 ipad 端与手机端公共的样式只需要写一套代码就能实现,而使用vw作为单位在无论什么情况下都需要写2套样式,见下面的例子:
假设现在2倍视觉稿上有一个显示 500*300 的按钮,而这个按钮在ipad端和手机端的样式相同。下面是两种写法的对比:
使用vw作为单位,视觉稿上手机宽度为 750,ipad宽度为 2048

.button.phone {
    width: 100 * 500 / 750vw;
    height: 100 * 300/ 750vw;
}
.button.ipad {
    width: 100 * 500 / 2048vw;
    height: 100 * 300/ 2048vw;
}

使用rem作为像素单位,根据屏幕的宽度(1024px作为分界点)大于这个像素按ipad的样式做适配,否则按手机设备做适配。设置基数为20,即视觉稿上的html根元素字体大小20px。

let wWidth = document.body.offsetWidth;  
let html = document.documentElement;  
let remNum = wWidth < 1024 ? (wWidth / 750) * 20 : (wWidth / 2048) * 20;  
 html.style.fontSize = `${remNum}px`;

因此对于公共的样式只需要写一套代码就行。

.button {
    width: 500 / 20rem;
    height: 300 / 20rem;
}

当然上述的都是伪代码,less是不支持这种写法 因此最终的代码要这么写:

.button {
    width: 25rem;
    height: 15rem;
}

手动计算不恶心吗?

每次写样式都要在心里计算一遍单位,如果碰到不能被20整除的单位,只能使用计算器,非常恶心。
好在 less 提供了一套单位转换函数 unit(@px, rem) 将px转化为rem,且它支持四则运算。
因此上述的样式可以这么写:

.button {
    width: unit(500/20, rem);
    height: unit(300/20, rem);
}

结束了?

这样就结束了?远远不够,每次都要写重复的代码,非常麻烦,可以再节约些吗?
使用less 提供的mixin 封装公共的样式方法:

.button {
    .w(500);
    .h(300);
}

// mixin
.w(@px) {
    width: unit(@px / @baseUnit, rem);
}
.h(@px) {
    height: unit(@px / @baseUnit, rem);
}

真的结束了吗?

看似解决了重复的问题,但是有引入了新的问题: 设置margin的值,不能连着写, 必须写四个样式,虽然说mixin 支持...arguments 实现动态参数,但别忘记了还需要对参数做单位转化呢,因此不能满足我们的需求。

.button {
    .mt(10);
    .mr(10);
    .mb(10);
    .ml(10);
}

遇到 translate, background-size 等这些不常用的样式,推荐用原始的方式去写。对这种不常用的样式封装意义不大,而且还会增加mixin函数的记忆成本。

.button {
    translate: (unit(300/20, rem), unit(300/20, rem));
    background-size: (unit(300/20, rem), unit(300/20, rem));
}

切换成 sass 去避坑

sass自带了自定义函数的功能,可以解决上述问题的痛点。

// px to rem
@function x2r($px) {
  @return $px * $baseUnit * 1rem;
}
.button{
  width: x2r(500);
  height: x2r(300);
  margin: x2r(10) x2r(10);
  transform: translate(x2r(500), x2r(300));
}

唯一的缺点是语法不够优雅😂

有最终的解决方案吗?

感觉这样用起来还是很不方便,还达不到完美的境界,有更好的解决办法吗? 最终解决方案:采用 webpack 的 loader 直接完成单位的转换。

.button {
  width: 500pxr;
  height: 300pxr;
  margin: 10pxr 10pxr;
  transform: translate(500pxr, 300pxr);
}
// loader 转化后
.button {
  width: 250rem;
  height: 150rem;
  margin: 0.5rem 0.5rem;
  translate: (150rem, 150rem);
}

具体思路:对.vue文件与.less 文件中的less代码做一次替换,把pxr单位转换成rem单位。在vue-loader与less-loader之前插入这个单位转化的 loader 完成单位的转化。

// unit-convert-loader.js
const loaderUtils = require('loader-utils');

exports.default = function(source) {  
    const { remBase = 16, isVueFile = false } = loaderUtils.getOptions(this);
    function replaceStyle(styleStr) {
        return styleStr.replace(/\d*\.?\d+pxr(?=;|\)|,| )/g, $1 => {
            const pixels = parseInt($1);
            return `${pixels / remBase}rem`;
        });
    }
    // .vue 文件中从 style 标签中获取样式规则进行替换
    if (isVueFile) {
        source = source.replace(
            /(<style.+>)([\s\S]*)(<\/style>)/g,
            (_, $1, style, $2) => {
                return `${$1}${replaceStyle(style)}${$2}`;
            }
        );
        return `export default ${source}`;
    } else {
        // 其他的样式文件,直接进行替换
        return replaceStyle(source);
    }
};

不用手动计算单位,不用去记mixin的函数,不用每次写重复的的代码,书写规则更接近于原始,是不是很方便😄。

VS postcss-pxtorem

与postcss-pxtorem 做比较,不敢说比它更优秀,但是应该比它更能满足我们目前的业务需求。一旦将来切换到其他项目,单位换成vw,这个工具只需稍微做个拓展,改变下loader中传入的参数也依旧可以支持将单位转换为vw。

结束了

仅仅只是是文章结束了。这种方法还有一些局限性,不支持vue模板中的style语法中的单位转换。不是不能实现,一旦支持但是上面的“replaceStyle” 函数就没法复用,替换的正则表达式会更加复杂,而且即时支持了收益也不大,完全有代替的方案。杀鸡不用牛刀,所以我选择放弃。

最后再附上vue-cli3 自定义loader的配置

    chainWebpack: config => {
        config.module
            .rule('less')
            .test(/\.less$/)
            .oneOf('normal')
            .use('unit-convert-loader')
            .loader(path.resolve('unit-convert-loader.js'))
            .options({
                remBase: 20
            });
        config.module
            .rule('vue')
            .test(/\.vue$/)
            .use('unit-convert-loader')
            .loader(path.resolve('unit-convert-loader.js'))
            .options({
                remBase: 20,
                isVueFile: true
            });

分享到

   
MySQL 不同数据量的优化方案
加入我们