前端使用FileReader 读取本地文件和校验文件惟一

故事背景

昨天下午被问到一个问题:oss 对象存储里边因为有些图片被共享,致使上传了不少的重复的图片或者文件,有没有办法在上传以前判断一下这个文件是否被上传过,若是上传过直接去后端拿存储的地址行不行。前端

当时被问到的时候,第一反应是根据file的文件类型名称和大小生成一个MD5,后来被否决了,加入文件改了名字的话,这个文件仍是会被上传上去git

而后经过一天的调研,学习了这个以前没有用过的FileReader对象,顺便被他的其余方法给吸引住了,今天这里分享一下es6

是什么FileReader

FileReader 对象容许Web应用程序异步读取存储在用户计算机上的文件(或原始数据缓冲区)的内容,使用File或Blob

Blob 对象表示一个不可变、原始数据的类文件对象。Blob 表示的不必定是JavaScript原生格式的数据。npm

File 接口基于Blob,继承了 blob 的功能并将其扩展使其支持用户系统上的文件。") 对象指定要读取的文件或数据。后端

其中File对象能够是来自用户在一个 input 元素用于为基于Web的表单建立交互式控件,以便接受来自用户的数据; 可使用各类类型的输入数据和控件小部件,具体取决于设备和user agent元素上选择文件后返回的FileList对象,也能够来自拖放操做生成的DataTransfer对象,还能够是来自在一个HTMLCanvasElement上执行mozGetAsFile()方法后返回结果。(MDN)数组

说白了就是FileReader对象能够对内存中的数据进行操做promise

而后须要知道一个重点就是浏览器

FileReader仅用于以安全的方式从用户(远程)系统读取文件内容 它不能用于从文件系统中按路径名简单地读取文件。

也就是说他是不能够直接用本地的路径去读取文件的,能够请求后端的资源,来读取对应的文件,或者前端以一个比较安全的方式读取文件,常见的好比说input的文件上传安全

FileReader 的属性

打印一下 如图异步

1.jpg

EMPTY: 0
LOADING: 1
DONE: 2

这三个是对象实例的状态,分别是未读取文件,正在读取和读取完毕

readyState: [Exception: TypeError: Illegal invocation at FileReader.invokeGetter (<anonymous>:1:142)]
    result: [Exception: TypeError: Illegal invocation at FileReader.invokeGetter (<anonymous>:1:142)]
    error: [Exception: TypeError: Illegal invocation at FileReader.invokeGetter (<anonymous>:1:142)]

而后这三个属性

  • readyState,这个是获取当前对象实例的状态
  • result,这个是读取文件成功以后的返回值,具体的返回值是根据,你调用的对象实例的方法而返回的,下边我会讲一下FileReader的方法的具体使用
  • error,显而易见,报错的时候的信息
onloadstart: [Exception: TypeError: Illegal invocation at FileReader.invokeGetter (<anonymous>:1:142)]
    onprogress: [Exception: TypeError: Illegal invocation at FileReader.invokeGetter (<anonymous>:1:142)]
    onload: [Exception: TypeError: Illegal invocation at FileReader.invokeGetter (<anonymous>:1:142)]
    onabort: [Exception: TypeError: Illegal invocation at FileReader.invokeGetter (<anonymous>:1:142)]
    onerror: [Exception: TypeError: Illegal invocation at FileReader.invokeGetter (<anonymous>:1:142)]
    onloadend: [Exception: TypeError: Illegal invocation at FileReader.invokeGetter (<anonymous>:1:142)]
  • onloadstart 读取开始时触发,这里能够作一些经常使用的处理 好比加载loading什么的
  • onprogress 这个事件就比较让人喜欢了,这个方法会在文件被读取的过程当中被触发,每大概11927552字节左右会被触发一次,这个里边会反给一个ProgressEvent 对象,这个对象里边有本次读取文件的最大字节数和已经读取完毕的字节数,能够用来作进度条什么的
  • onload 这个事件是文件读取成功的时候触发 ,在这里里边可使用上边说道的实例上边的result属性,查看你操做的函数的对应的内容
  • onabort 读取文件被终端的时候触发,与之对应的有一个中断读取的方法
  • onerror 读取文件失败的时候触发
  • onloadend 读取文件 无论失败仍是成功都会触发这个方法,这个方法的执行时机在onload方法以后

FileReader 的方法

  • readAsDataURL , 这个方法会返回一个你获得的这个对象的一个base64的地址,可是这个地址,你会发现你的文件越大,这个地址就越长,其实这个地址是一个Base64编码的文件数据字符串

而后以前说了FileReader全部的操做都是异步的,因此你并不能像下边这样获取返回值

let fileReader = new FileReader()
        let url = fileReader.readAsDataURL(file.file)
        console.log(url)

这样是打印不出来的,你须要在他自身的处理事件上边回调获取

let fileReader = new FileReader()
        fileReader.readAsDataURL(file.file)
        fileReader.onload=()=>{
            console.log(fileReader.result)
        }

