最近在一个移动端的 Web 项目中踩了很多的坑,感觉有必要把它们记录下来,分享给即将踏入移动端 Web 开发大门的新人们。
移动端的整体布局一般来说可以分为上中下三个部分,分别为 header、main、footer,其中header、footer 是固定高度,分别固定在页面顶部和页面底部,而 main 是占据页面其余位置,并且可以滚动。
(上图是使用纯 CSS 实现,然后截图,上传到专栏有点失真,看官老爷们将就着看吧。)
页面布局如下:
1
2
3
4
5
|
class
=
"header"
>
class
=
"main"
>
class
=
"footer"
>
|
根据页面滚动的位置分为两种布局,一种是滚动 body,另一种是固定 body 的高度为100%,在 main 中滚动。
第一种布局有个优点,就是页面的地址栏会随着 body 的滚动隐藏起来,并且 Android 设备中,滚动 body 会更加的流畅,如果项目中有类似需求可以考虑。
实现布局的方式如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
body
{
overflow
:
auto
;
}
.header,
.footer
{
position
:
fixed
;
left
:
0
;
right
:
0
;
height
:
44px
;
}
.header
{
top
:
0
;
}
.footer
{
bottom
:
0
;
}
.main
{
height
:
100%
;
padding
:
44px
0
;
}
|
第一种情况比较适合长列表页面,整个页面除了 header 和 footer 之外都需要滚动,但很多时候,我们只希望页面的某个元素滚动,这个时候,就采取第二种布局方式。
这种页面布局有三种相对简单的实现方式:
最容易想到的实现方式是 fixed 定位,实现方式如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
html, body
{
height
:
100%
;
overflow
:
hidden
;
}
.header,
.footer
{
position
:
fixed
;
left
:
0
;
right
:
0
;
height
:
44px
;
}
.header
{
top
:
0
;
}
.footer
{
bottom
:
0
;
}
.main
{
height
:
100%
;
padding
:
44px
0
;
box-sizing
:
border-box
;
}
|
fixed 定位实现起来简单,在大多数浏览器中也能正常显示,但是 fixed 定位在移动端会有兼容性问题,后面会提到,所以不建议这种实现方式。
absolute 定位和 fixed 定位类似,只要把 header 的 footer 的 position 改为 absolute 就可以了。
细心的小伙伴可能发现了,这里的 main 没有设置 overflow ,因为这里有一个坑,不管是absolute 定位还是 fixed 定位都一样,为了方便描述,以下只说 fixed 定位(在 absolute 定位也一样成立)。在PC端没有问题,但是在移动端,如果 main 设置了 overflow 为 true,header 会被 main 遮住,对,没有错,虽然是 fixed 定位,但是在移动端,如果 fixed 定位节点后面紧接跟着的兄弟节点是可滚动的(也就是设置了 overflow 为 true ),那么 fixed 节点会被其后的兄弟节点遮住。
这个问题解决方式有很多,既然是 fixed 定位后面紧接着可滚动的兄弟节点才会有这个坑,只要让他的条件有一个不成立就好了,有以下解决方案:
第一种方方案有以下可选方法:
1. 把所有 fixed 节点放在 scroll 元素后面,即把 header 节点放在 main 节点后面
1
2
3
4
5
|
class
=
"main"
>
class
=
"header"
>
class
=
"footer"
>
|
但这样显然不太符合一般人的思维习惯,代码可读性降低。
2. 使 main 不可滚动,给 main 嵌套一层可滚动的子节点
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
class
=
"header"
>
class
=
"main"
>
class
=
"scroll-container"
>
class
=
"footer"
>
.main
{
overflow
:
hidden
;
}
.scroll-container
{
height
:
100%
;
overflow
:
auto
;
}
|
第二种方案有以下可选方法:
1. 让 scroll 节点不与 fixed 节点有重合
1
2
3
4
5
6
7
8
|
body
{
padding
:
44px
0
;
}
.main
{
padding
:
0
;
}
|
2. 给 fixed 节点设置 z-index
1
2
3
4
|
.header,
.footer
{
z-index
:
8888
;
}
|
看到这里可能会有小伙伴觉得,一个简单的布局,还要绕过这么多坑,难道没有简单的方式吗,答案当然是肯定的,那就是第三种实现方式,flex 布局。flex 定位在移动端兼容到了 iOS 7.1+,Android 4.4+,如果使用 autoprefixer 等工具还可以降级为旧版本的 flexbox ,可以兼容到 iOS 3.2 和 Android 2.1。而且用 flex 实现起来相对简单,在各个浏览器里表现也相对一致。实现如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
body
{
display
:
flex
;
flex-direction
:
column
;
}
.main
{
flex
:
1
;
overflow
:
auto
;
-webkit-overflow-scrolling
:
touch
;
}
.header
{
height
:
44px
;
}
.footer
{
height
:
44px
;
}
|
刚接触移动端 Web 开发的小伙伴应该都会听前辈们说过,不要在有 input 标签的页面使用 fixed 定位,因为这两者在一起的时候,总是会有奇奇怪怪的问题。
在 iOS 上,当点击 input 标签获取焦点唤起软键盘的时候,fixed 定位会暂时失效,或者可以理解为变成了 absolute 定位,在含有滚动的页面,fixed 定位的节点和其他节点一起滚动。
其实这个问题也很好解决,只要保证 fixed 定位的节点的父节点不可滚动,那么即使 fixed 定位失效,也不会和其他滚动节点一起滚动,影响界面。
但是除此之外,还有很多坑比较难以解决,例如 Android 软键盘唤起后遮挡住 input 标签,用户没法看到自己输入的字符串,iOS 则需要在输入至少一个字符之后,才能将对应的 input 标签滚动到合适的位置,所以为了避开这些难以解决的坑,在有表单输入的页面,尽量用absolute 或者 flex 替换 fixed。
在 Web 开发中,经常要对表单元素的输入进行限制,比如说不允许输入特殊字符,标点。通常我们会监听 input 事件:
1
2
3
4
5
|
inputElement
.
addEventListener
(
'input'
,
function
(
event
)
{
let
regex
=
/[^1-9a-zA-Z]/g
;
event
.
target
.
value
=
event
.
target
.
value
.
replace
(
regex
,
''
)
;
event
.
returnValue
=
false
}
)
;
|
这段代码在 Android 上是没有问题的,但是在 iOS 中,input 事件会截断非直接输入,什么是非直接输入呢,在我们输入汉字的时候,比如说「喜茶」,中间过程中会输入拼音,每次输入一个字母都会触发 input 事件,然而在没有点选候选字或者点击「选定」按钮前,都属于非直接输入。
所以输入「喜茶」两个字,会触发6次 input 事件,如果把每次 input 的 value 打印出来,结果如下:
这显然不是我们想要的结果,我们希望在直接输入之后才触发 input 事件,这就需要引出我要说的两个事件—— compositionstart 和 compositionend。
compositionstart 事件在用户开始进行非直接输入的时候触发,而在非直接输入结束,也即用户点选候选词或者点击「选定」按钮之后,会触发 compositionend 事件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
var
inputLock
=
false
;
function
do
(
inputElement
)
{
var
regex
=
/[^1-9a-zA-Z]/g
;
inputElement
.
value
=
inputElement
.
value
.
replace
(
regex
,
''
)
;
}
inputElement
.
addEventListener
(
'compositionstart'
,
function
(
)
{
inputLock
=
true
;
}
)
;
inputElement
.
addEventListener
(
'compositionend'
,
function
(
event
)
{
inputLock
=
false
;
do
(
event
.
target
)
;
}
)
inputElement
.
addEventListener
(
'input'
,
function
(
event
)
{
if
(
!
inputLock
)
{
do
(
event
.
target
)
;
event
.
returnValue
=
false
;
}
}
)
;
|
添加一个 inputLock 变量,当用户未完成直接输入前,inputLock 为 true,不触发 input 事件中的逻辑,当用户完成有效输入之后,inputLock 设置为 false,触发 input 事件的逻辑。这里需要注意的一点是,compositionend 事件是在 input 事件后触发的,所以在 compositionend事件触发时,也要调用 input 事件处理逻辑。
iOS设备上,由于retina屏的原因,1px 的 border 会显示成两个物理像素,所以看起来会感觉很粗,这是一个移动端开发常见的问题。解决方案有很多,但都有自己的优缺点。
0.5px border
从iOS 8开始,iOS 浏览器支持 0.5px 的 border,但是在 Android 上是不支持的,0.5px 会被认为是 0px,所以这种方法,兼容性是很差的。
背景渐变
CSS3 有了渐变背景,可以通过渐变背景实现 1px 的 border,实现原理是设置 1px 的渐变背景,50% 有颜色,50% 是透明的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
@mixin commonStyle() {
background-size: 100% 1px,1px 100% ,100% 1px, 1px 100%;
background-repeat
:
no-repeat
;
background-position
:
top,
right
top,
bottom,
left
top
;
}
@mixin border($border-color) {
@include commonStyle();
background-image
:
linear-gradient
(
180deg,
$border-color,
$border-color
50%,
transparent
50%
)
,
linear-gradient
(
270deg,
$border-color,
$border-color
50%,
transparent
50%
)
,
linear-gradient
(
0deg,
$border-color,
$border-color
50%,
transparent
50%
)
,
linear-gradient
(
90deg,
$border-color,
$border-color
50%,
transparent
50%
)
;
}
|
这种方法虽然可行,但是没有办法实现圆角。
伪类 + transform
这类方法的实现原理是用伪元素的 box-shadow 或 border 实现 border,然后用 transform缩小到原来的一半。即使有圆角的需求也能很好的实现。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
@mixin hairline-common($border-radius) {
position: relative;
z-index
:
0
;
&:before
{
position
:
absolute
;
content
:
''
;
border-radius
:
$border-radius
;
box-sizing
:
border-box
;
transform-origin
:
0
0
;
}
}
@mixin hairline($direct: 'all', $border-color: #ccc, $border-radius: 0) {
@include hairline-common($border-radius);
&:before
{
transform
:
scale
(
.5
)
;
top
:
0
;
left
:
0
;
width
:
200%
;
height
:
200%
;
box-shadow
:
0
0
0
1px
$border-color
;
z-index
:
-1
;
}
href
="http://www.jobbole.com/members/wx2715401697">@else if $direct == 'left' or $direct == 'right'
{
#
{
$direct
}
:
0
;
top
:
0
;
width
:
0
;
height
:
200%
;
border-#
{
$direct
}
:
1px
solid
$border-color
;
#
{
$direct
}
:
0
;
left
:
0
;
width
:
200%
;
height
:
0
;
border-#
{
$direct
}
:
1px
solid
$border-color
;
}
}
}
|
以上的坑都是在项目里频繁遇到的,每一个都给出了对应的解决方式,但由于笔者也是初入坑移动 Web 开发的新人一枚,所以给出的方案未必是最合适的,做了点微小的工作,希望能为大家提供一点帮助,不足的地方也请大家多多指正。