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