回调结果在对象实例的result属性上边,上边有说过

  • readAsBinaryString 开始读取指定的Blob中的内容。一旦完成,result属性中将包含所读取文件的原始二进制数据。

这个方法获取的结果是原始二进制数据,不能直接使用,还须要作一些转换或者使用标签什么得才能用,打印出来大概是这样的

1.jpg

  • abort 这个是中断文件的读取,好比你以为这个读取的事件有点长,再或者在某个特定状况下你但愿他停下来,那么这个时候可使用这个方法中断他,而且使用这个方法以后fileReader对象的状态是DONE 也就是说能够在onload里边去获取已经读取的数据

readAsArrayBuffer

  • 最后这两个是我今天用到的方法readAsArrayBuffer 开始读取指定的Blob中的内容, 一旦完成, result 属性中保存的将是被读取文件的 ArrayBuffer数据对象.

    • 这个ArrayBuffer上边有mdn的传送门能够看下,或者看一下阮一峰老师的es6最下边的讲解,他是一个字节数组,用来表示通用的、固定长度的原始二进制数据缓冲区
    • 他不能直接被操做,你能够用对应的TypedArray接口或者DataView的接口来操做他,这是一个对二进制字节数据操做的底层接口,我这里使用了TypeArray
  • TypeArray的经常使用构造函数

    • Int8Array:8 位有符号整数,长度 1 个字节。
    • Uint8Array:8 位无符号整数,长度 1 个字节。
    • Uint8ClampedArray:8 位无符号整数,长度 1 个字节,溢出处理不一样。
    • Int16Array:16 位有符号整数,长度 2 个字节。
    • Uint16Array:16 位无符号整数,长度 2 个字节。
    • Int32Array:32 位有符号整数,长度 4 个字节。
    • Uint32Array:32 位无符号整数,长度 4 个字节。
    • Float32Array:32 位浮点数,长度 4 个字节。
    • Float64Array:64 位浮点数,长度 8 个字节。

好,看到这里,以前没有接触过的同窗是否是脑瓜子嗡嗡的。。不要紧 我昨天我也嗡嗡的。。。

简单扼要的说一下就是说,上边列出来的这九个构造函数,都会根据你传进去的参数,生成一个对应的数组,而后这些数组统称为TypeArray视图,这个数组包含了全部的数组的方法和属性,你能够像数组同样去操做他们,一会我会在下边打印一下他们的结果,看一下就知道了

  • 而后上边说的DataView,简单说一下这个DataView是和TypedArray 配套使用的,由于DataView的参数是接受一个TypedArray对象,具体方法以下
  • 读取

    • getInt8:读取 1 个字节,返回一个 8 位整数。
    • getUint8:读取 1 个字节,返回一个无符号的 8 位整数。
    • getInt16:读取 2 个字节,返回一个 16 位整数。
    • getUint16:读取 2 个字节,返回一个无符号的 16 位整数。
    • getInt32:读取 4 个字节,返回一个 32 位整数。
    • getUint32:读取 4 个字节,返回一个无符号的 32 位整数。
    • getFloat32:读取 4 个字节,返回一个 32 位浮点数。
    • getFloat64:读取 8 个字节,返回一个 64 位浮点数。
  • 写入

    • setInt8:写入 1 个字节的 8 位整数。
    • setUint8:写入 1 个字节的 8 位无符号整数。
    • setInt16:写入 2 个字节的 16 位整数。
    • setUint16:写入 2 个字节的 16 位无符号整数。
    • setInt32:写入 4 个字节的 32 位整数。
    • setUint32:写入 4 个字节的 32 位无符号整数。
    • setFloat32:写入 4 个字节的 32 位浮点数。
    • setFloat64:写入 8 个字节的 64 位浮点数。

readAsText

这个是以前的时候搞得一个读取文件的方法,里边用到了FileReaderreadAsText方法,很少说废话了,直接附上代码和效果图

export default function readFile(model) {
    return new Promise((resolve) => {
        // 谷歌
        if (window.FileReader) {
            // 获取文件流
            let file = model.currentTarget ? model.currentTarget.files[0] : model;
            // 建立FileReader实例
            let reader = new FileReader();
            // 读文件
            reader.readAsText(file);
            reader.onload = () => {
                resolve(reader.result)
            }
        }
        //支持IE 7 8 9 10
        else if (typeof window.ActiveXObject != 'undefined') {
            let xmlDoc;
            xmlDoc = new ActiveXObject("Microsoft.XMLDOM");
            xmlDoc.async = false;
            resolve(xmlDoc.load(model))
        }
        //支持FF
        else if (document.implementation && document.implementation.createDocument) {
            let xmlDoc;
            xmlDoc = document.implementation.createDocument("", "", null);
            xmlDoc.async = false;
            resolve(xmlDoc.load(model))
        }
    })
}
   
//安装依赖
npm i zjsmethods -S
~~~~
// 页面引入并使用
 import { _readFile } from "zjsmethods"
