從零實現Vue的元件庫(四)- File-Reader實現

FatGe發表於2018-12-25

實現一個File-Reader元件用來讀取本地資源。

概述: 在使用者手動上傳一些資源的時候,需要分為兩步,第一步是將其從本地讀取出來,得到一個file物件,然後再上傳至伺服器。該元件用於第一步,然後可通過後續進一步封裝程Upload元件。

該元件的痛點在於:
  • 新增拖拽上傳的功能;
  • 本地檔案的批量讀取,以及相關預覽;
  • 相關瀏覽器的預設行為的preventDefault
  • 相關檔案的基本校驗。

2019年1月1日更新

在該元件的 destroyed 生命週期中新增新特性,利用 URL.revokeObjectURL() 靜態方法用來釋放一個之前通過呼叫 URL.createObjectURL() 建立的已經存在的 URL 物件,從而進行垃圾收集。

destroyed() {
    if (this.targetFiles.length && window.URL.revokeObjectURL) {
        this.targetFiles.forEach(file => {
            window.URL.revokeObjectURL(file.url);
        });
    }
}
複製程式碼

1. 例項

最終效果

程式碼

<!-- 基礎用法 -->
<fat-filereader
    accept=".png, .jpg"
    :size="500 * 1024"
    @success="event => readHandler(event, 'file')"
    @error="errorHandler"
/>

<!-- 自定義上傳區域 -->
<fat-filereader @success="event => readHandler(event, 'otherFile')">
    <div slot="clickarea" class="upload-area">
        <fat-icon class="icon" name="cloud_upload" size="24"/>
    </div>
</fat-filereader>

<!-- 拖拽上傳 -->
<fat-filereader dragable @success="event => readHandler(event, 'anotherFile')">
    <div slot="clickarea" class="upload-area">
        <fat-icon class="icon" name="cloud_upload" size="24"/>
    </div>

    <span v-if="anotherFile.name">已上傳:{{ anotherFile.name }}</span>
</fat-filereader>

<!-- 上傳多個檔案 -->
<fat-filereader multiple :limit="5" @success="multipleHandler" @error="errorHandler"/>
複製程式碼

例項地址:File-Reader 例項

程式碼地址:Github UI-Library

2. 原理

該元件的實現是基於原生的<input type="file" />,再通過新增拖拽功能來以達到上圖效果。

元件的基本結構如下,主要包含兩個部分:

  • 其一是<slot name="clickarea"></slot>自定義的點選區域;
  • 其二是原生的< input type="file" />用來完成檔案上傳工作。
<div
    :class="['file-reader', { 'is-disabled': disabled }]"
>
    <div 
        class="click-area" 
        @click.stop="eventHandler('readFile')"
        @dragenter="prevent"
        @dragover="prevent"
        @drop="event => dragable && eventHandler('dropReadFile', event)"
    >
        <slot name="clickarea">
            <fat-button :disabled="disabled" type="success">上傳</fat-button>
        </slot>
    </div>
    <slot></slot>
    <input
        ref="input"
        type="file"
        class="is-hide"
        v-bind="$attrs"
        @change="event => eventHandler('change', event)"
        :disabled="disabled"
    >
  </div>
</template>
複製程式碼

<slot name="clickarea"></slot>外層包裹div標籤,用於監聽該區域的點選事件。當它觸發時,相關處理函式為

const handler = {
    readFile: () => {
        // fix change again
        this.$refs.input.value = "";
        this.$refs.input.click();
    },
    ...
};
複製程式碼

先重置this.$refs.input.value,不然會出現無法再上傳的問題。然後,觸發原生<input type="file" />的點選事件,此時頁面上會彈出原生的上傳框。

監聽該標籤的change事件, @change="event => eventHandler('change', event),如果發生上傳,會觸發相關處理函式。

eventHandler(type, event = {}) {
    const handler = {
        ...
        // read files
        change: event => (this.sourceFiles = event.target.files)
    };
    handler[type] && handler[type](event);
}
複製程式碼

event物件中,讀取選中待上傳的檔案物件

