一開始聽說開發H5,以為就是做適配現代瀏覽器的移動網頁,心想不用管IE了,歐也。到今天,發現當初too young too simple,兼容IE和兼容安卓與IOS,后者讓你更抓狂。接下來數一下踩過的坑。主要分UI展示,鍵盤,輸入框等等。解決bug最苦惱的問題不是沒有解決方案,而是你沒有找到真正的原因。再就是現象難以重現,每次都要發布代碼,然后到手機app中去測試,模擬。這些地方會耗費大量的精力。
一、UI相關
1.安卓4.4以下不支持fixed布局。
fixed布局的作用之一就是在手機鍵盤彈起來的時候,可以自動把頁面頂起來。如果不支持的話,換絕對定位也是可以的。但是絕對定位某些機型比如sm-n7508,華為m7上還是沒有能頂起來。IOS沒有這個問題。
2.小於1px顯示問題
部分安卓機器(比如小米)的分辨率低,如果border的寬度小於1px。安卓機出現一種邊框消失了的現象。樣式上有點奇怪,IOS沒有這個問題。
一開始以為是別的元素擋住了,但是調了半天無解。最后突然意識到是不是計算出來的高度有小數導致的。然后馬上取整:
$target.css("height", Math.ceil(maxline * lineHeight));
但是,華為的某些類型的是上面顯示不正常,出來一排點點。
再修正一下:
$target.css("height", Math.ceil(maxline * lineHeight) - 1);
或者用floor。你好奇為什么會有小數高度呢,因為這個功能設計一個折疊,需要重新計算dom的高度。
二、鍵盤相關
1.ios鍵盤擋住輸入框。
這個發生的頻率很高,中文輸入法或者輸入法切換的時候會遮擋。
解決的辦法如下:
setInterval(function () {
if (document.activeElement.className.indexOf('inputcontent') >= 0) { document.activeElement.scrollIntoViewIfNeeded(); } }, 300);
這是最管用的辦法,inputcontent為輸入框的樣式。activeElement表示獲得焦點的元素。但是這個方法只在app中有用,如果是在瀏覽器中還是會失效。
2.鍵盤收下留下空白陰影。
這個在部分安卓機上比較明顯,當鍵盤在激活狀態,突然來一個模態彈框,很明顯的看到鍵盤收下去之后出現了一個短暫(不到1秒的樣子)的空白,然后頁面再收下去,讓人感覺閃了下。比如華為P7。但是ios上沒有關系,這個問題沒招,系統不流暢。
3.無法保持鍵盤在彈出狀態。
web其實無法直接控制鍵盤,只有通過讓輸入框獲focus來讓手機的鍵盤彈出來,但是三星SM-N8507v 這款就是不聽話。我也無解了。
4.確保彈出來的是數字鍵盤
<input type="number" pattern="[0-9]*" /> <input type="password" pattern="[0-9]*" />
只有number或者tel還是不夠,只有加入正則,ios才會出現九宮格。
三、輸入框相關
1. fastClick 鎖住輸入框。
在ios中,會出現幾秒的輸入框沒有反應,開始也怎么想不明白,各種嘗試,推測,搜索發現原來是使用的輕框架中用到了fastClik引起的,解決的辦法就是加上一個樣式。
<div id="content" class="inputcontent needsclick" ></div>
解決方法到是簡單,但是當初為找到這個原因,費了好大勁,把框架的模塊一個一個刪,最后才定位到。如果不加這個,不單會鎖住輸入框,每次調用focus都會設置光標跑在最前面,無法移動到后面。這個體驗很糟糕。
2.模擬placeholder。
div作為輸入框,本身加入placeholder是無效的。得借助於偽元素。
<div id="content" class="inputcontent needsclick" placeholder="有問題就盡管提問吧" contenteditable="true"></div>
.tools .inputcontent:after { display: inline-block; width: 100%; color: #999; content: attr(placeholder); }
然后在獲得焦點和失去焦點的時候對這個屬性進行移除和添加操作。這樣做的好處在於,分離用戶輸入的內容,如果把placeholder的值直接在寫輸入框中這樣會多一些判斷,讓代碼不太干凈。
3.div的換行是div
在div中觸發換行,會得到一個div。這個就相當於是在編輯器中。而不是我們認為的<br>或者換行符。
所以,需要對div進行過濾替換。
var replace = /<div.*?>(.*?)<\/div>/g; txt = txt.replace(replace, function(a) { if (a != "<br>") { return "<br>" + delHtmlTag(a); } return delHtmlTag(a);}
4.粘貼莫名奇妙帶了樣式
在輸入框中粘貼會以這樣的形式出現:
<font style='font-size:30px;color:#999'><span>粘貼內容</span></font>
不清楚是系統還是app定義的。所以也得過濾。 用戶還可以粘貼其他文章,容易搞亂輸入框中的樣式,需要加上:
.tools .inputcontent *{font-size: 0.50rem !important; color: #000 !important;line-height: 22px !important;font-weight: normal !important;}
因為如果從粘貼事件里面處理的話,容易搞亂焦點,讓焦點在最前面。這樣很不爽。所以還是樣式控制,在發送的時候過濾掉所有的標簽。
5.超出遮擋/換行遮擋
這是一個神奇的bug,當內容超過div的最大高度后,最后一行出現一個神奇的現象,頭兩個字顯示了,后面的內容不見了(快快后面其實有內容),直到下一次換行才會出現。
我alert里面的內容,發現並沒有其他的元素,我不斷的嘗試,發現div overflow: hidden;的時候字都會顯示出來,但是為了讓用戶能夠在內容框里面上下滾動。我得讓y軸是可以滾動的:overflow-y: auto; 所以應該是滾動條引起的。但我確實不知道如何修改。后來試出一個hack的方法。只要有一個換行就不會出現這種情況。所以,我就在用戶輸入到特定行的時候就塞進一個1px的換行:
if ($("#content").height() == 88 && isIOS() && !haveAppendBr) { $("#content").append("<div style='height:1px;border-top:1px solid #fff' ><br></div>"); haveAppendBr = true; }
當然這不是正規的解決辦法,如果園友有遇到相似的劇情,可以賜教一下。
6.安卓第一次不能換行
這個現象是消息發送成功之后,用戶(小米)一來就是點換行,卻無法換行,我懷疑是安卓系統阻止了這樣的行為。但是在輸入一個字之后,換行就是正常的了(哪怕再刪掉這個字)。ios里面沒有這個問題。開始我嘗試去人為加一個換行,又發現焦點沒了。想想這樣問題不改也罷。
7.輸入框光標閃動
這個是translate3d屬性引起,修改sm框架中的一段代碼設置useTranslate為false。位於handleTouchMove方法中。
四、圖片相關
1.安卓不能上傳。
安卓很多時候都不能觸發input type='file'的彈框,上傳就得走原生的幫助。原生攔截到鏈接后就會彈出圖片選擇框。
window.location = 'xxapp:h5Upload({"openType":2,"needChop":false,"uploadUrl": '+fileUploadUrl+',"key":' + totalFiles + ',"callbackMethodName": "uploadComplete"})';
key是為了記住是第幾張圖片,便於在原生上傳結束之后把地址和key一起傳到uploadComplete方法中。這樣界面上才可以做相應的修改。但這不是我說的重點。重點是Url不要帶中文啊或者特殊符號之類的。一方面很多手機里面的圖片命名很奇怪,一大堆,像在uc上面保存下來的圖片后面跟了幾個類型。這種名稱沒啥用,非常建議服務器端像微信一樣處理上傳圖片,成功之后得到一個唯一的字符串就行。不然有的系統會自動解碼你得區分的用上了encodeURI或者encodeURIComponent。測試妹子會說這個手機可以,那個不可以,然后是苦了前端。
2.圖片轉了90度。
安卓部分機型(小米2A,三星N7,三星G9)對於拍照的圖片上傳之后居然左轉了90度。
(非原圖)
我把照片發出來之后,放在桌面上也是歪的,拍攝的圖片本身會帶有一個exifdata,這個對象里面包含了拍攝的時間、方向、曝光時間等一些信息。用exif.js可以看出來。也有網站可以直接查看。
function getdata(id) { EXIF.getData(document.getElementById(id), function () { var data = EXIF.getTag(this, 'Orientation'); console.log(data); }); }
思路就是通過這個方向來判斷是否給圖片來個再旋轉。
this.style.transform = 'rotate(' + current + 'deg)';
實際沒有使用exif.js,因為exif還需要再請求一次,而且這個圖片有驗證,居然還讀不到方向信息。最后讓后端對上傳圖片進行方向旋轉糾正。我自己實現了一個。
public static Bitmap RotateImage(Stream sm) { Image img = Image.FromStream(sm); var exif = img.PropertyItems; byte orien = 0; var item = exif.Where(m => m.Id == 274).ToArray(); if (item.Length > 0) orien = item[0].Value[0]; switch (orien) { case 2: img.RotateFlip(RotateFlipType.RotateNoneFlipX);//horizontal flip break; case 3: img.RotateFlip(RotateFlipType.Rotate180FlipNone);//right-top break; case 4: img.RotateFlip(RotateFlipType.RotateNoneFlipY);//vertical flip break; case 5: img.RotateFlip(RotateFlipType.Rotate90FlipX); break; case 6: img.RotateFlip(RotateFlipType.Rotate90FlipNone);//right-top break; case 7: img.RotateFlip(RotateFlipType.Rotate270FlipX); break; case 8: img.RotateFlip(RotateFlipType.Rotate270FlipNone);//left-bottom break; default: break; } return (Bitmap)img; }
[HttpPost] public ActionResult UploadImg(HttpPostedFileBase file, string dir = "UserImg") { if (CheckImg(file) != "ok") return Json(new { Success = false, Message = "文件格式不對!" }, JsonRequestBehavior.AllowGet); if (file != null) { var path = "~/Content/UploadFiles/" + dir + "/"; var uploadpath = Server.MapPath(path); if (!Directory.Exists(uploadpath)) { Directory.CreateDirectory(uploadpath); } string fileName = Path.GetFileName(file.FileName);// 原始文件名稱 string fileExtension = Path.GetExtension(fileName); // 文件擴展名 //string saveName = Guid.NewGuid() + fileExtension; // 保存文件名稱 這是個好方法。 string saveName = Encrypt.GenerateOrderNumber() + fileExtension; // 保存文件名稱 這是個好方法。 var saveUrl = uploadpath + saveName; // file.SaveAs(saveUrl); System.Drawing.Image image = ImageManageHelper.RotateImage(file.InputStream); image.Save(saveUrl); if (file.ContentLength / 1024 > 500)//大於0.5M { var _saveName = Encrypt.GenerateOrderNumber() + "_thumbnailUrl" + fileExtension; var thumbnailUrl = uploadpath + _saveName; var maxh = 400; var maxw = 400; if (image.Width>maxw) { maxh = 400*image.Height/image.Width; } ImageManageHelper.GetPicThumbnail(saveUrl, thumbnailUrl, maxh, maxw, 90); return Json(new { Success = true, SaveName = "/Content/UploadFiles/Mobile/" +_saveName }); } return Json(new { Success = true, SaveName = "/Content/UploadFiles/Mobile/" + saveName }); } return Json(new { Success = false, Message = "請選擇要上傳的文件!" }, JsonRequestBehavior.AllowGet); }
3.圖片半截
這個問題在安卓上面會有,不是加載的問題。正確的效果圖片應該是垂直居中的。但不知道為什么偶爾的會只出來個半截。而且我發現,給圖片設置百分比,手機和pc不一樣,手機圖片的百分比並不是相對於其父類元素,而是它自己。
所以圖片的寬度會超出其父類,即使<div><img></div>的寬度都是100%。overflow:hidden吧,圖片可能顯示不全。超出的部分會導致用戶可以在圖片上面左右滑動,這在ios中有個搞笑的現象,就是對彈出的圖片不斷的左右滑動,再恢復后居然能讓原先綁定的點擊事件失效,不確定是框架的原因還是系統的原因。當時是用一個模態框改造的。最后干脆自己再寫一個:
Client.modalImg = function (src) { if (!src) return; if ($(".img-overlay").length) { $(".img-overlay").remove(); }; var modal = '<div class="img-overlay"> <div class="imgheader"></div> <div class="imgbox"><img onload="reCalcuImg();" src="' + src + '" /></div>'; $("body").append(modal); }; //校准位置 function reCalcuImg() { var headerH = $(".imgheader").height(); var boxH = $(".imgbox").height(); var imgH = $(".imgbox img").height(); var realMaxH = boxH - headerH; // console.log("headerH", headerH, "boxH", boxH, "imgH", imgH, "realMaxH", realMaxH); if (imgH > realMaxH) { $(".imgbox img").css("height", realMaxH + "px"); console.log("大於最大高度,realMaxH", realMaxH); } else { var gap = (realMaxH - imgH) / 2; // console.log("小於最大高度,margintop",(realMaxH - imgH), gap); $(".imgbox img").css("margin-top", gap + "px"); } }
寫在onload事件結束后是確保圖片已經加載完成。這樣才能計算,如果直接在modalimg中計算,圖片的高度可能為0。然后如果圖片的高度大於最大高度則設置為最大高度,否則的話在進行margin,讓其垂直居中。
現在使用的是photoSwipe插件。需要結合圖片的onload事件先存下圖片的原始寬高。
//圖片加載完成后調用 function imgloading(img,srcoll) { console.log("imgloading", img.height); var cached = { height: img.naturalHeight, width: img.naturalWidth, src: img.src }; imClient.imgCache[img.src] = cached; srcoll&&imClient.scroll(); }
獲取原始寬高是為了顯示時候的比例自然:
//圖片放大 imClient.imgCache = {}; var ispop = false; function getaimg(src) { if (ispop) return; function loadaction(w,h) { imClient.debugSay("圖片加載:w" + w + " h:" + h); loadimg(src, w, h); ispop = false; } var cached = imClient.imgCache[src]; if (cached) { ispop = true; loadaction(cached.height, cached.width); return; } console.log("未加載"); } $(document).on("click", ".bubbleimgright img,.bubbleimgleft img", function (e) { if ($(this).hasClass("msgfailimg")) return; var url = $(this).attr("src");//就放大縮略圖 getaimg(url); }); function loadimg(url, hei, width) { var pswpElement = document.querySelectorAll('.pswp')[0]; var maxH = $("#historylist").height(); var maxW = $("#historylist").width(); var fH = 400; var fW = 400; //如果都比默認的小 if (hei <= maxH && width <= maxW) { fH = hei; fW = width; } if (hei > maxH && width < maxW) { fH = maxH; fW = Math.ceil((maxH * width) / hei); } if (width > maxW) { fW = maxW; fH = Math.ceil((maxW * hei) / width); } // build items array var items = [ { src: url, w: fW, h: fH } ]; // define options (if needed) var options = { index: 0 // start at first slide }; var gallery = new PhotoSwipe(pswpElement, PhotoSwipeUI_Default, items, options); gallery.init(); }
五、消息
1.websocket是脆弱但又頑強的。
websocket很容易受到網絡的影響而中斷,但網絡一恢復能自動重連。而手機會有切到后台運行的這種情況,比如小米系統會在手機黑屏之后把網絡斷掉,用戶進入應用的時候,你得有重連的機制,確保消息沒有遺漏。
socket.on("reconnect", function () { isConneted = true; eventManger.trigger("reconnect"); listenChannel(); });
在listenChannel中通過 socket.emit 告之node后端重連了,拿消息的姿勢調整下。
but紅米手機有一款socketio老是重連,所以手機上也要准備輪詢的機制。重連三次關掉socket,直接輪詢。
2.消息先發后到。
先發的消息后到,這是很有可能的,但是用戶就會奇怪。比如一條長消息分成幾次發,前端可以在前一條發完了再發第二條。
var limit = 1000; function loop(i) { if (i < num) { send(content.substr(i * limit, limit), function () { i++; loop(i); }); } }; loop(0);
六、前端的一些思考
那天走在路上想,前端是個什么樣的職位,會JavaScript,node,熟悉mvvm,html5特性就ok了嗎,其實技能只占到一部分。前端相當於是個連接者,產品經理的想法,美工的設計,后端程序員的接口實現,客戶的期待,測試妹妹的重點,領導巡視的對象,大部分都匯集在前端完成的頁面上,所以溝通,理解能力比較重要。產品經理和測試也會容易太專注於界面效果而忽略整個系統的分配合作。有時候看到界面或者流程不對,可能是后端的問題,你得去推。就像看見一人面有難色,其實體內已經有病了,所以你得會表達,表達問題在哪,或者表達需要怎樣的協助;而在面對bug的時候要清楚哪些是功能性的bug,哪些是體驗性的bug,基本功能必須保障正確,體驗性的問題凡是影響基本功能的使用的都是嚴重的體驗性bug,但bug總是有的,不太可能做到所有機型都一樣。這里面還有時間成本,你解決一個不太重要的bug可能會花上很長的時間;再就是分析思考,各種不正常現象如何找到原因,除了依賴於經驗你要堅信所有妖魔鬼怪都是有出處的,每個界面都可能有自己的差異性,不正常的現象重現的條件是什么,有哪些相關因素,然后順藤摸瓜;再就是代碼能力,盡量保證模塊的獨立和職責的單一,這個方面細節很多,是每個程序員需要注意的地方。最后就是想說,神奇的bug還不止這些,因為缺少這方面的開發經驗和知識儲備所以花了很長時間,遠超自己的預估。文中如有不當或更好的方法還請指出。