第三代移动端布局方案
大家有没有发现淘宝的 H5 移动端没有使用任何 rem 和 vw 单位,而是和 web 端项目一样,使用的是 px 单位。虽然是 px 但它也很完美的将整个页面渲染了出来。那淘宝的 FE 是怎么实现的呢?
最近在研究关于布局的设计方案,通过学习理解阿里的 fusion.design 的设计思想并结合手机淘宝 H5 版的 px 布局问题。逐渐有了一些想法,这里进行综合整理,也算是抛砖引玉吧。
1、rem 和 vw
rem 和 vw 都是为了解决移动端适配问题。rem 方案中最成功的就是淘宝的 lib-flexible 了,它是通过 javascript 将整个布局分割成 10 份,从而进行有效布局。不过有计算 dpr 的问题,在一些 dpr 比较怪异的手机上会出现脱相的问题。后来又产生了 vw 布局,使用了 vw 之后,也再无需通过 javascript 的帮助进行布局的切分,而是自动的将整个布局切割为等分的 100 份,也就是 1vw = 1% 的页面宽度。
1.1、rem 的问题
在奇葩的 dpr 设备上表现效果不太好,比如 一些华为的高端机型 用 rem 布局会出现错乱。
设置根字体大小的方式有两种,一种是媒体查询,优点:不需要额外使用 js 去更改 html 的字体,缺点:不连续,或者说并能完 * 全实现对所有设备的布局规范统一;
另一种是 js 动态更改 html 字体,优点:连续;缺点:不如直接写媒体查询的体验好;
不支持 css3 calc 的需要大量密集的 @media hack;
使用 iframe 引用也会出现问题;
需要解决在 ios 上的 1px 边框问题,但是这个在 lib-flexible 中已经解决:(1px 变 2px,又被 initial-scale=0.5 缩小了一半
rem 需要引入一个 lib 库
html 的 font-size 设置到 12px 以下还是会按照 12px=1rem 来计算,这样所有使用了 rem 单位的尺寸都是错的
1.2、vw 的问题
支持程度不太好,安卓 4.4 以下都不支持
1.3、它们共同的问题
都需要计算以达到适配的目的
额外引入工具,在编译阶段完成转换
UI 的回归测试不友好。毕竟设计稿是 px,而页面是 rem 或 vm。
都是相对单位。rem 的比例是可以通过控制 html 字体大小来控制的,而 vw 的比例是固定的。
无法和 web 项目共用统一套工程化方案,因为 web 项目不需要使用 rem 和 vw 单位。
2、移动端布局的初衷
可以轻松搞定任意布局
通过设计稿,可以让应用在不同的设备上有完美的体验效果
虽然 rem 和 vw 可以很好完成它们的初衷,不过同时它们也是有代价的(就是它们存在的问题)。那有没有一种方案可以规避掉以上 rem 和 vw 的问题又可以很好的完成初衷哪?
2.1、一个新的 Units 单位(该小节摘自 https://fusion.design/design/…)
DP 为 UI 设计中的唯一可用单位
由于 DP 在不同设备中的叫法不同,且用于描述字号的单位有所不同(如 SP,PT),但其基本计算方法和原则相同且通用,所以在设计过程中,我们考虑到严谨性,统一采用只写数字不带单位的方法书写。
选用 DP 的原因
像素密度 PPI:指每英寸包含的像素 (Px) 个数
如图同一物理尺寸(肉眼所见尺寸)下,低密度显示器的像素个数明显小于高密度显示器的像素个数,所以像素(PX)在多变的设备和分辨率下不是一个稳定可用的单位
与密度无关的像素(DP):设备独立像素
如图,DP 与 PX 的对比可见,DP 可以自适应屏幕的密度,不管屏幕密度怎么变化,实际显示的物理尺寸相同,DP 可以保证物理尺寸的一致性,DP 是目前最适合 UI 设计的单位,同时也是使代码语言相通的尺寸。
转换公式当屏幕的 PPI 为 160 时,1DP=1PX;例:Iphone4,Iphone5,Iphone6,PPI 为 326,在这些屏幕之下 1DP=2PX
DP=(PX*160)/PPIPX=DP*(PPI/160)
切图规则 DP 是与开发代码共用的语言,但一些需要置入的 jpg,png 等图片非矢量,依旧采用 px 作为单位,这个时候我们需要将图片适配到不同 PPI 的屏幕中去。
图示,为一块 banner 适配到不同分辨率屏幕时的像素值:
但实际场景中,无法为各种屏幕做切图适配,我么遵循大图可压缩小,小图不可变大的原则:
【Mobile】选择 3x 图输出,适配于 ios 和 andirod
【Web】选择 2x 的图输出,适配普通屏幕和 retina 屏幕
画布设置规则
【Mobile】选择 375*667 作为绘图尺寸
【Andriod】选择 360*640 作为绘图尺寸
【Web】使用 1440 宽作为绘图尺寸
3、具体实现
主要思路
设计稿中统一使用 dp 作为像素单位,具体规则参考上面的切图规则和画布设计规则
rem 和 vw 多多少少存在各种问题,所以统一使用 px 作为实现单位
web 和 wap 可以使用同一套工程方案
实现设计稿的 dp 到真实应用中 px 的映射关系,并且 px 会随着设备窗口大小的改变而改变
当然,如果稿子是 px 的也可以手动将 px 转换为 dp。
想要实现这个整体方案,核心就在于第 4 条(实现设计稿的 dp 到真实应用中 px 的映射关系),并且这个过程只靠工程化的编译阶段是无法完美解决的,必须和浏览器运行时一起配合工作才能够达成我们的目标。
前提
业务模块的 css 不可以抽离为独立的 css 文件,必须输出在 js 文件中(style-loader 的能力),这样才有改变 css 内容的基本能力。
定义一个尺寸单位 dp,标识这是在设计稿上的尺寸(类似于小程序中的 rpx)。
并不是所有的 px 都需要做弹性转换的,对于需要做弹性转换的容器的 px 统一改为 dp,否则继续使用 px。
假设我们根据 Mobile 设计稿定义一个移动端 H5 的容器元素:
<div class=”box”>
<div class=”tip”>this is tip</div>
</div>
.box {
/* 这里使用的单位为 dp,表示需要根据设备大小进行弹缩 */
width: 100dp;
height: 150dp;
background: red;
}
.box .tip {
/* 使用的单位为 px,不需要根据设备大小进行弹缩。无论设备怎么变化,该元素的宽高都是 10 像素。*/
width: 10px;
height: 10px;
background: blue;
}
最终,元素.box 会根据设备的宽高的改变而改变自身的大小,下方就是.box 元素在不同设备下的宽和高:
设备
宽度
高度
设计稿
100dp
150dp
iPhone 5/SE
85.33px
128px
iPhone 6/7/8
100px
150px
iPhone 6/7/8 Plus
110.4px
165.60px
iPhone X
100px
150px
Galaxy S5
96px
144px
在实现这个功能必须先提供一个转换 dp 为 px 的帮助函数:unitParser。因为接下来的两种方式中都需要这个函数来帮助我们实现最终目的。
const allowMiniPixel = () => {
let allow = false;
if (window.devicePixelRatio && devicePixelRatio >= 2) {
let ele = document.createElement(“div”);
ele.style.border = “.5px solid transparent”;
document.body.appendChild(ele);
allow = 1 === ele.offsetHeight;
document.body.removeChild(ele);
}
return allow;
}();
function unitParser(unit) {
let type = void 0 === unit ? “undefined” : getType(unit);
if (“number” === type) {
unit += “dp”
}
if (“string” !== type) {
return unit;
}
let regExp = /^([\d\.]+)(np|dp)?$/g;
return unit.replace(regExp, (chars, count, suffix) => {
count = Number(count)
switch (suffix) {
case “np”:
// np 不做转换。1np 就是 1px 100np 就是 100px
break;
case “dp”:
default:
// 注意这里 375。说明的上文说了,设计稿是按照 iphone 6 的 375 进行设计的。
// deviceWidth 为屏幕的宽度。iphone 5/SE 为 320、iphone 6/7/ 8 为 375
count = count / 375 * deviceWidth
};
if (!allowMiniPixel && count < 1) {
count = 1
}
return count + “px”;
})
}
3.2.1、方式一:styled-components + css-in-js + JSX
Vue:
import styled from ‘vue-emotion’
import unitParser from ‘./unitParser.js’;
const box = styled(‘div’)`
width: ${unitParser(“100dp”)};
height: ${unitParser(“150dp”)};
background: red;
`
const tip = styled(div)`
width: 10px;
height: 10px;
background: blue;
`
new Vue({
render() {
return (
<box>
<tip>this is a tip</tip>
</box>
)
}
})
react:
import styled from ‘styled-components’;
import unitParser from ‘./unitParser.js’;
const Box = styled.div`
width: ${unitParser(“100dp”)};
height: ${unitParser(“150dp”)};
background: red;
`
const Tip = styled.div`
width: 10px;
height: 10px;
background: red;
`
render(
<Box>
<Tip>this is a tip</Tip>
</Box>
)
注意:使用此方案需要注意和 sass、post-css 等工具的结合使用问题,会增加一定的工程复杂度。另外这种方案会产生大量的元素 style 属性,导致 dom 复杂度增加。
3.2.2、方式二:在浏览器运行时动态计算
自定义一个 webpack 的 css-loader,进行 unitParser 处理。
function styleInject(css, ref) {
if (ref === void 0) ref = {};
var insertAt = ref.insertAt;
if (!css || typeof document === ‘undefined’) {return;}
var head = document.head || document.getElementsByTagName(‘head’)[0];
var style = document.createElement(‘style’);
style.type = ‘text/css’;
if (insertAt === ‘top’) {
if (head.firstChild) {
head.insertBefore(style, head.firstChild);
} else {
head.appendChild(style);
}
} else {
head.appendChild(style);
}
if (style.styleSheet) {
style.styleSheet.cssText = css;
} else {
style.appendChild(document.createTextNode(css));
}
}
var css = “.box {width: ”
+ unitParser(“100dp”)
+ “; height: ”
+ unitParser(“150dp”)
+ “; background: red;} .box .tip {width: 10px; height:10px; background: blue}”;
styleInject(css);
3.2.3、优缺点
优点:
文章开头所提到的 rem 和 vw 都存在众多问题,该方案都可以完美解决。
还可以和其他任何单位混合使用,这意味这使用这种方案的同时还可以使用之前的 rem 和 vw 方式。
缺点:
不管使用方式一还是方式二,都会对项目的工程化复杂度增加,不过和目前处理 rem 以及 vm 的 px2rem 以及 px2viewport 等工具相比,这点复杂度根本不值一提。
增加了 js bundle 文件的体积,减少了 css 文件的体积。不过没有整体的体积没有增加,仅仅是将部分业务的 css 内容搬到了 js 文件里。
最后
借用了大部分工程化的能力,主要还是用来 css in js 的能力
但是这样有个缺点。就是视窗大小改变的时候会出现布局错乱。不过影响不大,因为真机里是不会出现窗口大小改版的。而且这一套方案完全抹平了 web 和 wap 的差异性,在开发层面完全一直。需要缩放就用 dp,不需要就用 np 和 px 作为单位即可
这套方案还是蛮不错的,比 rem 和 vw 好很多。确实属于第三代移动端布局方案了