業務需求分析
簡單來說,需在前端實現搜索圖片的交互。流程來看,無非先本地選中圖片,瀏覽器端用戶可以對圖片進行相應的裁剪,將裁剪后的參數和圖片本身異步上傳到服務器,后臺分析圖片信息,裁剪圖片上傳,確認上傳完畢后,再讓前端以GET的形式跳轉到搜圖結果頁面,完成搜索。本文只討論前端實現。
我的做法 (環境是jQuery)
用戶點擊搜圖按鈕的時候,加載一個全新的Form表單,并且將其隱藏
<form id="my_form" style="display:none"
enctype="multipart/form-data">
<input type="file" name="inputname" class="inputname" id="inputname">
</form>
在同一個回調方法體內,觸發input file 的點擊
$('#inputname').click();
此時用戶瀏覽器將彈出一個資源窗口,選中具體的圖片,點擊確定后將觸發綁定在$('#inputname')
的change事件,事件回調中異步提交表單
$('#my_form').ajaxSubmit();//偽代碼,jQuery原生不支持
關于異步提交帶文件的二進制表單,用jQuery原生并不能很好的實現,因此借助了FormData對象
$.ajax({
url: 'http://yourdomain/upload',
data: new FormData($('#my_form')[0]),
method: 'post',
dataType: 'json',
processData: false,
contentType: false
}).done(function(){
alert('OK');
}).fail(function () {
alert('FAILED');
});
剩下回調后的處理我就不多說,也并非本文的重點。
遇到的問題
既然是IE8填坑之旅,還是應該著重說說坑。理論上來講,支持ES5規范的瀏覽器,在上述流程中都不會出現問題。然而IE8依舊有龐大的市場份額...
IE8的安全機制
問題描述
由于表單是通過change事件回調函數里提交的,而觸發change事件本身并非直接點擊input file按鈕并在資源窗口選擇文件后觸發的,而是間接通過點擊其它DOM結構的回調中觸發了input file 的點擊事件。那么問題來了,IE8可能會把這個當做是非用戶觸發的事件,不允許進行任何表單提交操作。題外話,這個與window.open(url,'_blank')
的限制機制異曲同工。
解決思路
因為之前沒有任何處理IE8的經驗,解決過程比較坎坷。
最簡單的思路是,既然IE8對此有安全限制,那么是否有辦法讓用戶直接點擊input file,直接觸發change事件回調,提交表單?
流程走下來固然可行,可是也有問題,我們都知道要想把input file控件的樣式改成自己想要的樣式,而且無法借助css3的前提下,是非常麻煩的,那么索性,同時也是借鑒淘寶的做法,把整個input file控件做成透明的,把自己預先定義好的按鈕,或是字體,或是圖片,統統放到透明控件下面,這樣用戶看到的是你定義的按鈕或是icon,實際上點擊的則是input file控件本身,沒有半點違和感。
當然這么處理仍然不完美,我們都知道input file在IE8默認樣式分兩個部分,一部分是輸入框,一部分是帶有"瀏覽"字樣的按鈕,點擊瀏覽按鈕可以彈出資源窗口,而輸入框則需要雙擊才能彈出資源窗口,那么如果你自定義的按鈕或者icon比input file中"瀏覽"按鈕大,如何能確保"瀏覽"按鈕尺寸足夠大不出錯呢?這里有一個比較巧妙的辦法,通過font-size
屬性可以把按鈕撐大,然后通過css定位一下確保完全能夠覆蓋自己定義的范圍就可以了。下面給出Demo。
.inputname{
width: 600px;
height: 600px;
position: absolute;
top:0;
right: 0px;
z-index: 9999;
display: block;
cursor: pointer;
font-size: 100px;
-ms-filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=0);
filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=0);
}
FormData不支持IE10以下的瀏覽器
解決思路
這個解決方式就比較簡單了,很多overflowstack網友都推薦了一個輪子jquery.form.js,參考了jquery.form.js的源碼以及其他聲稱自己能夠完美兼容IE8的異步提交帶文件的二進制表單的插件,我發現如果要在不支持FormData屬性的瀏覽器環境中做到異步提交,大家的做法都很統一。
思路大概是表單提交的時候,預先在用戶看不到的地方生成一個空白iframe,并將form的target
屬性設置為該空白iframe的name
。
這樣一來,表單提交后本頁就不會跳轉或刷新,返回的XML、JSON、PLAIN TEXT或是HTML的內容都會在空白iframe里加載出來,只需要開發者預先監聽該iframe的load
事件,并把iframe中加載出來的內容轉換成所需要的數據格式進行判斷就可以完成異步提交了。
//不是jQuery.form.js源碼
iframeObj.bind("load", function () {
var contents = $(this).contents().get(0);
var data = $(contents).find('body').text();
if ('json' == options.dataType) {
data = window.eval('(' + data + ')');
}
options.onSuccess(data);
iframeObj.remove();
form.remove();
iframeObj = null;
});
form.submit();
IE8各種PolyFills
由于各種瀏覽器ES規范不一致,因此就決定了每個項目或多或少都要寫一些必要的PolyFills,同時也是為了獲取更方便的開發體驗。
項目在用的PolyFills(Object.bind函數和Array.filter函數)
/**
* 針對ES兼容性的寫法
*/
module.exports = {
makePolyFill : function(){
if (!Function.prototype.bind) {
Function.prototype.bind = function (oThis) {
if (typeof this !== 'function') {
// closest thing possible to the ECMAScript 5
// internal IsCallable function
throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
}
var aArgs = Array.prototype.slice.call(arguments, 1),
fToBind = this,
fNOP = function () {
},
fBound = function () {
return fToBind.apply(this instanceof fNOP && oThis
? this
: oThis,
aArgs.concat(Array.prototype.slice.call(arguments)));
};
fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();
return fBound;
};
}
if (!Array.prototype.filter) {
Array.prototype.filter = function (fun /*, thisp */) {
"use strict";
if (this === void 0 || this === null)
throw new TypeError();
var t = Object(this);
var len = t.length >>> 0;
if (typeof fun !== "function")
throw new TypeError();
var res = [];
var thisp = arguments[1];
for (var i = 0; i < len; i++) {
if (i in t) {
var val = t[i]; // in case fun mutates this
if (fun.call(thisp, val, i, t))
res.push(val);
}
}
return res;
};
}
}
}
小結
如果沒有耐心,兼容問題根本處理不來。上世紀末到現在,我們經歷了瀏覽器陣營的各種混戰,實際上隨著ES標準不斷更迭,和各廠商的推進努力,瀏覽器兼容的問題相比以前容易處理很多。加之有許多優秀的CSS resets以及瀏覽器特性檢測框架(如Modernizr
),更撫慰了無數前端的小心臟。
參考資料
FormData API;
jQuery.form.js官網;
Github上不錯的PolyFills;
modernizr官網;