关于文件上传下载我所知道的全部内容

文件上传是一个很基础的内容,有很多的应用场景,但是前端各种库和框架实在是太便利了,根本不用了解到用原生的是怎么实现的,一遇到问题就各种懵逼,最近刚好经历了几种文件上传的需求,就以此来作为开年的第一篇分享

1. 表单上传

在AJAX还不流行的年代,表单上传文件是基本操作。表单上传文件很简单,有两个需要重点关注的属性:

1.1 enctype

属性用于设定form表单提交的时候数据编码方式,一共有三种参数选择:

  1. application/x-www-form-urlencoded 发送前编码所有字符
  2. multipart/form-data 不对字符进行编码
  3. text/plain 空格转换为+,但是不会对字符进行编码

如果想要使用文件上传,必须指定为第二个属性值:enctype=multipart/form-data

1.2 multiple

对于选择文件的时候如果想对文件进行多选,那么必须要设置<input type="file" multiple="multiple">

一个比较完整代码片段

<form action="http://localhost:3000/upload" method="POST" enctype="multipart/form-data">
    <input type="file" name="file" multiple="multiple">
    <input type="submit" value="submit"/>
</form>

2. AJAX上传

如果要实现页面不刷新的文件上传,有两种常用的方案:

  1. <iframe>表单提交方案
  2. AJAX方案

第一种方案在页面中嵌套一个<iframe>,将表单放置于<iframe>中,此时完成表单提交不会发生全局页面刷新。但是这个方案,随着AJAX的逐渐完善以及前后端分离和单页面应用的普及,轮为了很不常规的替代方案。

2.1 基本内容

