36

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

 5 years ago
source link: https://www.tuicool.com/articles/bUFFr2i
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

单位选择: 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));
}

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

有最终的解决方案吗?

感觉这样用起来还是很不方便,还达不到完美的境界,有更好的解决办法吗? 最终解决方案:采用 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的函数,不用每次写重复的的代码,书写规则更接近于原始,是不是很方便:smile:。

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
            });

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK