用js实现文件上传

一次面试的时候,面试官问我,有没有自己写过文件上传,不依赖任何模板,框架?因此今天特地将文件上传那些事进行总结梳理一下。

本文使用的例子使用nodejs作为后台,jade模板作为前台的来实现文件上传的功能。当然你用其他后台语言也是可以的,我相信道理都是一样的。下面将提到几个小例子,分为普通文件上传,异步上传,显示上传进度,图片预览,多文件上传,拖拽上传这几部分。

[TOC]

普通文件上传

面试的时候问你文件上传怎么实现,一般人都能答出来,使用input标签,并将type设置为file,同时将form表单设置为multipart/form-data.恩没错,就是这样,代码如下:

1
2
3
4
5
6
7
8
9
form(method="post" enctype="multipart/form-data")
fieldset
legend 文件上传
span 文件上传:
input#file1(
type='file'
name="file1"
)
button#submit(type="button") 文件上传

这样,当我们提交表单的时候,就会上传该文件了。

那后台怎么处理呢?后台采用的是nodejs推荐的multer模块。

multer的具体用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var multer = require('multer');
var storage = multer.diskStorage({
//给上传文件重命名,获取添加后缀名
filename : function (req, file, cb) {
var type = file.originalname.slice(file.originalname.lastIndexOf('.'));//截取后缀
var newName = Date.now()+type;
console.log(newName);
cb(null, newName);
},
//设置上传后文件路径
destination: function (req, file, cb) {
cb(null, 'uploads/')
},
})
//添加配置文件到multer对象
var upload = multer({
storage : storage
});
//使用
app.post('/upload',upload.single('file1'), (req, res) => {
console.log(req);
res.send('success');
})

这里需要在根目录下建一个uploads文件夹。

异步上传

其实在实际中,我们应该更推荐使用XHR异步上传(我猜的)。这里就不得不说下XMLHttpRequest level2,和level1相比呢,有以下改进:

  1. 支持接收二进制数据,提供xhr.overrideMimeType()方法,或者通过设置responseType=’blob’
  2. 可以上传文件, 可以使用FormData对象管理表单.
  3. 提供进度提示, 可通过 xhr.upload.onprogress 事件回调方法获取传输进度.
  4. 依然受 同源策略 限制, 这个安全机制不会变. XHR2新提供 Access-Control-Allow-Origin 等headers, 设置为 * 时表示允许任何域名请求,从而实现跨域CORS访问
  5. 可以设置timeout 及 ontimeout, 方便设置超时时长和超时后续处理.

详细请参考这里

那我们到底应该如何使用呢?诺,像下面这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function upload() {
var xhr = new XMLHttpRequest();
var formData = new FormData();
var fileInput = $("#file1")[0];
var file = fileInput.files[0];
formData.append('myFile', file);
formData.append('name','ssss');
xhr.open('POST', "/upload");
xhr.onload = function () {
if (this.status === 200) {
console.log("success");
}
else {
console.log(xhr.status);
}
}
xhr.send(formData);
}

提供进度条

html5提供了一个很好用的progress标签,通过设置total和value就可以显示进度了,并且level 2又恰好提供了onprogress回调函数,是不是感觉so easy呢

首先在jade中增加:

1
2
sapn 上传进度:
progress#progress(max=100)

js中,
在shr.send调用之前加上下面代码,

1
2
3
4
5
6
7
//上传进度模块
xhr.upload.onprogress = function (event) {
if(event.lengthComputable){
var val = (event.loaded / event.total) * 100;
$('#progress').attr('value',val)
}
}

其中事件的lengthComputable属性代表文件总大小是否可知,如果 lengthComputable 属性的值是 false,那么意味着总字节数是未知并且 total 的值为零。

图片预览

已经实现进度显示了(偷笑),那如何实现图片预览呢?幸运的是,html5新增的File API给我们提供了很大的帮助,这就是大名鼎鼎的FileReader。

h5给input type=file,类型的dom元素增加了files集合,通过文件输入选择一个或多个文件使,files将会包含一组File对象,因此我们可以采用var file = fileInput.files[0];这种模式获取我们选择的文件对象。同时FileReader还提供了

(1). reader.readAsDataURL(file)方法,将文件以数据URI的形式保存在reader对象的result中。

(2). 对象URL,使用对象URL的好处是可以不必把文件内容读取到js中,而直接引用文件内容。因此只要在需要文件内容的地方提供对象的URL即可。

下面就有两种方式实现图片预览

首先需要在jade中添加一个div用于预览图片

1
div#previewImg

js代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//图片预览——使用fileReader中的readAsDataURL方法,在reader.onload中为img.src赋值
function previewImage(file) {
var previewImg = $("#previewImg")[0];
var img = $("<img style='width: 400px; height: 400px'/>")[0];
var fileInput = $('#file1')[0];
var file = fileInput.files[0];
img.file = file;
previewImg.appendChild(img);
var reader = new FileReader();
reader.onload = (function (aImg) {
return function (e) {
aImg.src = e.target.result;
}
})(img);
reader.readAsDataURL(file);
}

