前言
最近在一個移動端的 Web 專案中踩了很多的坑,感覺有必要把它們記錄下來,分享給即將踏入移動端 Web 開發大門的新人們。
一、從佈局說起
移動端的整體佈局一般來說可以分為上中下三個部分,分別為 header、main、footer,其中header、footer 是固定高度,分別固定在頁面頂部和頁面底部,而 main 是佔據頁面其餘位置,並且可以滾動。
(上圖是使用純 CSS 實現,然後截圖,上傳到專欄有點失真,看官老爺們將就著看吧。)
頁面佈局如下:
1 2 3 4 5 |
<body> <div class="header"></div> <div class="main"></div> <div class="footer"></div> </body> |
根據頁面滾動的位置分為兩種佈局,一種是滾動 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 定位
- absolute 定位
- flex 定位
最容易想到的實現方式是 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 定位後面緊接著可滾動的兄弟節點才會有這個坑,只要讓他的條件有一個不成立就好了,有以下解決方案:
- 讓 fixed 定位節點後面不緊接著可滾動的節點
- 不讓 scroll 節點遮住 fixed 節點
第一種方方案有以下可選方法:
1. 把所有 fixed 節點放在 scroll 元素後面,即把 header 節點放在 main 節點後面
1 2 3 4 5 |
<body> <div class="main"></div> <div class="header"></div> <div class="footer"></div> </body> |
但這樣顯然不太符合一般人的思維習慣,程式碼可讀性降低。
2. 使 main 不可滾動,給 main 巢狀一層可滾動的子節點
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
<body> <div class="header"></div> <div class="main"> <div class="scroll-container"></div> </div> <div class="footer"></div> </body> <style> .main { overflow: hidden; } .scroll-container { height: 100%; overflow: auto; } </style> |
第二種方案有以下可選方法:
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; } |
二、fixed 與 input
剛接觸移動端 Web 開發的小夥伴應該都會聽前輩們說過,不要在有 input 標籤的頁面使用 fixed 定位,因為這兩者在一起的時候,總是會有奇奇怪怪的問題。
在 iOS 上,當點選 input 標籤獲取焦點喚起軟鍵盤的時候,fixed 定位會暫時失效,或者可以理解為變成了 absolute 定位,在含有滾動的頁面,fixed 定位的節點和其他節點一起滾動。
其實這個問題也很好解決,只要保證 fixed 定位的節點的父節點不可滾動,那麼即使 fixed 定位失效,也不會和其他滾動節點一起滾動,影響介面。
但是除此之外,還有很多坑比較難以解決,例如 Android 軟鍵盤喚起後遮擋住 input 標籤,使用者沒法看到自己輸入的字串,iOS 則需要在輸入至少一個字元之後,才能將對應的 input 標籤滾動到合適的位置,所以為了避開這些難以解決的坑,在有表單輸入的頁面,儘量用absolute 或者 flex 替換 fixed。
三、input 的 compositionstart 和 compositionend 事件
在 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 1px border 實現
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); <a href="http://www.jobbole.com/members/s642943041">@if</a> $direct == 'all' { top: 0; left: 0; width: 200%; height: 200%; box-shadow: 0 0 0 1px $border-color; z-index: -1; } <a href="http://www.jobbole.com/members/wx2715401697">@else</a> if $direct == 'left' or $direct == 'right' { #{$direct}: 0; top: 0; width: 0; height: 200%; border-#{$direct}: 1px solid $border-color; } <a href="http://www.jobbole.com/members/wx2715401697">@else</a> { #{$direct}: 0; left: 0; width: 200%; height: 0; border-#{$direct}: 1px solid $border-color; } } } |
總結
以上的坑都是在專案裡頻繁遇到的,每一個都給出了對應的解決方式,但由於筆者也是初入坑移動 Web 開發的新人一枚,所以給出的方案未必是最合適的,做了點微小的工作,希望能為大家提供一點幫助,不足的地方也請大家多多指正。