最終效果

包含著name檔名,size大小,type型別等屬性。

由於部分檔案需要進行預覽,例如img、svg等,所以需要生成對應的URL,基本方法為:

  • window.URL.createObjectURL:該方法會建立一個 DOMString,其中包含一個表示引數中給出的物件的URL;
  • new FileReader:該物件允許Web應用程式非同步讀取儲存在使用者計算機上的檔案(或原始資料緩衝區)的內容,再通過readAsDataURL讀取指定的 Blob 或 File 物件。

首先判斷當前是否支援window.URL && window.URL.createObjectURL,如果支援,只需將已上傳的targetFiles進行處理

Array.prototype.map.call(value, file => ({
    // file => url
    url: window.URL.createObjectURL(file),
    file
}));
複製程式碼

如果不支援該方法,則需要利用File ReaderreadAsDataURL,由於該方法是非同步的,所以將其封裝為Promise形式

const getReader = file => {
    return new Promise(function(resolve, reject) {
        const fileReader = new FileReader();
        // 讀取相關檔案
        fileReader.readAsDataURL(file);
        // fileReader onload 時,返回其結果
        fileReader.onload = () => resolve(fileReader.result);
        fileReader.onerror = () => fileReader.abort();
    });
};
const readers = Array.prototype.map.call(value, file => getReader(file));
複製程式碼

再利用Promise.all統一進行處理,結合已上傳的targetFiles生成返回結果。

Promise.all(readers).then(results => {
    this.targetFiles = results.map((url, i) => ({
        url,
        file: value[i]
    }));
});
複製程式碼

以上完成基本的上傳、預覽功能。然後新增拖拽上傳功能,在clickArea監聽dragenterdragoverdrop事件。

<div 
    class="click-area" 
    @click.stop="eventHandler('readFile')"
    @dragenter="prevent"
    @dragover="prevent"
    @drop="event => dragable && eventHandler('dropReadFile', event)"
>
    <slot name="clickarea">
        <fat-button :disabled="disabled" type="success">上傳</fat-button>
    </slot>
</div>
複製程式碼

相關處理函式為

eventHandler(type, event = {}) {
    const handler = {
        ...
        dropReadFile: event => {
            event.preventDefault();
            this.sourceFiles = event.dataTransfer.files;
        }
    };
    handler[type] && handler[type](event);
},
prevent(event) {
    event.preventDefault();
}
複製程式碼

利用event.dataTransfer.files獲取相關上傳檔案,再通過event.preventDefault阻止遊覽器的一些預設行為,例如直接預覽該檔案等。獲取到sourceFiles之後的操作等同於之前點選上傳。

讀取完成之後需要對檔案進行校驗,主要有上傳檔案的大小以及數量,相關程式碼如下

targetFiles(value) {
    const { size, limit } = this;
        if (value.length > limit) {
            this.$emit("error", {
                msg: "the quantity of files is too large its number cannot exceed"
            });
        } else {
            if (
                value.some(item => {
                    const {
                        file: { size: fileSize }
                    } = item;
                    return size && fileSize > size;
                })
            ) {
                this.$emit("error", {
                    msg: "file is too large its size cannot exceed"
                });
            } else {
                this.$emit("success", value);
            }
        }
    }
}

複製程式碼

3. 使用

相關的原生<input type="file" />acceptmultiple等屬性,利用v-bind=$attrs傳遞給原生的<input type="file" />

之後, 可以結合相關上傳方法將其封裝程Upload元件,以Axios為例

uploadFile (fileData) {
    const config = {
        // 依據當前環境配置
        baseURL: ...,
        headers: { 'Content-type': 'multipart/form-data' } 
    }
    let data = new FormData();
    data.append('file', fileData);
    data.append('name', fileData.name);
    
    return Axios.post('upload url', data, config)   
}
複製程式碼

4. 總結

封裝一個File-reader元件,簡化其內部邏輯,方便後續擴充。

往期文章:

原創宣告: 該文章為原創文章,轉載請註明出處。

相關文章