關於文件上傳那些可能不怎麼對的姿式

原文地址:震驚!一個文件上傳插件居然如此簡單!javascript

請你們多多指教,鞠躬。css

背景

在以前的工做當中,遇到有文件上傳的需求基本都是經過引用插件來實現的,效果是完成了,可是其實並無一個系統的認識,理解比較粗淺。html

魯迅曾經曰過:java

好讀書,也要求甚解。諸葛村夫不求甚解,因此多智也只能近妖。jquery

最近又遇到了相關的需求,在閱讀了Deng mu qin(大神都是這樣的,只留下了一串拼音字符,不帶走一片雲彩~)前輩的upload.js源碼後,以爲可能跟業務比較耦合,通用性相對不是那麼好,因此決定本身擼一個文件上傳的小插件,既當是學習,同時也吸(chao)取(xi)一下前輩的人生經驗。第一次寫技術文章,其實技術性談不上多強,主要是提醒本身要不斷學習、不斷總結,但願之後能成爲一方小牛。真心但願能多多討論,一塊兒進步!git

一些熱身準備

FileUpload對象

初來乍到,萌新們可能跟我同樣對FileUpload對象一無所知,無妨,先看一個最簡單的例子:github

<input type="file">
複製代碼

當上面的標籤出如今頁面中時,一個FileUpload對象就會被建立,而後就會出現一個你們熟悉的銀灰色小方塊,點擊選擇文件,出現對應的文件名稱和格式。編程

XMLHttpRequest請求

現代瀏覽器中(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);
複製代碼

代碼解析

Upload構造函數

一個構造函數須要作些什麼呢?

  1. 經過掛載到this的方式,初始化一些後續須要使用到的變量,此過程能夠視後續代碼須要不斷增量更新
  2. 配置一個defaultConfig默認配置項,在用戶直接調用xupload方法時直接使用配置項,固然,當用戶傳遞屬於本身的配置項時,須要將用戶配置項跟默認配置項進行更新合併,此時能夠用到jQuery的extend函數
  3. 調用初始化函數

代碼以下:

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初始化函數,在這裏調用必須用到的方法。 仔細想一想,一個上傳插件,第一步最須要的是否是響應用戶選擇文件的操做呢?再進一步,頁面中是否只有一個上傳input?
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,去負責接下來具體的邏輯,好比規則的驗證、圖片預覽等等
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,可以自動或者手動的執行接下來的上傳操做,而後再多思考一步,用戶會不會只想上傳其中某一個文件呢?這是徹底有可能的,因此咱們得提供多一種思路,這裏咱們可使用「函數重載」,當用戶不傳值時,則默認所有上傳,若是傳入了指定的index值,則單獨上傳該文件,之因此帶引號,是由於確實只是經過簡單的參數去實現的,更高級的函數重載,能夠參考jQuery之父John Resig利用閉包巧妙實現的重載 譯文
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了,須要這樣一個函數去處理上傳的POST請求,同時暴露出一些狀態函數,好比onloadstart、onerror等等
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吧。首先應該明確的是須要一個預覽容器,否則圖片不知道改放哪;接着圖片的樣式咱們也應該讓用戶去控制(暫時沒有作模版),因此有兩個傳入的新屬性previewWrap、previewImgClass,顧名思義。
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);
    }  
}
複製代碼
  • 有了預覽,是否是還差個刪除呢,讓咱們回想triggerUpload方法,此時應該也沿用那種思想,傳入指定的index值去刪除指定的文件,不傳值則默認刪除全部。
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開始的整數且不得大於您上傳的文件數' // 拋出異常
    }
}
複製代碼
  • 同時須要一些私有狀態函數來接收xhr的事件回調方法,而後"call"一下暴露在外的config裏面的對應的函數,瘋狂打call後,就能夠在外邊接收到xhr的事件回調啦
// 開始上傳
_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是必不可少的,可是這裏我只是經過rules簡單的定義了一些規則,並且感受這塊其實應該給用戶去自定義,而後我在代碼裏面去轉義成個人代碼能看懂的方法,這裏還須要改進,也歡迎你們提寶貴意見
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大妹子
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和set files的值呀,暫時想到的是這些
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...好像也沒啥說的了,你們都是面向工資編程,那就祝你們早日一晚上暴富吧。

代碼是什麼,能吃嗎?

Todo

  1. 文件的拖拽上傳
  2. 文件的取消上傳,從新上傳
  3. 一些其餘細節和bug處理
相關文章
相關標籤/搜索