首發我的部落格:Android Hybrid App四大坑
首先解釋下題目,Hybrid App,混合應用,代表平臺PhoneGap,一般指使用原生包裝Web頁面開發的應用。與原生應用相比,主要使用者介面和業務邏輯都是用Web技術也就是HTML+CSS+Javascript實現的;與Web應用相比,Web部分打包在應用內部,使用時不需要網路。
順便說一句,很多解決方案其實不算Hybrid,比如Adobe AIR、Titanium、Mono,這些都是使用某一特定技術開發跨平臺應用的工具,最終產品都是編譯成原生來跑的。
我們沒有選擇PhoenGap為技術基礎(我對此並不滿意,我認為以PhoneGap為基礎可以少走一些彎路,少花一些精力,還能產出很多有價值的副產品),而是自行開發原生框架,主要目標平臺是Android——嗯,就是那個從系統版本到模組組合都巨分散的Android,可以這麼說,坎坷從立項的那一刻起就已經註定了……接下來,便請聽我一一講述:Android Hybrid App四大坑。(此文主要針對Android 4.3及之前的webview,部分瀏覽器比如Chrome已經改善了具體實現,所以Web App其實環境不錯。)
前端程式碼開源就好,https://github.com/Dianjoy/gamepop,要跑起來需要修改config.js,把if (debug) {}
的內容刪掉。
缺少標準flexbox
Flexbox是CSS3裡面一項非常重要的改進,大大改善了佈局工作。可惜從草案到終案時間跨度太長,於是市面上絕大部分裝置只支援display:-webkit-box
。這給上圖中圖示區域的佈局帶來了難處,最終只能混合使用兩種佈局,display:inline-block
和display:flex
。需要注意,inline-block
元素之間,如果在程式碼中有空格換行符的話,渲染時會有約0.25em
~0.5em
大小的間隙,所以想一行4列每列width:25%
的話會放不下導致折行。開始我在其父元素中設定font:0/0 a
,大部分手機都OK,中興ZTE795就不行,擦,上次也是它……後來我索性把所有空格和換行符都幹掉,終於OK了。
這段程式碼演示inline-block佈局時父元素字型對子元素之間間歇的影響。
<iframe src="http://jsfiddle.net/meathill/9Uk5Y/embedded/result,html,css,js/presentation/" height="200" width="100%" allowfullscreen="allowfullscreen" frameborder="0"></iframe>所以,對於多行多列的grid佈局,我們要謹記:
對父元素使用display:flex;
flex-direction: row;
flex-wrap: wrap;
在現代瀏覽器和Android4.4以上的系統中,可以取得完美表現。
使用display:inline-block;
box-sizing: border-box;
vertical-align: top;
,並且在程式碼中移除塊與塊之間的空白字元,避免換行,保留合適的邊距。
遺憾的是,低版本的webview中,高度不會自適應,除非使用js進行計算,不然還是儘量避免在元素下方畫線的舉動。
tap
vs click
(這個問題簡直令人髮指。)我們知道,很多瀏覽器預設行為都依賴click
事件觸發,比如超連結、input[type=radio]
的選中,等等。在4.4之前的webview中,click
比真實操作延遲約300ms觸發,會帶給使用者明顯的延滯感。為了提升使用者體驗,我們多數使用tap
事件部分替代click
事件,以便及時響應使用者操作。可選方案很多,Hammer.js、Fastclick,甚至Zepto都有封裝。
於是新的問題出現了。比如我們用<a>
實現了一個刪除按鈕,tap
時,刪除當前元素,並且將按鈕重置為下載按鈕,href
為下載的url。用PC開發時一切正常,但到真機測試就會發現,因為tap
是即時的,新按鈕立刻替換了舊按鈕,300ms後,click
事件在新按鈕上觸發了,結果又開始下載……再比如,圖層中有個後退按鈕,點選後,圖層移除,露出下面覆蓋的部分,如果在tap
的位置上剛好有一枚連結,此時就會觸發click
,頁面跳轉……以及,上次總結說的下載連結不觸發click
,問題也一樣,只不過翻過來,click
該觸發的時候,被圖層擋住了,所以沒有觸發。
解決方案基本圍繞“如何熬過300ms”,和“甄別事件物件”來設計。
還是上面兩個場景,第一個,按下之後,setTimeout
400ms(以防萬一)後再替換按鈕;第二個,給圖層加一個消失的動畫,持續400ms,保證click
觸發時圖層還在。可能大家有疑問,為何不乾脆禁掉click
事件,前面說了,很多瀏覽器預設行為依賴click
事件,禁掉之後又會引發別的問題。
另一個解決方案是甄別事件物件,在使用者touchstart
的時候,記下event.target
,等到click
觸發時,對比,如果不一致則認為是抽風,event.preventDefault()
。我更推薦這種做法,畢竟400ms也不算短。不過要注意,<label>
在響應click
時,會自動觸發它for
的元素的click
,所以要用新增的control
屬性進行二次對比,以免誤傷。
position:relative;
竟然搶事件
這個問題解決起來容易,排查時沒少花時間。表現為,一個按鈕,就在那裡,但怎麼點都沒用。後來只好追蹤全域性事件,發現事件的物件並非按鈕,而是按鈕下面被遮蓋的層。很奇怪,我知道translateZ
可能導致這個問題,不過現在完全沒用到;而且下面的圖層實際並沒有那麼長(參照上圖中,內容列表和下面的三個按鈕,就是那裡),列表的底部是bottom:60px
,剛好應該把<footer>
的三個按鈕空出來。
然後我開始懷疑position:relative;
,因為只有這個東西會影響佈局。把它點掉後,按鈕果然可以點了,於是我給<footer>
也加上了position:relative;
問題就這樣解決了。我覺得這肯定又是個Android Webview的Bug,因為我並沒有給下面層裡的元素設定z-index
,只有position:relative
。總之,如果按鈕點選沒反應,儘早看看事件物件是哪個,說不定被哪個層搶走了。
各種待實現的API
caniuse.com是個神器,我在使用某個API或者CSS屬性時不太有把握的話,都會去查一下。然而,誰又知道Android Webview預設很多API都沒開呢?大到localStorage
,小到alert
、confirm
,都需要主動開放,不然就沒法用。測試的時候又很難查,原生的環境多不在我這兒,配也不太容易,大家的習慣都不一致,只能等對方反饋過來錯誤資訊,我到程式碼裡查,但常常死活看不出來哪兒有問題。
後來認清現實就好辦,Android Webview其實是個半殘廢,有不明白的問題,多半是API實現不全導致的。最簡單的辦法是用modernizr之類的工具進行特性檢測,或者在Weinre、Adobe Inspect CC裡直接敲API。總之,別懷疑自己,基本都是Android Webview的問題。
這也是我為什麼希望以PhoneGap為基礎的原因:PhoneGap已經按照規範實現了大部分API。外掛庫也很豐富,比如GA——說到這個,我們想實現一個簡單的統計,因為老闆嫌GA太大,所以乾脆使用同樣的介面,只請求一下我們的日誌伺服器,留下條日誌就好,結果我們驚訝地發現:Android裡原生給Webview環境增加API,方法的引數不能多也不能少,不然就報錯,真奇葩——可以省下很多時間。產出的結果,還可以分享到社群一部分,對於我們公司,也能增加了不少開發者資源。
總結
早先別人鄙視Hybrid App的時候我也各種不服,心說“你們丫懂毛啊就指手畫腳的說不行”;現在經歷了各種大坑小坑連環坑之後,我的想法已經變成了“你們丫懂毛啊不就是一不小心蒙對了麼”。
Hybrid App開發會遭遇比原生和Web App更多的坎坷,這我有心理準備;如此多的坑無形中也在提升我的個人價值,我對此甚至隱隱有些高興。不過我還是希望這段混亂之治儘快過去,Android平臺不要再分裂了,高版本系統儘快普及,所有前端開發的日子都會好過許多。
現在,如果可能的話,儘量以PhoneGap為基礎進行開發,能省很多時間和精力。對於前端人來說,更是能把測試環境搬到本地,實在是非常大的幫助。
Android 4.4時再次分裂,之後使用blink作為webview的基礎;之前則是webkit,前者無論CSS還是API都有更佳表現,Nexus 5基本沒怎麼改就都測試通過了。所以將來老羅的錘子手機開賣我一定買一臺支援他,因為他良心的採用4.4為基礎,對前端真是個難得的好訊息。