为什么要写这篇文章?
最近公司做了很多花里胡哨的H5活动,其实H5页面并不难每个前端都可以写,但细说下来有很多前端细节做的并不是那么完美,其实把H5页面做完善,适配完美也是件挺难的事(至少我觉得是这样),下面我们就来总结下关于H5适配的那些事
说明
为了更好理解此篇文章,你可以先阅读为什么我们常说1px问题而不说2px 对设备独立像素
,css像素
,逻辑像素
,设备像素比
概念有基本的了解
此文是适配系列文章的上
普遍的解决方案
研究之前我们可以看看大厂都是如何适配H5
淘宝
方案: Flexible
分析:值得聊的是,虽然Flexible
是淘宝团队出的关于移动端适配的方案,但手机淘宝似乎并没有使用此方案,可以看下面几张图得出结论
我们现在改变手机型号
可以发现人家的适配单位直接是px
根本没有使用rem
,只不过px
的值是通过手机屏幕的不同动态计算出来的,所以当我们改变苹果的大小时,网站就会刷新
动态计算出对应的px
值,从而达到适配的目的
随便进去一个淘宝的内页,发现使用的适配方案是vw
京东
地址:m.jd.com/
方案:rem
分析:京东的适配比较粗暴,直接使用 媒体查询改变html的根font-size 然后使用rem
进行适配
字节跳动
地址:job.bytedance.com/campus/m/po…
方案:responsive.js
分析: 我觉得responsive.js
和淘宝的Flexible.js
本质上是一个东西,都是动态的改变html
的font-size
然后用rem
进行适配
适配总结
通过这些大厂的产品,我们可以总结到,移动端适配的三种方案
- rem (主流)
- vw/vh (部分)
- 直接px (分场景)
那么?这边文章就这么完了?😶其实这才刚刚开始我们今天的干货
说说Flexible
Flexible
作为移动端适配的鼻祖,非常具有研究价值,并且现在很多的移动端H5都在用这个方案进行适配,今天我们就来学习下他的原理
0.3.2版本
这个版本Flexible
适配原理是通过meta
标签改变页面的缩放比例,从而达到适配的目的,同时,这个方案也可以解决1px
的问题,源码如下
(function(win, lib) {
var doc = win.document;
var docEl = doc.documentElement;
var metaEl = doc.querySelector('meta[name="viewport"]');
var flexibleEl = doc.querySelector('meta[name="flexible"]');
var dpr = 0;
var scale = 0;
var tid;
var flexible = lib.flexible || (lib.flexible = {});
// 如果已经设置<meta name="viewport">属性,就根据当前设置的属性
if (metaEl) {
var match = metaEl
.getAttribute("content")
.match(/initial\-scale=([\d\.]+)/);
if (match) {
scale = parseFloat(match[1]);
dpr = parseInt(1 / scale);
}
} else if (flexibleEl) {
// 同上
var content = flexibleEl.getAttribute("content");
if (content) {
var initialDpr = content.match(/initial\-dpr=([\d\.]+)/);
var maximumDpr = content.match(/maximum\-dpr=([\d\.]+)/);
if (initialDpr) {
dpr = parseFloat(initialDpr[1]);
scale = parseFloat((1 / dpr).toFixed(2));
}
if (maximumDpr) {
dpr = parseFloat(maximumDpr[1]);
scale = parseFloat((1 / dpr).toFixed(2));
}
}
}
if (!dpr && !scale) {
// 这里就是 flexible 的核心代码
var isAndroid = win.navigator.appVersion.match(/android/gi);
var isIPhone = win.navigator.appVersion.match(/iphone/gi);
var devicePixelRatio = win.devicePixelRatio;
if (isIPhone) {
// iOS下,对于2和3的屏,用2倍的方案,其余的用1倍方案
if (devicePixelRatio >= 3 && (!dpr || dpr >= 3)) {
dpr = 3;
} else if (devicePixelRatio >= 2 && (!dpr || dpr >= 2)) {
dpr = 2;
} else {
dpr = 1;
}
} else {
// 其他设备下,仍旧使用1倍的方案
dpr = 1;
}
// 将 <meta> 根据当前设备的 dpr 标签进行缩放
scale = 1 / dpr;
}
docEl.setAttribute("data-dpr", dpr);
// 根据当前的 dpr 自动设置 <meta> 属性
if (!metaEl) {
metaEl = doc.createElement("meta");
metaEl.setAttribute("name", "viewport");
metaEl.setAttribute(
"content",
"initial-scale=" +
scale +
", maximum-scale=" +
scale +
", minimum-scale=" +
scale +
", user-scalable=no"
);
if (docEl.firstElementChild) {
docEl.firstElementChild.appendChild(metaEl);
} else {
var wrap = doc.createElement("div");
wrap.appendChild(metaEl);
doc.write(wrap.innerHTML);
}
}
function refreshRem() {
// 对ipad等设备的兼容
var width = docEl.getBoundingClientRect().width;
if (width / dpr > 540) {
width = 540 * dpr;
}
// 将屏幕10等分,设置 fontSize
var rem = width / 10;
docEl.style.fontSize = rem + "px";
flexible.rem = win.rem = rem;
}
win.addEventListener(
"resize",
function() {
clearTimeout(tid);
tid = setTimeout(refreshRem, 300);
},
false
);
win.addEventListener(
"pageshow",
function(e) {
if (e.persisted) {
clearTimeout(tid);
tid = setTimeout(refreshRem, 300);
}
},
false
);
// 设置字体 12 * dpr
if (doc.readyState === "complete") {
doc.body.style.fontSize = 12 * dpr + "px";
} else {
doc.addEventListener(
"DOMContentLoaded",
function(e) {
doc.body.style.fontSize = 12 * dpr + "px";
},
false
);
}
refreshRem();
flexible.dpr = win.dpr = dpr;
flexible.refreshRem = refreshRem;
// 工具函数
flexible.rem2px = function(d) {
var val = parseFloat(d) * this.rem;
if (typeof d === "string" && d.match(/rem$/)) {
val += "px";
}
return val;
};
flexible.px2rem = function(d) {
var val = parseFloat(d) / this.rem;
if (typeof d === "string" && d.match(/px$/)) {
val += "rem";
}
return val;
};
})(window, window["lib"] || (window["lib"] = {}));
适配效果如下
我们从源码中很容易看出data-dpr
,html的 font-size
,body的font-szie
及meta的缩放比例
是如何计算出来的
下面我们简单看下对dpr
的计算
if (isIPhone) {
// iOS下,对于2和3的屏,用2倍的方案,其余的用1倍方案
if (devicePixelRatio >= 3 && (!dpr || dpr >= 3)) {
dpr = 3;
} else if (devicePixelRatio >= 2 && (!dpr || dpr >= 2)) {
dpr = 2;
} else {
dpr = 1;
}
} else {
// 其他设备下,仍旧使用1倍的方案
dpr = 1;
}
可以看出,在这个版本中,只对ios
的dpr进行了处理,对于安卓
机型都是默认dpr = 1
,显然这样的处理有点不太合理
关于
<meta>
标签这一块,我们可以这样理解,你通过一个镜框(手机屏幕375px宽度)看一篇报纸(页面内容 750px 的宽度) ,此时镜框是紧贴着报纸的,那你通过镜框看到的内容,就只能镜框区域的那些内容,为了能看到全部的内容,就要镜头拉远一些,flexible就是做了以上的事情,然后让我们在写尺寸的时候完全可以按照设计稿来写,也不会帮我们除以对应的dpr的倍数,但是会帮我们把视口拉远了到1/dpr
flexible-2.0
2.0的版本已经没有针对viewport
的缩放了,增加了对0.5px
的判断,源码如下:
(function flexible(window, document) {
var docEl = document.documentElement;
var dpr = window.devicePixelRatio || 1;
// 设置 body 字体
function setBodyFontSize() {
if (document.body) {
document.body.style.fontSize = 12 * dpr + 'px';
} else {
document.addEventListener('DOMContentLoaded', setBodyFontSize);
}
}
setBodyFontSize();
// 设置 rem 基准值
function setRemUnit() {
var rem = docEl.clientWidth / 10;
docEl.style.fontSize = rem + 'px';
}
setRemUnit();
// reset rem unit on page resize
window.addEventListener('resize', setRemUnit);
window.addEventListener('pageshow', function (e) {
if (e.persisted) {
setRemUnit();
}
});
// detect 0.5px supports
if (dpr >= 2) {
var fakeBody = document.createElement('body');
var testElement = document.createElement('div');
testElement.style.border = '.5px solid transparent';
fakeBody.appendChild(testElement);
docEl.appendChild(fakeBody);
if (testElement.offsetHeight === 1) {
docEl.classList.add('hairlines');
}
docEl.removeChild(fakeBody);
}
})(window, document);
我们看对0.5px
问题的处理
if (dpr >= 2) {
var fakeBody = document.createElement('body');
var testElement = document.createElement('div');
testElement.style.border = '.5px solid transparent';
fakeBody.appendChild(testElement);
docEl.appendChild(fakeBody);
if (testElement.offsetHeight === 1) {
docEl.classList.add('hairlines');
}
docEl.removeChild(fakeBody);
}
大概逻辑是,判断设备支不支持0.5px
, 如果支持 就在body
上面添加一个名为hairlines
的class
,所以2我们的代码可以这样写
/* dpr=1的时候*/
.line{
border:1px solid red;
}
/* dpr>=2且支持0.5px的时候*/
.hairlines .line{
border:0.5px solid red;
}
但是这也会出现以下两个问题
- 对于那些
dpr>2 且不支持0.5px
的安卓机,我们应该如何统一处理呢? - 如果
dpr=3
那么border
就应该是0.3333px
而不是0.5px
了,但是flexible
把这些情况都用一个hairlines
包含了
看来 flexible
似乎并不完美,但是我们也不能否认flexible
适配方案, 抛去1px
问题,可以说flexible
是完美的
说说 vw/vh
个人认为vw
适配原理其实和flexible
一样,都是平分窗口,只不过一个分了10
份一个分了100
份
关于vw
我们在下章实战用的时候在具体说明
如何解决1px
问题
我在为什么我们常说1px问题而不说2px文章中提到过,如果对于
UI
要求不高的时候,1px
其实也不算什么问题,往往项目上有很多比1px
更重要的bug需要我们去解决,但了解1px
的本质有助于我们很好的理解移动端适配原理
解决思路
既然1个css像素代表两个物理像素,设备又不认0.5px的写法,那就画1px,然后再想尽各种办法将线宽减少一半。
基于这种思考,我们有以下解决方案
图片大法及背景渐变
这两种方案原理一样,都是设置元素一半有颜色,一半透明,比如做一个2px
高度的图片,其中1px
是我们想要的颜色,1px
设置为透明,适配过程如下
缩放大法
也是flexible 0.3.2
使用的适配方案
我们可以把代码稍微改造下
if (!dpr && !scale) {
var isAndroid = win.navigator.appVersion.match(/android/gi);
var isIPhone = win.navigator.appVersion.match(/iphone/gi);
var devicePixelRatio = win.devicePixelRatio;
// 对于2和3的屏,用2倍的方案,其余的用1倍方案
if (devicePixelRatio >= 3 && (!dpr || dpr >= 3)) {
dpr = 3;
} else if (devicePixelRatio >= 2 && (!dpr || dpr >= 2)) {
dpr = 2;
} else {
dpr = 1;
}
// 将 <meta> 根据当前设备的 dpr 标签进行缩放
scale = 1 / dpr;
}
原理也很简单,根据对应的dpr
调整对应的缩放比例,从而达到适配的目的,直接缩放页面个人感觉有点暴力
使用伪元素缩放
缩放整个页面太暴力
,那能不能只是缩放边框呢,答案肯定是可以的我们不是有 transform: scale
吗
.border1px{
position: relative;
&::after{
position: absolute;
content: '';
background-color: #ddd;
display: block;
width: 100%;
height: 1px;
transform: scale(1, 0.5); /* 进行缩放*/
top: 0;
left: 0;
}
}
总结
本文主要讲解了常见移动端适配及1px
解决方案,本章主要讲的理论,下一章我们会根据实战得出移动端适配的最佳实践
,如有兴趣记得点赞关注哦💓