第二种方式如下:
createObjectURL(file),返回的是一个字符串,指向一块内存的地址,直接把对象的url放在img标签中,img标签会找到相应的内存地址,直接读取数据并将图像显示在页面中。如果不再需要相应的数据,最好释放它占用的内容。胆只用有代码在引用对象URL,内存就不会释放。要手工释放内训,就可以调用window.URL.revokeObjectURL()方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//兼容性的读取对象URL方法
function createObjectURL(blob){
if(window.URL){
return window.URL.createObjectURL(blob);
}
else if(window.webkitURL){
return window.webkitURL.createObjectURL(blob);
}
else {
return null;
}
}
//兼容性的释放数据占用内存的方法
function revokeObjectURL(url) {
if(window.URL){
window.URL.revoleObjectURL(url);
}
else if(window.webkitURL){
window.webkitURL.revokeObjectUrl(url);
}
}

1
2
3
4
5
6
7
8
9
10
11
12
//第二种图片预览的方法——直接使用createObjectURL(file)读取对应file的url,并将url赋值给img.src。通过查看img的src可以发现二者其实是不同的
function previewImage2(file) {
var previewImg = $("#previewImg")[0];
var img = $("<img style='width: 200px; height: 200px'/>")[0];
//var fileInput = $('#file1')[0];
//var file = fileInput.files[0];
img.src = createObjectURL(file);
img.onload = function () {
revokeObjectURL(this.src);
}
previewImg.appendChild(img);
}

这样图片预览便完美实现了,如果我们想要多个文件预览呢?

多文件预览

多文件预览首先需要设置input的 multiple属性

1
2
3
4
5
input#file1(
type='file'
name="file1"
multiple
)

然后通过遍历fileInput.files中的文件,再依次调用previewImage(file)方法即可。

1
2
3
4
5
6
7
8
9
10
//多文件预览
function multipreview() {
var fileInput = $('#file1')[0];
var files = fileInput.files;
var formData = new FormData();
for(var i = 0; i < files.length; i++){
var file = files[i];
previewImage2(file);
}
}

多文件上传

多文件上传通过遍历fileInput.files属性,并将file依次加入formData中即可, 在后台处理中调用upload.array()方法即可。

js代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//多文件上传
function multiUpload() {
var xhr = new XMLHttpRequest();
var fileInput = $('#file1')[0];
var files = fileInput.files;
var formData = new FormData();
console.log(123);
console.log(files.length);
for (var i = 0; i < files.length; i++) {
var file = files[i];
formData.append('files[]', file, file.name);
}
xhr.open('POST', "/upload");
xhr.onload = function () {
if (this.status === 200) {
console.log("success");
}
else {
console.log(xhr.status);
}
}
//上传进度模块
xhr.upload.onprogress = function (event) {
if (event.lengthComputable) {
var val = (event.loaded / event.total) * 100;
$('#progress').attr('value', val)
}
}
xhr.send(formData);
}

后台稍微改变了一点点:

1
2
3
4
/处理多个文件的上传
app.post('/upload',upload.array('files[]'), (req, res) => {
res.send('success');
})

注意array中的参数一定要和formdata中的key值对应。

文件拖拽上传

h5中新增了文件拖拽api,当我们把拖拽元素(本例子中的图片文件)拖拽到目标区域(input type=file)上时,会依次触发以下事件:dragenter,dragover, dragleave或drop。因此,我们需要给input type=”file”绑定对应的处理事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
//实现文件拖拽的上传
function dragUpload(){
var dropTarget = $('#file1');//拖拽的目标当然是input type="file",当然也可以是其他的div之类的。
function handleEvent (e) {
e.stopPropagation();
e.preventDefault();
var files = e.originalEvent.dataTransfer.files,
data, xhr, i, length;
if(e.type === 'drop'){
length = files.length;
xhr = new XMLHttpRequest();
data = new FormData();
i = 0;
while (i < length) {
data.append('files[]', files[i]);
i++;
}
xhr.open('POST', "/upload");
xhr.onload = function () {
if (this.status === 200) {
console.log("success");
}
else {
console.log(xhr.status);
}
};
//上传进度模块
xhr.upload.onprogress = function (event) {
if (event.lengthComputable) {
var val = (event.loaded / event.total) * 100;
$('#progress').attr('value', val)
}
};
xhr.send(data);
}
}
dropTarget.on('dragenter', handleEvent);
dropTarget.on('dragover', handleEvent);
dropTarget.on('drop', handleEvent);
}

参考

https://segmentfault.com/p/1210000009289556/read

http://www.ruanyifeng.com/blog/2012/09/xmlhttprequest_level_2.html