liao一下JS的上传功能
讲真:我拼尽了一身的力气,过着平凡的一生。
拼命努力不是为了想做什么就做什么,只是为了想不做什么就不做什么。
前端开发绝对离不开上传功能,无论你开发的项目是什么类型都是如此。上传头像,图片,文档,PPT,导入表格,视频等等。
所以这门技术必须要掌握!我刚做前端的时候非常不喜欢上传下载,感觉相比操作数据来说,操作文档真的挺烦,但是又离不开,总是要做的。毕竟后端比你还烦,哈哈。
话不多说,开giao!
input标签实现上传功能
<input type="file"/>
目前通过浏览器上传的根本都是用input标签,所以万变不离其宗。用它!
想一下,实际开发中我们在做上传功能时,会触及那些需求:
- 各种千奇百怪的上传样式
- 限制类型
- 限制上传文件大小
- 获取上传图片的实际宽高
- 图片演示
- 支持多文件上传
- 上传至服务器
- 显示上传进度
很恶心哈,不过现在有很多主流的框架都封装好了这些功能,可以直接用,这些我们在这里就不提了,这里只讲述单纯的使用JS实现文件的上传。
好的,现在我们来解决以上需求,首先,要知道的是input type="file"上传标签拥有哪些属性:
属性名称 | 描述 |
value | 用户指定的上传文件名称 |
required | 是否为表单必填项 |
accept | 允许上传的文件类型 |
multiple | 是否上传多个文件 |
files | 已选择的文件列表 |
capture | 捕获的图片或视频源 |
然后再看,它拥有的事件:
事件名称 | 描述 |
onchange | 上传文件发生变化时触发 |
了解了这些之后,开始开发!
1. 样式问题
大家都知道input type="file"各浏览器的默认样式都很丑哈,肯定是不能直接使用的,一般我们伟大的UI设计师给我们的图图都是这样这样那样那样的:
所以这个时候,我们丑丑的input标签就需要隐藏起来了,点击上边这些图图后主动去触发input标签的点击事件。
大概酱:
<div class="your-style" onclick="this.nextElementSibling.click()">
<input type="file" style="display: none;"/>
关于nextElementSibling和nextSibling的区别,可以看最下边哈。
第一个问题解决,点击你设计好的样式的div或者其它元素,都可以弹出我们的上传选择文件框。
2. 限制类型
限制上传的文件类型。这个需求很常见哈,比如你想上传个头像,那就不能上传文档和视频吧,所以限制用户上传的文件类型还是很重要的,这时候,我们用到的就是原生的accept属性了。上面的表格中有介绍哈。
accept属性值为一个MIME 类型字符串,它规定了input能接受的上传文件类型。多个值的话用逗号分隔。(常见的MIME类型可以见最下边的拓展)
咱们举例说明一下:
<!-- 只允许上传png格式的图片 -->
<input type="file" accept=".png"/>
<input type="file" accept="image/png"/>
<!-- 允许上传固定几种格式的图片 -->
<input type="file" accept=".png, .gif, .jpg"/>
<input type="file" accept="image/png, image/git, image/jpg"/>
<!-- 允许上传所有类型的图片 -->
<input type="file" accept="image/*"/>
到此,限制上传文件类型的功能已经搞定了。当然如果这种方式不能满足你的需求,也可以在上传文件后获取文件的路径,然后判断文件的类型是否满足您的需求。下面会提及这个问题。
3. 限制大小
限制上传文件的大小。这个功能也非常常见和实用。
限制上传文件的大小,首先要知道的是用户选中文件的大小是多少,那么这个操作一定是在用户选中文件之后进行的。所以这个判断要在input的onchange函数中实现。
<input type="file" onchange="changeFile()" id="inputFile">
function changeFile() {
let fileDom = document.getElementById('inputFile');
// 获取上传文件路径,可以根据这个路径判断是否满足您特殊的文件类型的限制。
console.log(fileDom.value); // C:\fakepath\jiayou.gif
if (fileDom.value.indexOf('.gif') > 0) {
console.log('不支持上传.gif类型的文件!');
return false;
}
// 获取上传文件的大小,然后进行文件大小限制的判断。
console.log(fileDom.files[0].size); // 获取上传文件大小,这个上传文件的大小是以字节为单位的(byte)
// 一般我们的需求都是以MB为单位的,所以这里需要换算一下
if (fileDom.files[0].size > 1 * 1024 * 1024) {
alert('上传文件不得超过1M!');
return false;
}
}
4. 上传图片(实际宽高以及图片展示)
上传图片经常碰到的两个问题,就是需要根据图片的实际宽高来设置图片的适配比例。所以我们需要知道如何获取上传图片的实际宽高。
<input type="file" onchange="changeFile()" id="inputFile">
<img id="uploadImg" src=""/>
function changeFile() {
let fileDom = document.getElementById('inputFile');
let imgDom = document.getElementById('uploadImg'); // 页面的img对象
let reader = new FileReader(); // 读取计算机文件对象,详见下边扩展
reader.readAsDataURL(fileDom.files[0]);
reader.onload = (e) => {
let img = new Image(); // 创建图片对象实例
img.src = e.target.result; // 设置图片的src为上传的图片
img.onload = () => {
console.log(img.width); // 750 ,得到上传图片的实际宽度为750px
console.log(img.height); // 1280 ,得到上传图片的实际高度为1280px
}
imgDom.src = e.target.result; // 给img标签赋值,图片就能显示啦!
}
}
5. 多文件上传
input标签原生就是支持多文件上传的,只需要设置multiple属性即可。如下:
<input type="file" multiple onchange="changeFile()" id="inputFile">
function changeFile() {
let fileDom = document.getElementById('inputFile');
let reader = new FileReader();
let list = Array.from(fileDom.files); // 需要先将多文件对象转换成数组格式
list.forEach(file => {
reader.readAsDataURL(file);
reader.onload = (e) => {
// 你的代码
console.log(e.target.result);
}
});
}
6. 上传至服务器并显示上传进度
上传至服务器,对于前端来说,只需要确定好,后端需要的文件类型即可。
已知 FileReader 读取文件的方式有:readAsArrayBuffer, readAsBinaryString, readAsDataURL和readAsText。所以根据后端接口需要的文件类型,设置文件的读取方式,然后传过去就行啦。
<input type="file" multiple onchange="changeFile()" id="inputFile">
function changeFile() {
let fileDom = document.getElementById('inputFile');
let file = fileDom.files[0];
let reader = new FileReader();
reader.readAsArrayBuffer(file); // 比如后端接口需要的文件是ArrayBuffer的格式
reader.onload = (e) => {
console.log(e.target.result);
};
}
执行结果:把这个结果作为参数传过去就可以啦。
那接下来问题就来了,我们怎么获得上传的进度呢?
这个基本原理就是监听XMLHttpRequest对象的onprogress,即xhr.upload.onprogress。
当然了各种框架都对xhr进行了封装,所以你可以根据使用的框架选择不同的方法,不过原理都是一致的,在这里简单的说三种方法:关于进度条的样式问题,很多UI框架都是可以直接用的,没有的话,也可以自己写,很简单,外边一个div,里边一个div,设置不同的颜色,动态改变里边div的宽度就可以啦。如下图:
代码:
- js通过xhr监听文件上传进度
function changeFile() {
let fileDom = document.getElementById('inputFile');
let file = fileDom.files[0];
let reader = new FileReader();
reader.readAsArrayBuffer(file);
let progressBar = document.getElementById('progressBar'); // 进度条(里面的div)
reader.onload = (e) => {
let form = mew FormData();
form.append('file', e.target.result);
let xhr = new XMLHttpRequest(); // 创建XMLHttpRequest对象实例
xhr.open('post', '/url/upload'); // 设置http请求的方式和地址
xhr.onreadystatechange = function () { // 请求完成时的回调函数
if (xhr.status === 200){
progressBar.style.width = '100%'; // 设置进度条宽度为100%,上传成功。
} else {
console.log('上传出错!');
}
};
xhr.upload.onprogress = function (e) { // 监听上传进度
if (event.lengthComputable){
progressBar.style.width = e.loaded / e.total * 100 + '%';
// 修改进度条宽度百分比
// e.loaded 为当前已上传的大小,e.total为文件总大小。
}
};
xhr.send(form); // 执行上传
};
}
- jQuery通过ajax获取文件上传进度
let form = mew FormData();
form.append('file', e.target.result);
$.ajax({
url: "/url/upload",
type: "POST",
data: form,
processData: false, // 必须false才会避开jQuery对 formdata 的默认处理
contentType: false, // 必须false才会自动加上正确的Content-Type
xhr: function(){
myXhr = $.ajaxSettings.xhr();
if(myXhr.upload){
myXhr.upload.addEventListener('progress',function(e) {
progressBar.style.width = e.loaded / e.total * 100 + '%';
}, false);
}
return myXhr;
},
success: function(res){
// 请求成功
},
error: function(res) {
// 请求失败
console.log(res);
}
});
- axios+vue上传文件显示进度
随便挑了一个当前主流的前端框架举个例子。
methods: {
uploadFile: function (e) {
let formData = new FormData();
formData.append("file", file);
let config = {
onUploadProgress: function (e) { // 监听上传进度
// 属性lengthComputable主要表明总共需要完成的工作量和已经完成的工作是否可以被测量
if (e.lengthComputable) {
progressBar.style.width = e.loaded / e.total * 100 + '%';
}
}
};
axios.post("/url/upload", formData, config).then(function (data) {
console.log('上传成功!');
}).catch(function (err) {
console.log(err);
});
}
}
其实原理都是一样的,本质上都是对xhr对象做的upload.onprogress的监听。
到此,前端关于上传的基本知识都捋了一遍了。那么老规矩,giao点乱七八糟的知识点玩玩……
拓展
1.多次上传同一文件也想触发onchange事件的解决办法。
这个需求,其实在实际开发中经常遇到,反正我是经常碰到。
一般是已经选中了上传文件,然后又重新选中该文件上传,这时input会认为上传的文件没有发生变化,即input的value没有发生改变,所以不会触发onchange事件,所以解决的方案就是:选中文件后,将input的value值设置为null即可,代码如下:
document.getElementById('uploadfile').value = null;
2. nextElementSibling和nextSibling的区别是什么?
这个还是需要了解一下的,毕竟浏览器自带的input file的样式太丑了,基本我们都会将其隐藏的,但是还得触发它的点击事件。
- nextSibling属性返回元素节点之后的兄弟节点(包括文本节点、注释节点(包括回车、换行、空格、文本等等));
- nextElementSibling属性只返回元素节点之后的兄弟元素节点(不包括文本节点、注释节点);
什么意思呢?咱们举例说明一下,看下边两段代码:
<div class="your-style" onclick="console.log(this.nextSibling);"><!--123-->
<input type="file" style="display: none;"/>
执行一下:
<div class="your-style" onclick="console.log(this.nextElementSibling);"><!--123-->
<input type="file" style="display: none;"/>
执行一下:
这回看出区别了吧,nextSibling是当前元素紧跟着的节点,无论紧跟着的是空格,回车,还是注释,反正只要是紧跟着的就是你了。
nextElementSibling就是真正的下一个兄弟节点。ok!就这么回事。
3. 常见的MIME类型(通用型)?
MIME(Multipurpose Internet Mail Extensions)媒体类型。
上面说到我们在限制文件的上传类型的时候,可以通过accept属性来做限制,而该属性值为一个MIME 类型字符串。那么常见的MIME类型字符串有哪些呢?
MIME类型 | 举例 |
超文本标记语言文本 | .html text/html |
xml文档 | .xml text/xml |
XHTML文档 | .xhtml application/xhtml+xml |
普通文本 | .txt text/plain |
RTF文本 | .rtf application/rtf |
PDF文档 | .pdf application/pdf |
Microsoft Word文件 | .word application/msword |
PNG图像 | .png image/png |
GIF图形 | .gif image/gif |
JPEG图形 | .jpeg,.jpg image/jpeg |
au声音文件 | .au audio/basic |
MIDI音乐文件 | mid,.midi audio/midi,audio/x-midi |
RealAudio音乐文件 | .ra, .ram audio/x-pn-realaudio |
MPEG文件 | .mpg,.mpeg video/mpeg |
AVI文件 | .avi video/x-msvideo |
GZIP文件 | .gz application/x-gzip |
TAR文件 | .tar application/x-tar |
任意的二进制数据 | application/octet-stream |
4. 关于FileReader
FileReader 对象允许Web应用程序异步读取存储在用户计算机上的文件(或原始数据缓冲区)的内容,使用 File 或 Blob 对象指定要读取的文件或数据。
已知FileReader提供了以下读取方式:
读取方式 | 描述 |
readAsDataURL | 读取文件内容,结果用data:url的字符串形式表示 |
readAsArrayBuffer | 按字节读取文件内容,结果用ArrayBuffer对象表示 |
readAsBinaryString | 按字节读取文件内容,结果为文件的二进制串 |
readAsText | 按字符读取文件内容,结果用字符串形式表示 |
当 FileReader 读取文件的方式为 readAsArrayBuffer, readAsBinaryString, readAsDataURL 或者 readAsText 的时候,会触发一个 load 事件。从而可以使用 FileReader.onload 属性对该事件进行处理。
我们一般都是在onload的回调函数中,对e.target.result进行操作。
当然,FIleReader还有一些其它的事件和方法,这里简要的提一下,有兴趣的自己去查。
注:以下内容部分摘抄自MDN。
FileReader的时间处理:
FileReader.onabort —— 处理abort事件。该事件在读取操作被中断时触发。
FileReader.onerror —— 处理error事件。该事件在读取操作发生错误时触发。
FileReader.onload —— 处理load事件。该事件在读取操作完成时触发。
FileReader.onloadstart —— 处理loadstart事件。该事件在读取操作开始时触发。
FileReader.onloadend —— 处理loadend事件。该事件在读取操作结束时(要么成功,要么失败)触发。
FileReader.onprogress —— 处理progress事件。该事件在读取Blob时触发。
FileReader的支持的方法:
FileReader.abort() —— 中止读取操作。在返回时,readyState属性为DONE。
FileReader.readAsArrayBuffer() —— result 属性中保存的将是被读取文件的 ArrayBuffer 数据对象。
FileReader.readAsBinaryString() —— result 属性中保存的将是被读取文件的原始二进制数据。
FileReader.readAsDataURL() —— result 属性中保存的将是被读取文件的URL格式的Base64字符串。
FileReader.readAsText() —— result 属性中保存的将是一个字符串(表示所读取的文件内容)。
以上四个方法都是用来设置读取文件的方式,开始读取指定的 Blob中的内容, 一旦完成,e.target.result 属性中保存的将是被读取文件对应类型的数据对象。
5. 关于XMLHttpRequest
xhr对象用于与服务器进行交互,通过XMLHTTPRequest可以在不刷新页面的情况下请求特定的url,获取数据,允许在不影响用户操作的情况下,更新页面的局部内容。
xhr的属性:
XMLHttpRequest.readyState: (返回一个xhr代理当前所处的状态。)
一个xhr代理总是处于下列状态列表中的一个
状态码 | 含义 | 说明 | 实例 |
0 | unset | 创建xhr对象,但是尚未初始化 | let xhr = new XMLHttpRequest(); |
1 | opened | 载入,调用open方法 | xhr.open(method,url,true); |
2 | headers_recieved | send方法已经被调用,响应头也已经被接收 (但是此阶段的数据为原始数据,客户端不能直接使用) | xhr.send(params); |
3 | loading | 解析接收到的服务器端响应数据下载中 | |
4 | done | 请求操作已经完成。 |
注:下面内容部分摘抄自MDN。
XMLHttpRequest.onreadystatechange (readyState发生改变时触发)。所有的浏览器都支持onreadystatechange 。后来许多浏览器为了方便,又提供了一些额外的事件(onload、onerror、onprogress 等)。
还有一些属性,简单说一下:
XMLHttpRequest.response —— 获取请求的响应
XMLHttpRequest.responseType —— 一个用于定义响应类型的枚举值
XMLHttpRequest.timeout —— 设置请求超时时间
XMLHttpRequestEventTarget.ontimeout —— 请求超时的触发器
XMLHttpRequest.upload —— 上传进度
XMLHttpRequest.withCredentials —— 布尔值,用来指定跨域 Access-Control 请求是否应当带有授权信息
xhr的方法:
XMLHttpRequest.abort() —— 中止请求(我有一次面试被问过这个,但当时我特么说的是bort(),我也是……)
XMLHttpRequest.getAllResponseHeaders() —— 以字符串的形式返回所有用 CRLF 分隔的响应头
XMLHttpRequest.getResponseHeader() —— 返回包含指定响应头的字符串
XMLHttpRequest.open() —— 初始化一个请求。
XMLHttpRequest.overrideMimeType() —— 覆写由服务器返回的 MIME 类型。
XMLHttpRequest.send() —— 发送请求。
XMLHttpRequest.setRequestHeader() —— 设置 HTTP 请求头的值。