從選擇上傳文件縮略圖預覽到提交上傳全流程總結方案

前言

上傳圖片生成縮略圖,這個需求很常見,網絡上的文章也不少。可是大多數都是直接丟一堆代碼出來,也很少解釋,不說下注意點,不說明優缺點等,也缺少場景的延伸。css

我這裏所寫的,不只是生成縮略圖這個需求,還把驗證上傳文件、刪除已選文件、提交上傳文件的一個完整的實際流程來展開此文章。html

由於網上不少就單純地解決一個生成縮略圖的方案,可是忽略實際場景的應用,不少時候仍是存在一些不足,在zheli作些小優化和作下幾個方法的對比,也有一些注意點說起出來,幫助你們避坑。我看不少文章,都僅只說生成縮略圖,卻不告知如何配合發送數據。數組

好記性不如爛筆頭,人老了不少會忘記,趁我人老頭禿前記一下爲好。瀏覽器

需求

首先要明確需求,根據需求才能肯定方案:bash

點擊上傳工具,彈出上傳文件對話框,可單選可多選文件,這次選擇以後生成文件的羅略圖(本文主要針對圖片格式文件),以後每次的選擇文件上傳,都會在本來已選擇的基礎上新增。最後把每次選擇的文件一併進行上傳。網絡

要注意的一點是,在此需求中,並非每一次的選擇文件,會覆蓋上次選擇的文件。而是會保留上次選擇的結果,而後把每次選擇的結果一併提交了。要進行刪除已選的文件的話,就刪除縮略圖即表明刪除了已選文件。app

需求細節分點描述:dom

  1. 記錄每次選擇結果,最後一併提交
  2. 每次選擇可多選
  3. 對選擇的文件進行校驗
  4. 根據所選的文件生成縮略圖(隨機 or 根據選擇順序)
  5. 點擊縮略圖進行預覽(在文章不展開說,由於單純是css樣式便可控制,沒多大難度)
  6. 刪除縮略圖表明刪除已選文件

生成縮略圖

要生成縮略圖,無非就是展現「圖」。「圖」的展現有兩種表現形式,一個是用img標籤來直接展現圖片,一個是用background-image樣式來展現圖片。因此這裏的生成縮略圖,能夠往這兩個方向走。異步

這裏以用img標籤表示爲例:函數

/** * 生成預覽容器 * @param {String} url - 文件的url * @param {Function} cb - 圖像加載完成時的執行函數 */
 function createPreviewWrap (url, cb) {
    let imgWrap = document.createElement('div');
    let image = new Image();
    imgWrap.className = 'img-wrap';
    image.src = url;
    cb && (image.onload = cb);
    // 這裏我放在imgWrap裏是爲了更好地控制縮略圖的樣式
    // 實際上用background-image的方式呈現圖片,反而更好地控制圖片的顯示樣式
    imgWrap.appendChild(image);
    // 生成刪除縮略圖icon,樣式請本身補充
    const deleteIcon = document.createElement('i');
    deleteIcon.onclick = deleteFile; // deleteFile函數後面在各自方案中說明
    imgWrap.appendChild(deleteIcon);
    // img-container爲用來放置縮略圖的所在標籤,如div
    document.getElementById('img-container').appendChild(imgWrap);
}
複製代碼

可是要能展現出「圖」,上面兩個思路的首要條件都是要知道圖片的「源」,即圖片路徑。所以要實現生成縮略圖的問題,就轉化爲生成圖片路徑的問題了。

上傳文件所用到的標籤是

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

因此,要處理的是文件類型(Blob類型的子類),針對文件類型生成URL的方式有兩種

  • FileReader
  • URL.createObjectURL

使用FileReader

使用FileReader,生成的url是基於base64編碼的url(Data Url),所以長度會比較大。

基本用法:

var fileReader = new FileReader(); // 新建一個FileReader
fileReader.readAsDataURL(blob); // 對blob以data url的形式進行讀取
 // 讀取過程是異步的,讀取成功後的this.result即爲file的Data Url結果
