移动端特性
设备像素 (device pixels)
也称为物理像素,显示器的最小物理单位。这里需要注意,一个像素并不一定是一个小正方形区块,也没有标准的宽高,只是用于显示丰富色彩的一个“点”而已。可以参考公园里的景观变色彩灯,一个彩灯(物理像素)由红、蓝、绿小灯组成,三盏小灯不同的亮度混合出各种色彩。设备独立像素(device independent pixels)
独立于设备的像素。比如我们偶尔会说“电脑屏幕在 2560x1600分辨率下不适合玩游戏,我们把它调为 1440x900”,这里的“分辨率”(非严谨说法)指的就是设备独立像素。可以通过 window.screen.width/ window.screen.height 查看。 另外,平时我们所说的 iphoneX的逻辑分辨率375 x 812指的就是设备独立像素。chrome检查元素模拟调试手机设备时显示如375x667和 320x480都是设备独立像素。
一个设备独立像素里可能包含1个或者多个物理像素点,包含的越多则屏幕看起来越清晰。
像素分辨率
以手机屏幕为例,iphonex像素分辨率为1125x2436,是指屏幕横向能显示1125个物理像素点,纵向能显示2436个物理像素点。通常说的4K显示屏指的是 4096x2160。
PPI (pix per inch)
每英寸的物理像素数。以尺寸为5.8英寸(屏幕对角线长度)、分辨率为1125x2436的iphonex为例, ppi = Math.sqrt(11251125 + 24362436) / 5.8 ,值为 463ppi
CSS像素
浏览器使用的单位,用来精确度量网页上的内容,比如 div { width: 100px; }。 在一般情况下(页面缩放比为1),1个CSS像素 等于 1个设备独立像素。比如,假设把屏幕独立像素分辨率设置为1440x900,给页面元素设置为宽度720px,则视觉上元素的宽度是屏幕宽度的一半。这也解释了为什么当我们把独立像素分辨率调高后网页的文字感觉变小了。
当页面缩放比不为1时,CSS像素和设备独立像素不再对应。比如当页面放大200%,则1个CSS像素等于4个设备独立像素。
devicePixelRatio
window.devicePixelRatio指的是设备物理像素和设备独立像素(device-independent pixels, dips)的比例。window.devicePixelRatio = 物理像素 / 设备独立像素(dips) 。经计算, iphonex的 devicePixelRatio 是3。
viewport
尺寸的区别
-
屏幕的尺寸:这里获取的宽高是设备独立像素值。
console.log( screen.width )
console.log( screen.width )
-
窗口尺寸:这里用的是CSS像素
console.log( window.innerWidth )
console.log( window.innerHeight )
viewport的概念
对于页面上一个div元素,我们给他设置 width: 10%,这个10%是相对于谁?最终是多宽?
<html>
<body>
<div></div>
</body>
</html>
div是body的10%, body和html等宽,html窗口等宽。
[图片上传失败...(image-d2ada-1615192672515)]
header的宽度==body的宽度==html的宽度==viewport(窗口)的宽度, 当窗口宽度小于main的宽度时,导致header的宽度小于main的宽度。
测量viewport
使用 document.documentElement.clientWidth和document.documentElement.clientHeight来测量视窗宽高
document.documentElement.clientWidth和screen.width看起来差不多,唯一的区别是前者不包含滚动条宽度,而后者包含。
测量html元素
使用document.documentElement.offsetWidth 和 -Height.
移动端的viewport
移动端的问题
屏幕窄,一般来说设备独立像素不超过400px。
比如把网站侧边栏宽度设置为10%,这在PC浏览器看起来没问题(一般视窗大于1000px),但在手机上就只有40px,太小了什么都放不下。
visual viewport 和 layout viewport
我们把layout viewport想象为一个很大的照片,把visual viewport想象为在一张板子上开了的矩形口子。板子在照片上移动时,透过口子(visual viewport) 可以看到照片(layout viewport)的其中部分。
- layout viewport是页面渲染时所参考的宽高
- visual viewport 是移动端视窗的设备独立像素宽高
在移动端,页面在渲染时以layout viewport宽度作为计算标准。以iphone为例,其 layout viewport 宽度是980px,页面元素渲染时就认为窗口为980px宽。对于如下两个buton,在Iphone Safari上.button-1的宽度为为.button-2的1/10。渲染后在整体缩小(user-scale)页面,让页面宽度正好完全填充屏幕。
.button-1 {
width: 98px;
}
.button-2 {
width: 100%;
}
测量layout viewport
document.documentElement.clientWidth 和 -Height来获取layout viewport 的宽高
屏幕尺寸
viewport设置
假设有如下页面,没设置viewport。对于大部分浏览器,会以980px宽度去渲染,然后页面会缩小到正好被屏幕容纳为止。此时页面上元素看起来很小。如下图所示:
部分浏览器可能不去做自动缩放,用户需要左右滑动页面才能看到所有内容,如下图:
现在添加 <meta name="viewport" content="width=device-width">,假设在IphoneX上预览,其device-width就是其设备独立像素,即375px。
最终效果如下,看起来好多了。
一个完整的viewport设置范例
<meta name="viewport" content="width=device-width, minimum-scale=0.5, initial-scale=1, maximum-scale=2">
适配方案之媒体查询
开发流程
适配原则
- 开发时方便
- 适配的设备类型多
- 让用户无不适感
思路1
以原型稿的宽度和标准开发页面,在手机上整个页面内容等比放大或者缩小填充手机屏幕宽度
思路2
以原型稿的宽度和标准开发页面,在手机上部分内容根据屏幕宽度等比放大或者缩小,而部分内容不变或者按需受控变化
需要随屏幕宽度等比缩放的元素用相对单位 rem或者vw,不需要随屏幕宽度缩放的元素用固定单位px
思路3
固定尺寸+弹性布局,无需放大缩小
方案一:viewport缩放适配,统一等比缩放
原理
开发流程
- 开发者拿到设计稿(假设设计稿尺寸为750px,设计稿的元素标是基于此宽度标注)
- 开始开发,css以设计稿标注的尺寸来开发,比如页面元素字体标注的大小是32px
- 在html的head里引入适配代码,这段代码其实就只在做适配,做一个等比缩放,实现一个效果<meta name="viewport" content="width=750px, initial-scale=0.5">,而页面中那段代码就是在找不同的手机对应的缩放比例initial-scale,750是设计稿的尺寸
- 缺陷:统一的放大缩小,一些地方我们可能并不想等比缩放。最大的缺点在边框边线上,不同尺寸下,边线的粗细是不一样的,屏幕越大,边线越粗。
代码
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>viewport 等比放大</title>
<meta name="viewport" content="width=device-width">
<script>
const WIDTH = 750
const mobileAdapter = () => {
let scale = screen.width/WIDTH
let content = `width=${WIDTH}, initial-scale=${scale}, maximum-scale=${scale}, minimum-scale=${scale}`
let meta = document.querySelector('meta[name=viewport]')
if(!meta) {
meta = document.createElement('meta')
meta.setAttribute('name', 'viewport')
document.head.appendChild(meta)
}
meta.setAttribute('content', content)
}
mobileAdapter()
window.onorientationchange = mobileAdapter
</script>
</head>
<body>
<div id="app">
<section>
<div class="avatar"></div>
<div class="detail">
hello
</div>
</section>
</div>
<style>
* {
margin: 0;
padding: 0;
}
body {
font-size: 32px;
}
#app {
background: #eee;
}
section {
display: flex;
}
.avatar {
width: 200px;
height: 200px;
background: grey;
border: 1px solid red;
}
.detail {
padding: 10px;
background: lightgrey;
flex: 1;
}
</style>
</body>
</html>
实战
设计稿
开发页面
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>viewport缩放实战</title>
<script>
const WIDTH = 750
const mobileAdapter = () => {
let scale = screen.width/WIDTH
let content = `width=${WIDTH}, initial-scale=${scale}, maximum-scale=${scale}, minimum-scale=${scale}`
let meta = document.querySelector('meta[name=viewport]')
if(!meta) {
meta = document.createElement('meta')
meta.setAttribute('name', 'viewport')
document.head.appendChild(meta)
}
meta.setAttribute('content', content)
}
mobileAdapter()
window.onorientationchange = mobileAdapter
</script>
</head>
<body>
<header>
<span class="active" target="hot">热门</span>
<span target="focus">关注</span>
</header>
<main>
<section id="hot">
<figure>
<img src="http://cdnimg103.lizhi.fm//studio/2019/05/10/2736467140436393014.jpg" alt="">
<h2>举例0.01m<span class="topic">#点歌#</span></h2>
<div class="author">
<img src="http://cdnimg103.lizhi.fm/user/2019/05/28/2739819480224172034_80x80.jpg" alt="头像">
<span>爱尚独家记忆</span>
</div>
</figure>
<figure>
<img src="http://cdnimg103.lizhi.fm/studio/2019/05/28/2739736615570232886.jpg" alt="">
<h2>音乐与你同在<span class="topic">#点歌#</span></h2>
<div class="author">
<img src="http://cdnimg103.lizhi.fm/user/2019/05/28/2739819480224172034_80x80.jpg" alt="头像">
<span>爱尚独家记忆</span>
</div>
</figure>
<figure>
<img src="http://cdnimg103.lizhi.fm//studio/2019/05/10/2736467140436393014.jpg" alt="">
<h2>举例0.01m<span class="topic">#点歌#</span></h2>
<div class="author">
<img src="http://cdnimg103.lizhi.fm/user/2019/05/28/2739819480224172034_80x80.jpg" alt="头像">
<span>爱尚独家记忆</span>
</div>
</figure>
</section>
<section id="focus"></section>
</main>
<style>
body, figure {
margin: 0;
}
body {
color: #fff;
background: url(https://appweb.lizhi.fm/assets/images/liveShare/bg.jpg) center center no-repeat;
background-size: cover;
min-height: 100vh;
}
header {
font-size: 28px;
display: flex;
padding: 0 20px;
text-align: center;
}
header > span {
flex: 1;
line-height: 84px;
color: rgba(255, 255, 255, 0.3);
}
header > span.active {
color: #fff;
border-bottom: 2px solid rgba(255, 255, 255, 0.3);
}
main {
padding: 0 30px;
}
main figure {
padding: 32px 0;
border-bottom: 2px solid rgba(255, 255, 255, 0.1);
}
main figure > img {
width: 680px;
height: 420px;
object-fit: cover;
border-radius: 8px;
}
main figure > h2 {
font-size: 40px;
}
main figure .topic {
color: rgba(255, 255, 255, 0.3)
}
main .author {
display: flex;
align-items: center;
}
main .author img {
width: 64px;
height: 64px;
object-fit: cover;
border-radius: 64px;
}
main .author span {
color: rgba(255, 255, 255, 0.3);
margin-left: 20px;
}
</style>
</body>
</html>
方案二:vw适配,选择性缩放
原理
开发流程
- 开发者拿到设计稿(假设设计稿尺寸为750px,设计稿的元素标是基于此宽度标注)
- meta进行设置,<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1">,这样就可以将layout viewport与visual viewport设为等宽
- 开始开发,对设计稿的标注进行转换,把px换成vw。比如页面元素字体标注的大小是32px,换成vw为 (100/750)*32 vw
- 对于需要等比缩放的元素,CSS使用转换后的单位
- 对于不需要缩放的元素,比如边框阴影,使用固定单位px
- 这里使用了一个新的语法,叫CSS自定义属性,很类似于SASS里面的变量,不过这个是原生的
代码
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>viewport缩放实战</title>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1">
<script>
const WIDTH = 750
document.documentElement.style.setProperty('--width', (100/WIDTH));
</script>
</head>
<body>
<header>
<span class="active" target="hot">热门</span>
<span target="focus">关注</span>
</header>
<main>
<section id="hot">
<figure>
<img src="http://cdnimg103.lizhi.fm//studio/2019/05/10/2736467140436393014.jpg" alt="">
<h2>举例0.01m<span class="topic">#点歌#</span></h2>
<div class="author">
<img src="http://cdnimg103.lizhi.fm/user/2019/05/28/2739819480224172034_80x80.jpg" alt="头像">
<span>爱尚独家记忆</span>
</div>
</figure>
<figure>
<img src="http://cdnimg103.lizhi.fm/studio/2019/05/28/2739736615570232886.jpg" alt="">
<h2>音乐与你同在<span class="topic">#点歌#</span></h2>
<div class="author">
<img src="http://cdnimg103.lizhi.fm/user/2019/05/28/2739819480224172034_80x80.jpg" alt="头像">
<span>爱尚独家记忆</span>
</div>
</figure>
<figure>
<img src="http://cdnimg103.lizhi.fm//studio/2019/05/10/2736467140436393014.jpg" alt="">
<h2>举例0.01m<span class="topic">#点歌#</span></h2>
<div class="author">
<img src="http://cdnimg103.lizhi.fm/user/2019/05/28/2739819480224172034_80x80.jpg" alt="头像">
<span>爱尚独家记忆</span>
</div>
</figure>
</section>
<section id="focus"></section>
</main>
<style>
:root {
--color: red;
}
body, figure {
margin: 0;
}
body {
color: #fff;
background: url(https://appweb.lizhi.fm/assets/images/liveShare/bg.jpg) center center no-repeat;
background-size: cover;
min-height: 100vh;
}
header {
font-size: calc(28vw * var(--width));
display: flex;
padding: 0 calc(20vw * var(--width));
text-align: center;
}
header > span {
flex: 1;
line-height: calc(84vw * var(--width));
color: rgba(255, 255, 255, 0.3);
}
header > span.active {
color: #fff;
border-bottom: 2px solid rgba(255, 255, 255, 0.3);
}
main {
padding: 0 calc(30vw * var(--width));
}
main figure {
padding: calc(32vw * var(--width)) 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
main figure > img {
width: calc(680vw * var(--width));
height: calc(420vw * var(--width));
object-fit: cover;
border-radius: 8px;
}
main figure > h2 {
font-size: calc(40vw * var(--width));
}
main figure .topic {
color: rgba(255, 255, 255, 0.3)
}
main .author {
display: flex;
align-items: center;
}
main .author img {
width: calc(64vw * var(--width));
height: calc(64vw * var(--width));
object-fit: cover;
border-radius: calc(64vw * var(--width));
}
main .author span {
color: rgba(255, 255, 255, 0.3);
margin-left: calc(20vw * var(--width));
font-size: calc(32vw * var(--width));
}
</style>
</body>
</html>
代码解析:
- viewport设置 <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1"> 。把layout viewport 宽度设置为设备宽度,不需要缩放
- 用js定义css的自定义属性--width,对应的是把设计稿分成100份,每份的像素数。
- 根据设计稿标注设置样式, 比如标注稿里元素宽度为20px。这这里设置 calc(20px * var(--width))
- 对于不需要等比缩放的元素,比如边框,可以直接使用固定单位px即可。比如border-bottom: 1px solid #ccc
使用sass
@function px2rem($px) {
@return $px * 1rem / 100;
}
body, figure {
margin: 0
}
body {
color: #fff;
background: url(https://appweb.lizhi.fm/assets/images/liveShare/bg.jpg) center center no-repeat;
background-size: cover;
min-height: 100vh;
}
header {
font-size: px2rem(28);
display: flex;
padding: 0 px2rem(20);
text-align: center;
}
header > span {
flex: 1;
line-height: px2rem(84);
color: rgba(255, 255, 255, 0.3);
}
header > span.active {
color: #fff;
border-bottom: 2px solid rgba(255, 255, 255, 0.3);
}
main {
padding: 0 px2rem(30);
}
main figure {
padding: px2rem(32) 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
main figure > img {
width: px2rem(680);
height: px2rem(420);
object-fit: cover;
border-radius: 8px;
}
main figure > h2 {
font-size: px2rem(40);
}
main figure .topic {
color: rgba(255, 255, 255, 0.3)
}
main .author {
display: flex;
align-items: center;
}
main .author img {
width: px2rem(64);
height: px2rem(64);
object-fit: cover;
border-radius: px2rem(64);
}
main .author span {
color: rgba(255, 255, 255, 0.3);
margin-left: px2rem(20);
font-size: px2rem(32);
}
方案三:适配方案之动态rem
原理
rem是什么
rem和em单位一样,都是一个相对单位,不同的是em是相对于父元素的font-size进行计算,rem是相对于根元素html的font-size进行计算的,这样一来rem就完美的绕开了复杂的层级关系,实现了类似em单位的功能。默认情况下,浏览器给的字体大小是16px,按照转化关系16px = 1rem。
咦,都是固定了根元素是默认16px了,如何设备不同的移动设备呢?
问题提出也是答案所在,我们动态改变根元素html的字体默认大小不就行了嘛!
使用rem布局的时候,为了兼容不同的分辨率,我们应该要动态的修正根字体的大小,让所有的用rem单位的子元素跟着一起缩放,从而达到自适应的效果。
开发流程
- 开发者拿到设计稿(假设设计稿尺寸为750px,设计稿的元素标是基于此宽度标注)
- 开始开发,对设计稿的标注进行转换
- 对于需要等比缩放的元素,CSS使用转换后的单位
- 对于不需要缩放的元素,比如边框阴影,使用固定单位px
代码
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title> 动态rem缩放实战</title>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1">
<script>
const WIDTH = 750 //设计稿尺寸
const setView = () => {
console.log(screen.width)
document.documentElement.style.fontSize = (100*screen.width/WIDTH) + 'px'
}
window.onorientationchange = setView
setView()
</script>
</head>
<body>
<header>
<span class="active" target="hot">热门</span>
<span target="focus">关注</span>
</header>
<main>
<section id="hot">
<figure>
<img src="http://cdnimg103.lizhi.fm//studio/2019/05/10/2736467140436393014.jpg" alt="">
<h2>举例0.01m<span class="topic">#点歌#</span></h2>
<div class="author">
<img src="http://cdnimg103.lizhi.fm/user/2019/05/28/2739819480224172034_80x80.jpg" alt="头像">
<span>爱尚独家记忆</span>
</div>
</figure>
<figure>
<img src="http://cdnimg103.lizhi.fm/studio/2019/05/28/2739736615570232886.jpg" alt="">
<h2>音乐与你同在<span class="topic">#点歌#</span></h2>
<div class="author">
<img src="http://cdnimg103.lizhi.fm/user/2019/05/28/2739819480224172034_80x80.jpg" alt="头像">
<span>爱尚独家记忆</span>
</div>
</figure>
<figure>
<img src="http://cdnimg103.lizhi.fm//studio/2019/05/10/2736467140436393014.jpg" alt="">
<h2>举例0.01m<span class="topic">#点歌#</span></h2>
<div class="author">
<img src="http://cdnimg103.lizhi.fm/user/2019/05/28/2739819480224172034_80x80.jpg" alt="头像">
<span>爱尚独家记忆</span>
</div>
</figure>
</section>
<section id="focus"></section>
</main>
<style>
}
html {
/*font-size: 100 * window.innerWidth / 750*/
}
body, figure {
margin: 0;
}
body {
color: #fff;
background: url(https://appweb.lizhi.fm/assets/images/liveShare/bg.jpg) center center no-repeat;
background-size: cover;
min-height: 100vh;
}
header {
font-size: .28rem;
display: flex;
padding: 0 .20rem;
text-align: center;
}
header > span {
flex: 1;
line-height: .80rem;
color: rgba(255, 255, 255, 0.3);
}
header > span.active {
color: #fff;
border-bottom: 2px solid rgba(255, 255, 255, 0.3);
}
main {
padding: 0 .30rem;
}
main figure {
padding: .32rem 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
main figure > img {
width: 6.80rem;
height: 4.20rem;
object-fit: cover;
border-radius: 8px;
}
main figure > h2 {
font-size: .40rem;
}
main figure .topic {
color: rgba(255, 255, 255, 0.3)
}
main .author {
display: flex;
align-items: center;
}
main .author img {
width: .64rem;
height: .64rem;
object-fit: cover;
border-radius: .64rem;
}
main .author span {
color: rgba(255, 255, 255, 0.3);
margin-left: .20rem;
font-size: .32rem;
}
</style>
</body>
</html>
代码解析:
- viewport设置 <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1"> 。把layout viewport 宽度设置为设备宽度,不需要缩放
- 用js定义html的fontSize。假设设计稿的宽度和屏幕宽度相同,我们设置html的fontSize为100px,对应设计稿的20px即为0.2rem。若设计稿的尺寸和屏幕尺寸不同,那让html的fontSize为 100*屏幕宽度/设计稿宽度,此时设计稿中的20px仍然对应0.2rem。
- 对于不需要等比缩放的元素,比如边框,可以直接使用固定单位px即可。比如border-bottom: 1px solid #ccc
使用sass
@function px2rem($px) {
@return $px * 1rem / 100;
}
body, figure {
margin: 0
}
body {
color: #fff;
background: url(https://appweb.lizhi.fm/assets/images/liveShare/bg.jpg) center center no-repeat;
background-size: cover;
min-height: 100vh;
}
header {
font-size: px2rem(28);
display: flex;
padding: 0 px2rem(20);
text-align: center;
}
header > span {
flex: 1;
line-height: px2rem(84);
color: rgba(255, 255, 255, 0.3);
}
header > span.active {
color: #fff;
border-bottom: 2px solid rgba(255, 255, 255, 0.3);
}
main {
padding: 0 px2rem(30);
}
main figure {
padding: px2rem(32) 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
main figure > img {
width: px2rem(680);
height: px2rem(420);
object-fit: cover;
border-radius: 8px;
}
main figure > h2 {
font-size: px2rem(40);
}
main figure .topic {
color: rgba(255, 255, 255, 0.3)
}
main .author {
display: flex;
align-items: center;
}
main .author img {
width: px2rem(64);
height: px2rem(64);
object-fit: cover;
border-radius: px2rem(64);
}
main .author span {
color: rgba(255, 255, 255, 0.3);
margin-left: px2rem(20);
font-size: px2rem(32);
}
方案四:弹性盒适配
原理
不需要等比缩放,页面元素使用固定单位。使用弹性盒(flex)做布局,可结合媒体查询调整文字大小。
代码
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>jirengu.com</title>
<meta name="viewport" content="width=device-width">
</head>
<body>
<section>
<img src="http://cdn.jirengu.com/book.jirengu.com/img/22.jpg">
<div class="detail">
<h2>雪山</h2>
<p>雪山的介绍</p>
</div>
</section>
<section>
<img src="http://cdn.jirengu.com/book.jirengu.com/img/20.jpg">
<div class="detail">
<h2>深林</h2>
<p>深林的介绍深林的介绍深林的介绍深林的介绍</p>
</div>
</section>
<section>
<img src="http://cdn.jirengu.com/book.jirengu.com/img/19.jpg">
<div class="detail">
<h2>相框</h2>
<p>我是相框啊我是相框啊我是相框啊我是相框啊我是相框啊我是相框啊我是相框啊</p>
</div>
</section>
<section>
<img src="http://cdn.jirengu.com/book.jirengu.com/img/18.jpg">
<div class="detail">
<h2>人</h2>
<p>人的介绍</p>
</div>
</section>
<style>
section {
display: flex;
padding: 10px 0;
margin: 10px;
border-bottom: 1px solid #ccc;
background: #fff;
}
section > img {
width: 60px;
height: 60px;
border-radius: 50%;
}
section .detail {
margin-left: 10px;
flex: 1;
}
section .detail h2 {
font-size: 20px;
margin-top: 10px;
}
section .detail p {
font-size: 14px;
color: #676767;
}
</style>
</body>
</html>
总结:不是所有页面都需要特殊适配,有的是需要用布局就可以了。有的页面属于一个一个方块状,难以控制到底多少,可以做缩放等特殊适配;有的时候需要高度固定,那么得转变成用vh;
300ms延迟
现象
以下是测试延迟的代码,可新建页面在手机端查看效果
<!DOCTYPE html>
<html>
<head>
<meta name="description" content="300ms延迟演示,手机上打开连接,注释掉viewport" />
<meta charset="utf-8">
<title>300ms延迟</title>
<!--
<meta name="viewport" content="width=device-width">
-->
</head>
<body>
<div id="delay">click有延迟 </div>
<div id="no-delay">touchstart无延迟</div>
<div> <a id="link1" href="#1">链接1</a> <a id="link2" href="#2">链接1</a></div>
<div id="log"></div>
<style>
body {
font-size: 60px;
}
</style>
<script>
const $ = s => document.querySelector(s)
const log = str => $('#log').innerText = str
let t1, t2
$('#delay').ontouchstart = e => {
t1 = Date.now()
}
$('#delay').onclick = e => {
log(Date.now() - t1)
}
$('#no-delay').ontouchstart = e => {
log('touchstart无延迟')
}
$('#link1').ontouchstart = e => {
t2 = Date.now()
}
$('#link2').ontouchstart = e => {
t2 = Date.now()
}
window.onhashchange = () => {
log(`link ${location.hash} : ${Date.now() - t2}ms`)
}
</script>
</body>
</html>
延迟的原因
iphone是无键盘全触屏手机的始祖,当时的网站都是为大屏幕设备所设计的(没有设置<meta name="viewport">),为了便于用户阅读浏览器引入了双击缩放的功能。手指在屏幕上快速点击两次,iOS 自带的 Safari 浏览器会将网页缩放至原始比例。那如何判断用户快速点击两次呢?假定用户点击后300ms内如果有第二次点击,就认为用户的目的是双击缩放而不是普通的点击页面元素。所以用户第一次点击一个链接后,必须等300ms,浏览器才能决定是否是跳转还是缩放页面。 后来全触屏手机流行后,其他设备浏览器也开始效仿。
如何解决
方法1、设置meta
<meta name="viewport" content="width=device-width">
经过测试,在android手机只要添加name=viewport的meta即可消除延迟, 在ios上必须设置width=device-width。
其他方法
fastclick库,或者不用click用touchstart
fastclick的原理和简单实现
什么时候使用
- 当移动端因为一些原因未使用 <meta name="viewport" content="width=device-width">的时候使用。
- 2017年以前使用
如何使用
document.addEventListener('DOMContentLoaded', function() {
FastClick.attach(document.body)
}, false)
经测试,对于绑定的click事件和点击链接,无300ms延迟。
fastclick 原理
在检测到touchend事件的时候,会通过DOM自定义事件立即出发模拟一个click事件,并把浏览器在300ms之后真正的click事件阻止掉
实现一个简单的fastclick
const FastClick = (function(){
function attach(root) {
let targetElement = null
root.addEventListener('touchstart', function () {
targetElement = event.target
})
root.addEventListener('touchend', function (event) {
event.preventDefault()
let touch = event.changedTouches[0]
let clickEvent = document.createEvent('MouseEvents')
clickEvent.initMouseEvent('click', true, true, window, 1, touch.screenX, touch.screenY, touch.clientX, touch.clientY, false, false, false, false, 0, null)
clickEvent.forwardedTouchEvent = true
targetElement.dispatchEvent(clickEvent)
})
}
return { attach }
})()
FastClick.attach(document.body)
点击穿透现象和规避方式
什么是点击穿透现象
在移动端,当用户通过绑定touchstar事件监听函数让浮层关闭时,关闭后浮层后面对应位置页面其他元素也被点击,比如浮层的关闭按钮下是一个链接,当用户点击浮层关闭按钮浮层消失后大约300ms页面同时发生跳转。
现象演示
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>tap击穿</title>
<style>
body {
margin: 0;
padding: 30px;
font-size: 60px;
}
.mask {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
background: rgba(0,0,0,0.8);
color: #fff;
padding: 30px;
}
.close {
background: red;
padding: 10px;
}
</style>
</head>
<body>
<!--<a href="https://jirengu.com">饥人谷</a>-->
<span class="bg">饥人谷</span>
<p class="log">0</p>
<div class="mask">
<span class="close">X</span>
</div>
<script>
const $ = s => document.querySelector(s)
const log = str => $('.log').innerText = str
let i = 1
$('.close').ontouchstart = (e) => {
$('.mask').style.display = 'none'
log('touched ' + (++i))
}
$('.bg').onclick = () => {
log('点击了背景')
}
</script>
</body>
</html>
原因分析
前面讲过触屏设备为了区分用户双击缩放,对click做了300ms延迟触发,因此用户在移动端触屏设备的操作流程以及事件触发为:
- 手指触摸屏幕到屏幕,触发 touchstart
- 手指在屏幕短暂停留(如果是移动,触发 touchmove)
- 手指离开屏幕, 触发 touchend
- 等待约300ms,看用户在此时间内是否再次触摸屏幕。如果没有
- 300ms后 在用户手指离开的位置触发 click事件
击穿的根源在第4步。当用户用户点击关闭让遮罩隐藏后,浏览器在300ms后在原来用户手指离开的位置触发click事件,此时遮罩不存在了,自然“点击”到后面的元素。
解决方法
方法1
设置<meta name="viewport" content="device-width"> 。表面上看起来解决了300ms延时,就能解决穿透。实际上这种方法不可行。因为click事件是在touchend事件之后,更晚于touchstart(间隔了60~100ms),如果在touchstart 里绑定关闭了浮层,约100ms后 click事件触发的时候仍然会引发问题。
方法2
关闭浮层用click,不用touchstart。 可行。因为浮层关闭监听的是click,当浮层消失后不会再在原来的位置再冒出个click
方法3
浮层关闭的时间延长。可行。当用户触发touchstart关闭浮层时,浮层可以使用渐变消失(可设置浮层关闭动画在300ms以上),此刻click事件在原来的位置触发时点击的还是浮层,而不是浮层后面的链接
方法4
在ontouchstart 里阻止默认事件。可行。 这样可以阻止click的触发
1px实现
为什么要实现1px
当我们在css里写 border: 1px solid #000的时候,用户会觉得边线依然很粗不美观。如何实现比1px更细的边线呢?下面列举了几种方案。
现象演示
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>1px</title>
<meta name="viewport" content="width=device-width">
</head>
<body>
<div class="item item-1px">列表1</div>
<div class="item item-0_5px">列表2</div>
<div class="item item-css3transform">列表3</div>
<div class="item item-background">列表5</div>
<div class="item item-4-border">列表6</div>
<style>
.item {
padding: 10px 0
}
.item-1px {
border-bottom: 1px solid #ccc;
}
.item-0_5px {
border-bottom: .5px solid #ccc;
/*ios有效 android无效*/
}
.item-css3transform::after {
content: '';
display: block;
height: 1px;
transform: scaleY(0.5);
background: #ccc;
position: relative;
top: 10px;
}
.item-background::after {
display: block;
content: '';
height: 1px;
background: linear-gradient(0deg, #ccc 50%, transparent 50%);
position: relative;
top: 10px;
}
.item-4-border {
margin: 10px 0;
padding: 10px;
background: linear-gradient(to bottom, #ccc .5px, transparent 0), linear-gradient(to left, #ccc .5px, transparent 0), linear-gradient(to right, #ccc .5px, transparent 0), linear-gradient(to top, #ccc .5px, transparent 0);
}
</style>
</body>
</html>
方案总结
方案1, viewport缩放
前面讲过viewport适配的方案,适配的原理是整个页面放大缩小,当然也包括边线
方案2, 设置0.5px
ios有效,部分安卓机型无效。 慢慢等吧,总有一天(可能一两年后)安卓会生效
方案3,使用CSS3 transform,给伪元素缩小尺寸
都有效,缺点是一条边框还好,如果是4条边框两个伪元素不够得新增标签
方案4,使用背景线性渐变
方便,兼容性好。
其他方案
背景图片(麻烦、兼容性不好),svg背景图片(麻烦)