实现AJAX上传,首先需要对XHR有所了解(如有不了解的可以参照MDN的学习文档AJAX开始

XHR在发送数据的时候可以接受一个html5的新对象FormData,可以通过将包含文件的表单/活着将文件放到FormData中传递到后端接口,

html:
<form id="fileForm">
    <input type="file" name="file" multiple="multiple" onchange="changeFileChoose(event)">
    <input type="button" onclick="upload();" value="submit"/>
</form>

js:
let formData = new FormData(document.getElementById('fileForm'));
let xhr = new XMLHttpRequest();
xhr.open('POST', 'http://localhost:3000/upload');
xhr.setRequestHeader('Content-Type', 'multipart/form-data');
xhr.send(formData);

如果表单中每个文件想单独发送请求(发送多次请求),可以获取表单中文件信息并构建多个表单对象上传

formData.getAll('file').filter(file => {
    return file.name
}).forEach((file, index) => {
    let separateFormData = new FormData();
    separateFormData.set('file', file);
    xhr.send(separateFormData)
})

PS:在传递到时候注意设置请求头信息Content-type: multiple/form-data来支持文件上传操作

2.2 上传进度

将上传过程的上传进度告诉用户是一个很好的用户交互行为,一方面避免用户多次重复上传,另一方面也是对用户操作对反馈,告诉用户系统正在处理他的操作。

监听文件上传进度,个人认为要么前端轮询获取后端的文件写入情况,要么前端有支持上传进度获取对事件,其实确实AJAX上传过程中提供了相关对象,获取到文件的网络传输情况的,所以在对上传结果要求并非十分严格的情况下,通过前端监听反馈进度已经足够了

上传进度的监听需要使用xhr.upload对象的事件,利用监听xhr.upload.onprogress来实现上传进度的监听

xhr.upload.onprogress = ev => {
    console.log(`upload loaded: ${ev.loaded}, total: ${ev.total}`);
    progress = ev.loaded * 100 / ev.total;
}

onprogress事件的event对象中包含前端已经传输的数据信息ev.loaded以及文件的总尺寸信息ev.total,利用这些信息就可以在页面中显示文件上传进度

2.3 取消上传

AJAX自身提供了取消操作,通过利用xhr.abort()方法来取消掉整个xhr的请求,当然如果仅仅想取消文件上传而不是取消整个AJAX过程,也可以使用xhr.upload.abort()单独的取消掉AJAX过程中的文件上传

2.4 选择图片并上传预览

<input type="file">onchange事件在选择文件发生变更的时候会触发,利用事件中的event对象的event.target.files,可以获取到当前选择的文件集合,遍历该集合,根据file.type来判断文件类型,并利用window.URL.createObjectURL(file)可以拿到转换过后的base64图片地址,最后再给图片img.src设置路径从而实现选择回显(图片可以使用createElement('img')body.appendChild(),也可以使用new Image()canvasdragImage()方法来实现绘制)

/**
 * 验证图片类型
 * @param {*} type 文件类型
 */
function validateImage(type) {
    return ['image/jpeg', 'image/png', 'image/jpg'].includes(type);
}

if (validateImage(file.type)) {
    let image = document.createElement('img');
    // URL.createObjectURL可以接受File, Blob, MediaSource对象
    image.style.height = '100px';
    image.style.width = '100px';
    image.src = window.URL.createObjectURL(file);
    document.body.appendChild(image);
}

PS:由于图片加载对浏览器来说是异步的过程,如果要对图片进行相关操作,请在img.onload操作以后执行

3. 拖拽上传

在了解AJAX上传的基础上,其实拖拽上传只需要知道如何获取到拖拽文件对象,就可以使用相同的方法进行上传了。
拖拽也是有一系列事件,具体拖拽相关事件,可以参见接下来的分享或者MDN Drag and Drop API

3.1 文件拖拽

文件拖拽上传的关键在于,可以通过event.dataTransfer获取到拖拽信息。该对象存在的两个对象属性filesitems,如果拖拽的内容是文件,那么可以遍历files对象,就可以获得文件信息

html:
<div>
    <p>拖拽上传</p>
    <div id="fileArea" class="file_area">拖拽到此区域上传</div>
</div>

js:
let fileArea = document.querySelector('#fileArea')
fileArea.addEventListener('drop', ev => {
    let files = ev.dataTransfer.files
    for (let i = 0; i < files.length; i++) {
        // 调用ajax相关内容
        sendFile(files[i]);
    }
    // 防止浏览器直接打开文件
    ev.preventDefault();
})

3.2 目录拖拽

突然某一天出现了目录拖拽的需求,以为和文件上传是同样可以通过files来获取,结果发现不行。这个时候需要使用另一个属性对象items,并利用File and Directory Entries API来处理items

首先利用item.webkitGetAsEntry()/item.getEntry()获取到FileEntry,之后使用entry.createReader()获取到reader对象,之后reader.readEntries读取信息并递归分别处理文件和文件夹,如果是文件通过entry.file()的方式获取文件信息

js:
fileArea.addEventListener('drop', ev => {
    for (let i = 0; i < ev.dataTransfer.items.length; i++) {
        // 获取entry对象
        let entry = ev.dataTransfer.items[i].webkitGetAsEntry()
        if (entry) {
            scanFiles(entry, sendFile)
        }
    }
    // 防止浏览器直接打开文件
    ev.preventDefault();
})

function scanFiles (entry, callback) { // 浏览文件结构
    // 如果是文件目录,那么继续循环获取到目录下的文件
    if (entry.isDirectory) {
      let directoryReader = entry.createReader();
      directoryReader.readEntries(entries => {
        entries.forEach(entry => {
          scanFiles(entry, callback);
        })
      }, err => {
        console.log(err, err.message);
      })
    }
    // 如果是文件,安么添加到最后的文件数据集中
    if (entry.isFile) {
        i++
        entry.file(file => {
            callback(file, i);
        }, err => {
            console.log(err, err.message);
        })
    }
}

PS:

  1. 这里尤其要注意entry.file()方法,想要获取到文件信息只能在回调函数中获取
  2. 由于浏览器安全性问题,本地是不能直接访问文件系统的,所以,如果以上的例子不在服务端运行,会报错DOMException(这个问题花费了我N个小时),可以全局安装一个http-server来运行上面的代码

4. 总结

编程真的是一件很好玩的事情,最近看算法的基础,觉得真的很有意思,前端编程也一样,如果仅仅停留在使用组件上,真的很没意思,有时间可以多多看看各种原生的事件和方法,深入研究一下框架相当有意思。超级感谢MDN啊,基本上可以获取到所有想要的信息

完整DEMO的:https://github.com/PatrickLh/file-upload

5. 参考

MDN XMLHttpRequest

MDN File and Directory Entries API

MDN HTML Drag and Drop API

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容

  • 本文详细介绍了 XMLHttpRequest 相关知识,涉及内容: AJAX、XMLHTTP、XMLHttpReq...
    semlinker阅读 13,606评论 2 18
  • Ajax和XMLHttpRequest 我们通常将Ajax等同于XMLHttpRequest,但细究起来它们两个是...
    changxiaonan阅读 2,217评论 0 2
  • 看到标题时,有些同学可能会想:“我已经用xhr成功地发过很多个Ajax请求了,对它的基本操作已经算挺熟练了。” 我...
    前端渣渣阅读 5,752评论 1 12
  • 轻拨芦苇荡出笛声悠悠 撩拨了未到的秋 我在船头流连着那只白鸥 却想你说过的自由 那年桃花映的脸庞 微醺 轻叹若是岁...
    田老湿花痴爱写诗阅读 296评论 0 2
  • 做在地铁上,座位对面是地铁车窗,看着地铁车窗里反射的我的形象,帽子大衣包包牛仔裤,今天穿的一身算是我比较满意了。 ...
    nooooogod阅读 266评论 1 0