fileReader.onload = function() {
    console.log(this.result);
};
複製代碼

知道了基本用法後,要對上傳文件生成url就好辦了:

/** * FileReader方式實現縮略圖(按讀取速度快慢生成) * @param {FileList} files - 這次選擇上傳的文件列表 */
function createThumbnailRandom (files) {
    for (let i = 0; i < files.length; i++) {
        let fileReader = new FileReader();
        fileReader.readAsDataURL(files[i]);
        fileReader.onload = function() {
            createPreviewWrap(this.result);
        };
    }
}
複製代碼

因爲選擇上傳文件時是能夠多選的,因此上述方法中files是個FileList類型,類數組,須要對其遍歷一個個生成url。能夠留意到個人註釋有這麼一句「按讀取速度快慢生成」,由於上面說了fileReader的讀取是異步的,因此上述方法並無控制讀取順序。

若是你想按照順序,可參考下述方法:

/** * FileReader方式實現縮略圖(按順序生成) * @param fileReader * @param {FileList} files - 這次選擇上傳的文件列表 * @param {Number} index - files要處理的下標 */
function createThumbnail (fileReader, files, index) {
    let i = index || 0;
    if (i >= files.length) {
        return;
    }
    fileReader.readAsDataURL(files[i]);
    fileReader.onload = function() {
        createPreviewWrap(this.result);
        createThumbnail(fileReader, files, ++i);
    };
}
複製代碼

這裏用遞歸的方式來實現只有每次讀取完,再讀取下一個文件,進而來控制順序。

咱們要知道,file類型的input,當你在某次選擇上傳文件時,是多選的,此時FileList的默認順序是按照文件名升序排序的。

咱們把上述的兩個方法整合在一個方法裏:

/** * FileReader方式實現縮略圖 * @param {FileList} files - 這次選擇上傳的文件列表 * @param {Number} type - 1:按順序回顯 2:按速度回顯 */
function fileReaderPreview (files, type) {
    // IE9(含9)如下不支持FileReader
    if (!FileReader) {
        alert('您的瀏覽器不支持上傳,請換用或升級瀏覽器!');
    }
    let type = type || 1;
    if (type === 1) {
        let fileReader = new FileReader();
        // 按在文件選擇器裏的文件順序回顯縮略圖
        createThumbnail(fileReader, files);
    } else if (type === 2) {
        createThumbnailRandom(files)
    }
}
複製代碼

使用createObjectURL

語法:

window.URL.createObjectURL(blob)
複製代碼

參數爲Blob類型,返回值爲Objcet URL(Blob URL)

該方式簡單明瞭,很少講,直接看代碼:

/** * createObjectURL方式實現縮略圖 * @param {FileList} files - 這次選擇上傳的文件列表 */
function objectURLPreview (files) {
    // IE9(含9)如下不支持createObjectURL
    for (let i = 0; i < files.length; i++) {
        const url = window.URL.createObjectURL(files[i]);
        createPreviewWrap(url, function (){
            // 釋放url內存
            // 若是是用background-image來呈現圖片,那麼也是要經過new Image對象來加載圖片
            // 監聽onload事件後再把url賦值給background-image,以後再釋放內存便可
            URL.revokeObjectURL(url);
        });
    }
}
複製代碼

小結

既然有兩個方式,咱們選擇哪一個比較好呢,咱們簡單作個對比。

同:

  • 只支持IE10(含10)以上的瀏覽器

異:

執行時間

  • FileReader是異步執行的,讀取文件時異步的
  • createObjcetURL是同步執行的

內存狀況

  • FileReader產生的url是base64編碼地址,所佔內存通常比createObjcetURL要大。可是在它沒用的時候就會自動釋放內存(瀏覽器的內存回收機制)
  • createObjcetURL建立出來的網絡地址所佔用內存須要等到頁面刷新/關閉或者執行revokeObjcetURL方法時才被釋放

還有一些不肯定因素,就是我在查閱資料時,看到有些人反饋說用FileReader在某些安卓機上不能生成預覽圖,可是createObjectURL能夠,且createObjectURL性能會好點。 具體這點我還沒有驗證,請你們辯證地看待。

