原文地址:震驚!一個文件上傳插件居然如此簡單!javascript
請你們多多指教,鞠躬。css
在以前的工做當中,遇到有文件上傳的需求基本都是經過引用插件來實現的,效果是完成了,可是其實並無一個系統的認識,理解比較粗淺。html
魯迅曾經曰過:java
好讀書,也要求甚解。諸葛村夫不求甚解,因此多智也只能近妖。jquery
最近又遇到了相關的需求,在閱讀了Deng mu qin(大神都是這樣的,只留下了一串拼音字符,不帶走一片雲彩~)前輩的upload.js源碼後,以爲可能跟業務比較耦合,通用性相對不是那麼好,因此決定本身擼一個文件上傳的小插件,既當是學習,同時也吸(chao)取(xi)一下前輩的人生經驗。第一次寫技術文章,其實技術性談不上多強,主要是提醒本身要不斷學習、不斷總結,但願之後能成爲一方小牛。真心但願能多多討論,一塊兒進步!git
初來乍到,萌新們可能跟我同樣對FileUpload對象一無所知,無妨,先看一個最簡單的例子:github
<input type="file">
複製代碼
當上面的標籤出如今頁面中時,一個FileUpload對象就會被建立,而後就會出現一個你們熟悉的銀灰色小方塊,點擊選擇文件,出現對應的文件名稱和格式。編程
現代瀏覽器中(IE10 & IE10+),XMLHttpRequest請求能夠傳輸FormData對象,能夠經過XMLHttpRequest對象的upload屬性的onprogress事件回調方法獲取傳輸進度,這也是在下的xupload.js的安生立命之本。至於IE9IE8IE7IE6,emmmm...api
告辭。:running::running:數組
經過一個經典的自執行匿名函數,再將方法註冊到jQuery上,就能夠基本實現一個jq插件的初步創建:
// ;冒號防止上下文中有其餘方法未寫;從而引發沒必要要的麻煩
;(function ($) {
// 建立構造函數Upload
function Upload (config) {
// ...
}
// Upload的原型方法
Upload.prototype = {
// ...
};
// 實例化一個Upload,掛載到jQuery
$.xupload = function (config) {
return new Upload(config)
};
})(jQuery);
複製代碼
一個構造函數須要作些什麼呢?
代碼以下:
function Upload(config) {
var _this = this; // 緩存this
_this.uploading = false; // 設置傳輸狀態初始值
_this.defaultConfig = {
el: null, // {string || jQuery object} 綁定的元素,必填
uploadUrl: null, // {string} 上傳路徑,必填
uploadParams: {}, // {object} 上傳攜帶參數對象,選填
maxSize: null, // {number} 上傳的最大尺寸,選填
autoUpload: false, // {boolean} 是否自動上傳,默認否
noGif: false, // {boolean} 是否支持gif上傳,默認支持
previewWrap: null, // 圖片預覽的容器,選填
previewImgClass: 'x-preview-img', // 預覽圖片的class,previewWrap生效時方可用
start: function () {}, // 開始上傳回調
done: function () {}, // 上傳完成回調
fail: function () {}, // 上傳失敗回調
progress: function () {}, // 上傳進度回調
checkError: function () {}, // 檢測失敗回調
};
_this.fileCached = []; // 上傳文件緩存數組
_this.$root = null; // 掛載元素
// 防止previewImgClass爲null或undefine
if (config.previewImgClass === null || config.previewImgClass === '') {
config.previewImgClass = _this.defaultConfig.previewImgClass; // 置爲默認值
}
// 用戶傳入了配置項且配置項是一個純粹的對象
if (config && $.isPlainObject(config)) {
// 經過jquery的extend方法進行合併
_this.config = $.extend({}, _this.defaultConfig, config);
} else {
_this.config = _this.defaultConfig; // 繼承默認配置項
_this.isDefault = true;
}
_this.init(); // 調用初始化函數
}
複製代碼
prototype在我看來有點相似於class之於css,你能想象若是css中沒有class會發生什麼嗎?可用性和複用性都成了災難,這是絕對不行的。
關於prototype的進一步解讀,你們能夠參考一下方應杭老師的精彩解讀。
想象一下,咱們把一些經常使用的工具方法掛載到prototype上,這樣調用一個實例,這個實例就自動繼承了全部在prototype上的方法,修改一下prototype,全部實例也都自動響應過來,是否是跟css中的class很像呢?
那麼讓咱們來設計一下Upload的原型函數須要哪些基礎的方法吧:
init: function () {
var _this = this,
config = this.config, // 緩存合併後的config
el = config.el,
isEl = _this._isSelector('el'), // 調用_isSector判斷傳入的格式是否符合要求
isPreviewWrap = _this._isSelector('previewWrap'); // 同上
// 拋出異常
if (!isEl) {
throw '請輸入正確格式的el值'
}
if (!isPreviewWrap) {
throw '請輸入正確格式的previewWrap值'
}
_this.$root = $(el); // 將元素賦值,方便後續的調用
_this.$root.each(function () {
$('body').on('change', el, function (e) {
var files = e.target.files;
Array.prototype.push.apply(_this.fileCached, files); // 同以前的深拷貝不一樣,爲了後續的數組操做,咱們應該將僞數組轉化爲真正的數組
_this.handler(e, files); // 調用處理器函數
});
});
},
_isSelector: function (el) {
var which = this.config[el]; // 拿到config裏的屬性
return Object.prototype.toString.call(which) === '[object String]' && which !== '' && !/^[0-9]+.?[0-9]*$/.test(which); // 必須是字符串且不能爲空字符串且是非負整數
}
複製代碼
handler: function (e, files) {
var _this = this,
config = this.config,
fileCached = this.fileCached,
rules = this.validate(files);
if (rules.result) {
config.autoUpload && _this.triggerUpload();
// 暫時只支持圖片預覽
if (_this.$root.attr('accept').substr(0, 5) === 'image') { // 預覽模式暫時只支持圖片,經過判斷accept來判斷(需改進)
_this.previewBefore(); // 調用上傳前函數
}
} else {
_this._checkError(rules.msgQuene); // 驗證結果爲false則觸發_checkError函數
}
}
複製代碼
triggerUpload: function (index) {
var _this = this,
files = this.fileCached,
len = files.length;
var isIndex = (index >= 0); // 判斷是否傳入參數(排除index爲0時的特殊狀況)
var isValid = /^\d+$/.test(index) && index < len; // 判斷傳入的index是否爲整數,切數目不能大於文件個數
if (isIndex && isValid) { // 若是傳入了index參數且驗證經過
if (len > 1) {
_this.upload(files[index]); // 多個文件直接傳入指定index文件
} else if (len === 1) {
_this.upload(files[0]); // 不然傳入第一個
}
} else if (!isIndex && !isValid) { // 若是傳入了沒有傳入index參數且並無驗證經過
if (len > 1) {
_this.upload(files);
} else if (len === 1) {
_this.upload(files[0]);
}
} else if (isIndex && !isValid) { // 若是傳入了index參數且並無驗證經過
throw 'triggerUpload方法傳入的索引值爲從0開始的整數且不得大於您上傳的文件數' // 拋出異常
}
}
複製代碼
upload: function (files) {
var _this = this,
uploadParams = this.config.uploadParams, // 有些時候請求須要攜帶額外的參數
xhr = new XMLHttpRequest(), // 建立一個XMLHttpRequest請求
data = new FormData(), // 建立一個FormData表單對象
fileRequestName = ''; // 文件請求名
// 若是uploadParams有fileRequestName則直接使用,不然爲file[]
uploadParams.fileRequestName ?
fileRequestName = uploadParams.fileRequestName :
fileRequestName = 'file[]';
// 多文件上傳處理
for (var i = 0, len = files.length; i < len; i++) {
var file = files[i];
// 將fileappend到FormData對象
data.append(fileRequestName, file);
}
// 參數處理
if (uploadParams) {
for (var key in uploadParams) {
// 忽略fileRequestName
if (key !== 'fileRequestName') {
// 將各個參數append到FormData
data.append(key, uploadParams[key]);
}
}
}
// 上傳開始
xhr.onloadstart = function (e) {
_this._loadStart(e, xhr); // 調用_loadStart函數
};
// 上傳結束
xhr.onload = function (e) {
_this._loaded(e, xhr); // 同上
}
// 上傳錯誤
xhr.onerror = function (e) {
_this._loadFailed(e, xhr); // 同上
};
// 上傳進度
xhr.upload.onprogress = function (e) {
_this._loadProgress(e, xhr); // 同上
}
xhr.open('post', _this.config.uploadUrl); // post到uploadUrl
xhr.send(data); // 發送請求
}
複製代碼
previewBefore: function () {
var _this = this,
files = _this.fileCached,
filesNeed = [], // 咱們真正須要的file數組,防止往頁面裏屢次append以前存在的dom
filesHad = [], // 已經存在的file數組,方便後續計算
previewWrap = _this.config.previewWrap,
previewImgClass = _this.config.previewImgClass;
var $previewWrap = $(previewWrap);
// 若是已經存在預覽位置,即頁面中已經存在了預覽元素
if ($previewWrap.find('.' + previewImgClass).length > 0) {
$previewWrap.find('.' + previewImgClass).each(function (index, value) {
var $this = $(this);
filesHad.push($this.data('name')); // 把已經存在的file name推入filesHad
});
for (var i = 0; i < files.length; i++) {
if (filesHad.indexOf(files[i].name) < 0) { // 數組的去重
filesNeed.push(files[i]);
}
}
} else {
filesNeed = files; // 首次預覽不須要處理
}
for (var i = 0; i < filesNeed.length; i++) {
(function (i) { // 建立一個閉包獲取正確的i值
var reader = new FileReader(); // 新建一個FileReader對象
reader.readAsDataURL(filesNeed[i]); // 獲取該file的base64
reader.onload = function () {
var dataUrl = reader.result; // 獲取url
var img = $('<img src="' + dataUrl + '" class="' + previewImgClass + '" data-name="' + filesNeed[i].name + '"/>');
img.appendTo($previewWrap);
};
})(i);
}
}
複製代碼
delBefore: function (index) {
var _this = this,
files = this.fileCached,
len = files.length,
previewWrap = _this.config.previewWrap;
previewImgClass = _this.config.previewImgClass;
var isIndex = (index >= 0); // 判斷是否傳入參數(排除index爲0時的特殊狀況)
var isValid = /^\d+$/.test(index) && index < len; // 判斷傳入的index是否爲整數,且數目不能大於文件個數
if (isIndex && isValid) {
files.splice(index, 1); // 刪除數組中指定file
$(previewWrap).find('.' + previewImgClass).eq(index).remove();
} else if (!isIndex && !isValid) {
$(previewWrap).find('.' + previewImgClass).each(function () { // 刪除全部
$(this).remove();
})
} else if (isIndex && !isValid) {
throw 'delBefore方法傳入的索引值爲從0開始的整數且不得大於您上傳的文件數' // 拋出異常
}
}
複製代碼
// 開始上傳
_loadStart: function (e, xhr) {
this.uploading = true;
this.config.start.call(this, xhr);
},
// 上傳完成
_loaded: function (e, xhr) {
// 簡單的判斷一下請求成功與否
if (xhr.status === 200 || xhr.status === 304) {
this.uploading = false;
var res = JSON.parse(xhr.responseText);
this.config.done.call(this, res);
} else {
this._loadFailed(e, xhr);
}
},
// 上傳失敗
_loadFailed: function (e, xhr) {
this.uploading = false;
this.config.fail.call(this, xhr);
},
// 上傳進度
_loadProgress: function (e, xhr) {
// e.loaded爲當前加載值,e.total爲文件大小值
if (e.lengthComputable) {
this.config.progress.call(this, e.loaded, e.total);
}
},
// 驗證失敗
_checkError: function (msgQuene) {
// msgQuene爲錯誤消息隊列
this.config.checkError.call(this, msgQuene);
},
複製代碼
validate: function (files) {
var _this = this,
len = files.length,
msgQuene = [], // 建立一個錯誤消息隊列,由於多文件上傳可能有多個錯誤狀態
matchCount = 0; // 建立一個初始值匹配值方便後續計算
if (len > 1) {
for (var i = 0; i < len; i++) {
// 建立一個閉包
(function (index) {
// 參看下面的rules方法
var result = _this.rules(files[index], index);
// 根據rules計算返回的flag進行計數,正確則+1s,不然把錯誤消息推送到消息隊列
result.flag ? matchCount++ : msgQuene.push(result.msg);
})(i);
}
} else {
// 原理同上
var result = _this.rules(files[0]);
result.flag ? matchCount++ : msgQuene.push(result.msg);
}
// 當全部文件都經過validate
if (matchCount === len) {
return {
result: true // 告訴別人經過啦!
};
} else {
return {
result: false, // 告訴別人我以爲不行
msgQuene: msgQuene // 告訴別人哪裏不行
};
}
}
複製代碼
rules: function (item, index) {
var config = this.config,
flag = true,
msg = '';
// 一些暫時想到的驗證規則方案,只作參考
// 是否能傳gif
if (config.noGif) {
if (item.type === 'image/gif') {
flag = false;
msg = '不支持上傳gif格式的圖片'
}
}
// 是否設置了大小限制
if (config.maxSize) {
if (item.size > config.maxSize) {
flag = false;
// index = 0 隱式轉換爲false,這裏須要注意
index >= 0 ?
msg = '第' + (index + 1) + '個文件過大,請從新上傳':
msg = '文件過大,請從新上傳';
}
}
// 返回一個參考對象
return {
flag: flag,
msg: msg
};
}
複製代碼
get: function () {
return this.fileCached; // 這時候緩存值就有用啦
},
set: function (files) {
this.fileCached = files; // 簡單的處理下...
}
複製代碼
var up = $.xupload({
el: '#file', // || $('#file')
uploadUrl: '/test',
uploadParams: {
fileRequestName: 'uploadfile', // || undefined
param1: 1,
param2, 2
},
autoUpload: false, // || true,
maxSize: 2000,
noGif: true, // || false
start: function (files) {
console.dir(files);
},
done: function (res) {
console.dir(res); // 上傳成功responce
},
fail: function (error) {
console.error(error);
},
progress: function (loaded, total) {
console.log(Math.round(loaded / total * 100) + '%');
},
checkError: function (errors) {
console.error(errors); // 獲得驗證失敗數組
}
});
$('#someSubmitBtn').click(function () {
var files = up.get(); // 獲取待上傳的文件
console.dir(files);
up.triggerUpload(); // 觸發異步upload, autoUpload爲false時可用
});
複製代碼
第一次寫相似的插件,運用的技巧比較簡單粗淺,也有不少不足,已經在計劃改進了,大牛輕噴,之後會更加努力的(ง •̀_•́)ง。
雖然看到這篇文章的人可能很少,可是劉備也曾經曰過:
勿以善小而不爲
我這叫作「善」好像也有點牽強...總之就是那麼個意思!
emmm...好像也沒啥說的了,你們都是面向工資編程,那就祝你們早日一晚上暴富吧。
代碼是什麼,能吃嗎?