_readFile(file).then(res=>{
    console.log(res)
})

1.png

readAsArrayBuffer校验文件惟一

上边说了那么一大堆,终于要进入正题了哈,直接看初版代码

const reader = new FileReader();
    reader.readAsArrayBuffer(file.file);
    reader.onload = () => {
           let u8Arr =  new Uint8Array(reader.result)
           console.log(u8Arr)
           console.log(md5(u8Arr))
    }

ok 没得问题,结果以下
1.png

正在我以为如此简单的时候,意外发生了,在我用比较小的文件的时候,只有1M 左右,可是当我上传了一个视频作测试的时候大概有两个G,浏览器崩溃了。。崩溃了。。了

3.png

而后我展开了 以前比较小文件的字节数组,大概有这么大

2.png

缘由是readAsArrayBuffer在读取文件的时候会先把整个文件加载到内存中,那么若是文件太大,内存就不够用了,浏览器进程就会崩溃。

既然整个加载不行,那么咱们选择把一个文件分段加载,后来我以为10M一段比较稳妥,因而改为了当文件小于10M的时候平均分红10段,若是大于10M ,那么每10M 分红一段 直到分完为止,一样为了不加密的时候数据太多形成卡顿,在生成标识的时候放弃用整个数组生成标识,采起固定规则的最大10M 数据生成标识

async vaildArrayBuffer(){
        const reader = new FileReader();
        while(this.whileNumber--){
            this.start = this.end
            this.end = this.end+this.whileMax
            let { start,end,sliceEnd,file}=this
                reader.readAsArrayBuffer(file.slice(start,end));
                reader.onload = () => {
                        new Uint8Array(reader.result)
                            .slice(0, sliceEnd)
                            .join('')
                }
        }
    }

这个时候又出了一个小插曲,在调用的时候reader被提示,正在进行文件读取,也是就一个reader在作读取文件操做的时候不能同事读取两个,因而乎刚开始的时候我高估了读取的速度放在了回调里边读取文件,代价就是我在电脑前面眼巴巴的看了控制台大概5分钟,后来改为了promise包裹,最后整理出的代码以下

/*
 * @Date: 2020-03-22 16:36:37
 * @information: 最后更新时间
 */
import md5 from 'md5'
export default class vaileFile{
    constructor(file){
        this.file = file
        // 每次截取多少二进制
        this.whileMax = Math.floor(file.size / 10 > 10240 * 1024 ?  10240 * 1024 : file.size / 10);
        // 循环截取多少次
        this.whileNumber = file.size <= 10240 * 1024 ? 10 : Math.ceil(file.size/this.whileMax)
        // 二进制的截取长度,超出10M后 每10M 截取一部分,最多10M
        this.sliceEnd = Math.floor(1024 * 10240 / file.size * 100 / this.whileNumber * this.whileMax)
        this.sliceEnd = this.whileNumber>10?this.sliceEnd:10240 * 1024 
        // 转换二进制的长度
        this.start = 0 
        this.end = 0;
    }
    /**
     * @Author: 周靖松
     * @Date: 2020-03-22 15:53:07
     * @information: 校验文件惟一
     */
    async vaildArrayBuffer(){
            let promiseArr = []
            while(this.whileNumber--){
                this.start = this.end
                this.end = this.end+this.whileMax
                let { start,end,sliceEnd,file}=this
                let promiseArrayBuffer = new Promise((resolve,reject)=>{
                    const reader = new FileReader();
                    reader.readAsArrayBuffer(file.slice(start,end));
                    reader.onload = () => {
                        resolve(
                            new Uint8Array(reader.result)
                                .slice(0, sliceEnd)
                                .join('')
                        )
                    }
                })
                promiseArr.push(promiseArrayBuffer)
            }
            return md5((await Promise.all(promiseArr)).join(''))
        }
    }

大功告成,上传的文件后会生成一个md5 ,复制文件,文件更名字,均可以识别是以前的文件

而后写一个README.md 说明一下使用方法

### _vaileFile ,//使用文件二进制校验文件惟一性

    当有业务须要上传oss 对象存储的时候,为了不同一个文件(视频,音频,图片,压缩包等),有可能其余人复制或者更名字等等,形成文件重复上传,大量占用空间,写了一个校验文件的方法

    //安装依赖
    npm i zjsmethods -S
    
    //引入这个类

    import { _vaileFile } from 'zjsmethods'

    //  而后在你须要判断oss 是否有该文件的时候

    new _vaileFile('file对象').vaildArrayBuffer().then(res=>{
        console.log(res)
        // 继续上传 或者 向后端请求已经存在的文件url
    })

    // new 这个类以后 有一个vaildArrayBuffer 方法 他返回一个promise ,里边返回值是一个md5的字符串,这个是这个文件的惟一标识

最后发布npm包传git 结束学习 ★,°:.☆( ̄▽ ̄)/$:.°★

喜欢的点个赞吧,有不足之处欢迎斧正

相关文章
相关标签/搜索