驗證(限制)上傳文件

本文主要講述圖片類型的上傳,而後生成縮略圖。對於其餘文件類型的上傳,其實大同小異,只是縮略圖方面可能要額外處理,如找個固定的圖片表明一類文件等,視頻上傳生成截圖,也是一個值得分享的知識,可是不在本文以內。

言歸正傳,咱們能夠在html裏對上傳文件進行必定的限制

<!-- 用label替代input.file做爲視覺上的上傳交互觸發器,方便樣式統一和美化 -->
<label>
    請選擇上傳文件
    <!-- accept屬性僅僅是實現了文件選擇器裏展現的文件類型默認過濾出指定類型,可是不會真正阻止上傳別的類型,須要結合js檢驗 -->
    <input type="file" id="file" multiple accept="image/*">
</label>
複製代碼

關於accept的取值格式和形式可參考Unique file type specifiers

正如註釋所說,要在js裏作限制纔是真正起到限制做用

/** * 檢驗上傳文件類型 * @param {FileList} files - 這次選擇上傳的文件列表 */
function verifyType (files) {
    for (let i = 0; i < files.length; i++) {
        // type屬性是根據文件後綴名來判斷的,因此若是你修改了文件的後綴名,type值也會發生改變
        // 所以不建議僅用type來做爲惟一的判斷條件
        if (!/^image\//.test(files[i].type)) {
            return false;
        }
    }
    return true;
}
複製代碼

選擇上傳

有了上面的兩個基礎,咱們來對上傳選擇這個操做來作個處理,以知足需求裏的「屢次選擇一併提交」這個需求。

首先咱們要知道,利用type爲file的input實現的上傳,每次打開文件選擇框進行文件選擇後,都會覆蓋掉先前選擇的內容,若是不作任何處理,進行數據的提交,只會提交最後一次選擇的文件。因此咱們須要有一個變量,來記錄每次選擇的文件。

<label>
    請選擇上傳文件
    <input type="file" id="file" multiple accept="image/*">
</label>
複製代碼
// 用於記錄上傳的全部文件
var uploadFiles = [];

// 對上傳選擇作監聽
document.getElementById('file').addEventListener('change', upload, false);

/** * 選擇上傳文件後 */
function upload (ev) {
    let e = ev || event;
    const files = e.target.files;
    if (!files.length) {
        return;
    }
    // 檢驗上傳文件類型
    if (!verifyType(files)) {
        alert('請上傳圖片格式的文件');
        return;
    }

    Array.prototype.push.apply(uploadFiles, files);
    
    // 生成預覽圖,這裏選擇使用objectURL
    objectURLPreview(files);
}
複製代碼

提交數據

一切都就緒了,最後固然就是把數據提交給後臺了,缺乏這步,一切都失去意義了。

提及提交數據,傳統的方式,莫過於使用form表單進行提交了。

直接採用form表單

簡易版

從簡入難,雖然該方案不是本文章主要講的內容,可是仍是簡單介紹下吧,畢竟你搜索網上資料,都有此方案的推介,可是並無提出該方案的不足。我寫出來,引覺得戒。

順帶有些小知識點,能夠留意一下。

<!-- 在使用包含文件上傳控件的表單時,必須使用enctype="multipart/form-data" -->
<!-- 由於是上傳文件,用get請求大小受限制,因此用post -->
<form action="http://example.com" method="POST" enctype="multipart/form-data">
    <label>
        上傳文件
        <!-- 表單元素必定要寫name,否則提交不了數據給後臺 -->
        <input type="file" id="uploadFiles" name="uplpoadFiles" multiple accept="image/*" onchange="upload()">
    </label>
    <!-- button的type屬性默認是submit,可是爲了兼容ie(默認是button),顯示地寫出type爲submit比較好,會自動提交表單 -->
    <!--若是type是button,可是你綁定了onclick="submit()",就算你有一個函數名叫submit,可是仍是當作表單提交的submit來處理,-->
    <!--即發揮了type="submit"的按鈕處理-->
    <button type="submit">提交</button>
</form>
複製代碼

以上是一個最簡單的利用表單用html就能實現的提交功能。

可是要注意,一個input只能記錄最後一次選擇的一批文件。例如你點擊「上傳文件」,彈出選擇窗口,選擇了一批後,點肯定,此時窗口關閉了;而後你又點擊「上傳文件」,此時再重複上述過程,以此類推。那麼這個iduploadFilesinput被提交上去的只會是上述過程當中最後一次選擇的那批文件。

爲了配合此種行爲,咱們須要改造一下上面的upload函數,只是刪除了一行代碼Array.prototype.push.apply(uploadFiles, this.files);就好。

可是這樣的效果並非咱們想要的,咱們是想每次選擇以後都會生成縮略圖,而後提交的時候是要把每批選擇的文件都提交上去。這時候咱們須要對上述代碼進行升級改造。

改造升級版

思路:既然一個input只能表明一批文件,因此你想最終提交幾批,就對應新建幾個input就行了。只要每選擇一批文件以後,生成預覽圖後,已選擇的input就隱藏,新建一個新的input,每次如此。

醜話說在前,在容許一個input多選的狀況下,因爲文件上傳input的行爲就是若是你選擇錯或者反悔了,那從新選擇便可,後面一次覆蓋前面一次選擇,自己就沒有配備刪除已選文件功能。因此該方案的一個缺點就是無法刪除已選文件。這很顯然,該缺陷不能接受,那隻能退而求其次,只要限制每一個input爲單選文件,這樣刪除縮略圖時就把對應的input刪掉就行了。

<form id="myForm" action="http://example.com" method="POST" enctype="multipart/form-data">
    <label>
        上傳文件
        <input type="file" id="upload1" name="uplpoadFiles[]" accept="image/*" onchange="upload()">
    </label>
    <button id="submitBtn" type="submit">提交</button>
</form>
複製代碼

以上是html部分,咱們在選擇上傳文件,觸發的upload方法裏,添加對input的隱藏和添加。

/** * 選擇上傳文件後 */
function upload (ev) {
    let e = ev || event;
    const files = e.target.files;
    if (!files.length) {
        return;
    }
    // 檢驗上傳文件類型
    if (!verifyType(files)) {
        alert('請上傳圖片格式的文件');
        return;
    }

    // 隱藏和生成新的input
    e.target.parentNode.style.display = 'none';
    const form = document.querySelector('#myForm');
    const label = document.createElement('label');
    label.innerText = '上傳文件';
    const input = document.createElement('input');
    input.type = 'file';
    input.name = 'uplpoadFiles[]';
    input.accept = 'image/*';
    input.onchange = upload;
    label.appendChild(input);
    form.insertBefore(label, document.querySelector('#submitBtn'));
    
    // 生成預覽圖,這裏函數有調整,下方有說明
    objectURLPreview(files, e.target.parentNode);
}
複製代碼

刪除已選文件和縮略圖

接下來就是,若是刪除縮略圖,就得刪除對應的input。咱們怎麼找到這個對應的input呢,咱們對上述的createPreviewWrapobjectURLPreview作個簡單調整,把input所在的label節點(即上面upload方法中的e.target.parentNode)和縮略圖節點當作參數進行傳遞,而後進行刪除。

/** * createObjectURL方式實現縮略圖 * @param {FileList} files - 這次選擇上傳的文件列表 * @param label- 要刪除的包裹input的label */
function objectURLPreview (files, label) {
    for (let i = 0; i < files.length; i++) {
        const url = window.URL.createObjectURL(files[i]);
        createPreviewWrap(url, label, function (){ // 主要改動這裏
            URL.revokeObjectURL(url);
        });
    }
}

/** * 生成預覽容器 * @param {String} url - 文件的url * @param {String} label - input的對應的label * @param {Function} cb - 圖像加載完成時的執行函數 */
 function createPreviewWrap (url, label, cb) {
    let imgWrap = document.createElement('div');
    let image = new Image();
    imgWrap.className = 'img-wrap';
    image.src = url;
    cb && (image.onload = cb);
    imgWrap.appendChild(image);
    const deleteIcon = document.createElement('span');
    deleteIcon.onclick = () => {deleteFile(label, imgWrap);} // 主要改動爲這裏
    imgWrap.appendChild(deleteIcon);
    document.getElementById('img-container').appendChild(imgWrap);
}
複製代碼

對應的刪除節點函數

/** * 刪除對應input的label和縮略圖 * @param label - input的對應的label * @param imgWrap - 要刪除的縮略圖 */
function deleteFile (label, imgWrap) {
    document.querySelector('#myForm').removeChild(label);
    document.getElementById('img-container').removeChild(imgWrap);
}
複製代碼

小結

總結下該方案的缺點:

  • 每次選擇文件只能單選
  • 頻繁進行DOM操做,可能會引發重繪或重排

丟個demo

用XMLHttpRequest發請求提交

在這裏的方案,咱們可以在上文的基礎上(除去form表單提交方案的內容),就能知足全部需求,且不用退而求其次。

按照傳統的作法,要提交上傳文件的內容,都是藉助表單來實現的。那麼若是不想直接用上述的html形式的表單提交,咱們還能夠藉助FormData對象來表示有一份表單數據,所以能夠沒必要寫form標籤,而後利用XMLHttpRequest發送請求提交表單數據。

html部分能夠改爲這樣:

<div id="img-container"></div>
<div>
    <label>
        請選擇上傳文件
        <input type="file" id="file" multiple accept="image/*">
    </label>
    <button onclick="submitFormData()">提交</button>
</div>
複製代碼

提交按鈕綁定了一個方法,就是用於提交上傳數據的:

function submitFormData () {
    if (uploadFiles.length === 0) {
        alert('請選擇文件');
        return;
    }
    // FFormData ie10如下不支持
    let formData = new FormData();
    uploadFiles.forEach(item => {
        formData.append('uplpoadFiles[]', item);
    });
    let xhr = new XMLHttpRequest();
    xhr.open('POST', 'http://example.com');
    xhr.send(formData);
    xhr.onload = function () {
        if (this.status === 200 || this.status == 304) {
            alert('上傳成功');
        } else {
            alert('上傳失敗');
        }
    }
}
複製代碼

刪除已選文件和縮略圖

一樣的,在該方案下的刪除操做,須要對生成縮略圖相關的兩個函數作調整,這裏仍然以objectURLPreview爲例子。

咱們要找到要刪除對應的在uploadFiles數組中的元素,而後對其刪除,這裏咱們利用數組下標肯定找到。(主要調整在註釋處)

function objectURLPreview (files) {
    const index = uploadFiles.length - files.length; // 找出用於計算本批文件對應下標的基礎值
    for (let i = 0; i < files.length; i++) {
        const url = window.URL.createObjectURL(files[i]);
        // 基礎值+此批對應的下標即爲在整個uploadFiles數組中的下標
        createPreviewWrap(url, index + i, function (){
            URL.revokeObjectURL(url);
        });
    }
}
function createPreviewWrap (url, index, cb) {
    let imgWrap = document.createElement('div');
    let image = new Image();
    imgWrap.className = 'img-wrap';
    image.src = url;
    cb && (image.onload = cb);
    imgWrap.appendChild(image);
    const deleteIcon = document.createElement('span');
    deleteIcon.onclick = () => {deleteFile(index, imgWrap);}// 主要改動爲這裏
    imgWrap.appendChild(deleteIcon);
    document.getElementById('img-container').appendChild(imgWrap);
}
複製代碼

刪除函數即爲

function deleteFile (index, imgWrap) {
    uploadFiles.splice(index, 1);
    document.getElementById('img-container').removeChild(imgWrap);
}
複製代碼

最後,丟個demo,看看完整的一個例子

總結

整篇文章不管哪一個方案,兼容性都是要在ie10含10以上才能正常運行。

未經容許,請勿私自轉載

相關文章
相關標籤/搜索