未分类

Ajax文件上传

为了避免在实现简单的异步文件上传功能时候引入一个第三方库文件的尴尬情形(库文件可能造成多余的开销,拉低应用加载速度,尤其是在引入库文件之后仅使用其中一两个功能的情况下,性价比极低),最近了解了一下文件异步上传的实现原理,顺带看了看进度条、图片预览等功能的实现,做一点简单的整理。

本文所用的代码示例可以从github仓库 file-upload-demo 中找到。

文件上传

HTML结构如下,一个file input和一个button。当点击“上传”按钮的时候,将file input选中的文件上传到服务器。

1
2
<input type="file" name="file" id="file" />
<button id="upload">上传</button>

以下是“上传”按钮的点击事件处理器,点击按钮之后通过一个XMLHttpRequest对象来实现发送异步请求。上传的内容为文件,因此还需要用到FormData对象,FormData可以js里面创建表单对象,将file input的文件appendFormData对象中,最后调用XHR对象的send()方法将表单数据发送出去即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var file = document.querySelector('#file');
var upload = document.querySelector('#upload');
var xhr = new XMLHttpRequest();

// 点击上传
function uploadFile(event) {
var formData = new FormData();
formData.append('test-upload', file.files[0]);
xhr.onload = uploadSuccess;
xhr.open('post', '/upload', true);
xhr.send(formData);
}

// 成功上传
function uploadSuccess(event) {
if (xhr.readyState === 4) {
console.log(xhr.responseText);
}
}

上传进度

在进行文件上传的时候,xhr对象会有一个upload属性,会提供一个progress事件,在相应的事件处理器里面通过事件对象可以知道当前的上传进度,利用这个特点可以很方便地实现进度条或者进度提示。

1
2
3
<input type="file" name="file" id="file" />
<button id="upload">上传</button>
<span id="progress">0%</span>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var progress = document.querySelector('#progress');

// 点击上传
function uploadFile(event) {
var formData = new FormData();
formData.append('test-upload', file.files[0]);
xhr.onload = uploadSuccess;
xhr.upload.onprogress = setProgress;
xhr.open('post', '/upload', true);
xhr.send(formData);
}

// 进度条
function setProgress(event) {
if (event.lengthComputable) {
var complete = Number.parseInt(event.loaded / event.total * 100);
progress.innerHTML = complete + '%';
}
}

图片预览

上传图片的时候可以利用FileReader对象来实现图片预览。FileReader可以异步读取用户电脑上的文件,将file input选中的文件传给FileReader,读取之后取得文件的URL并设置为image元素的src即可让选中的图片文件显示出来。

1
2
3
4
<input type="file" name="file" id="file" />
<button id="upload">上传</button>
<span id="progress">0</span>
<img id="image" src="" width="200" />

1
2
3
4
5
6
7
8
9
10
11
var file = document.querySelector('#file');
file.addEventListener('change', previewImage, false);

// 图片预览
function previewImage(event) {
var reader = new FileReader();
reader.onload = function (event) {
image.src = event.target.result;
};
reader.readAsDataURL(event.target.files[0]);
}

服务端处理

使用express搭建一个简单的NodeJS服务端,提供上传文件的接口。express要支持文件上传需要用到中间件,在express官网上有很多介绍。这里我使用的是multer中间件,下面是简单的使用示例。upload.single表示这个接口接受的上传文件数量为1个,'test-upload'限制了上传的表单数据的键为'test-upload'formData.append('test-upload', file.files[0]);)。经过这个中间件处理之后,通过req.file可以访问到文件的相关信息,上传的文件存放在uploads文件夹中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const upload = require('multer')({ dest: 'uploads/' });

app.post('/upload', upload.single('test-upload'), (req, res) => {
// 没有附带文件
if (!req.file) {
res.json({ ok: false });
return;
}

// 输出文件信息
console.log('====================================================');
console.log('fieldname: ' + req.file.fieldname);
console.log('originalname: ' + req.file.originalname);
console.log('encoding: ' + req.file.encoding);
console.log('mimetype: ' + req.file.mimetype);
console.log('size: ' + (req.file.size / 1024).toFixed(2) + 'KB');
console.log('destination: ' + req.file.destination);
console.log('filename: ' + req.file.filename);
console.log('path: ' + req.file.path);
});

终端输出

由输出可以看到,文件的命名使用一个哈希值表示,并且去除了后缀名,想要保持文件的原有的命名格式,需要再通过fs对文件进行改名。

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
app.post('/upload', upload.single('test-upload'), (req, res) => {
// 没有附带文件
if (!req.file) {
res.json({ ok: false });
return;
}

// 输出文件信息
console.log('====================================================');
console.log('fieldname: ' + req.file.fieldname);
console.log('originalname: ' + req.file.originalname);
console.log('encoding: ' + req.file.encoding);
console.log('mimetype: ' + req.file.mimetype);
console.log('size: ' + (req.file.size / 1024).toFixed(2) + 'KB');
console.log('destination: ' + req.file.destination);
console.log('filename: ' + req.file.filename);
console.log('path: ' + req.file.path);

// 重命名文件
let oldPath = path.join(__dirname, req.file.path);
let newPath = path.join(__dirname, 'uploads/' + req.file.originalname);
fs.rename(oldPath, newPath, (err) => {
if (err) {
res.json({ ok: false });
console.log(err);
} else {
res.json({ ok: true });
}
});
});

完整代码

ajax异步文件上传、进度显示、图片预览

1
2
3
4
<input type="file" name="file" id="file" />
<button id="upload">上传</button>
<span id="progress">0</span>
<img id="image" src="" width="200" />

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
41
42
43
44
45
46
(function () {
'use strict';

var file = document.querySelector('#file');
var upload = document.querySelector('#upload');
var progress = document.querySelector('#progress');
var image = document.querySelector('#image');
var xhr = new XMLHttpRequest();

upload.addEventListener('click', uploadFile, false);
file.addEventListener('change', previewImage, false);

// 点击上传
function uploadFile(event) {
var formData = new FormData();
formData.append('test-upload', file.files[0]);
xhr.onload = uploadSuccess;
xhr.upload.onprogress = setProgress;
xhr.open('post', '/upload', true);
xhr.send(formData);
}

// 成功上传
function uploadSuccess(event) {
if (xhr.readyState === 4) {
console.log(xhr.responseText);
}
}

// 进度条
function setProgress(event) {
if (event.lengthComputable) {
var complete = Number.parseInt(event.loaded / event.total * 100);
progress.innerHTML = complete + '%';
}
}

// 图片预览
function previewImage(event) {
var reader = new FileReader();
reader.onload = function (event) {
image.src = event.target.result;
};
reader.readAsDataURL(event.target.files[0]);
}
})();

express服务器提供文件上传接口

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
41
42
43
44
45
46
47
48
49
50
const express = require('express');
const upload = require('multer')({ dest: 'uploads/' });
const path = require('path');
const fs = require('fs');
const port = 8080;

let app = express();

app.set('port', port);
// index.html, index.js放在static文件夹中
app.use(express.static(path.join(__dirname, 'static')));

app.get('*', (req, res) => {
res.redirect('index.html');
});

app.post('/upload', upload.single('test-upload'), (req, res) => {
// 没有附带文件
if (!req.file) {
res.json({ ok: false });
return;
}

// 输出文件信息
console.log('====================================================');
console.log('fieldname: ' + req.file.fieldname);
console.log('originalname: ' + req.file.originalname);
console.log('encoding: ' + req.file.encoding);
console.log('mimetype: ' + req.file.mimetype);
console.log('size: ' + (req.file.size / 1024).toFixed(2) + 'KB');
console.log('destination: ' + req.file.destination);
console.log('filename: ' + req.file.filename);
console.log('path: ' + req.file.path);

// 重命名文件
let oldPath = path.join(__dirname, req.file.path);
let newPath = path.join(__dirname, 'uploads/' + req.file.originalname);
fs.rename(oldPath, newPath, (err) => {
if (err) {
res.json({ ok: false });
console.log(err);
} else {
res.json({ ok: true });
}
});
});

app.listen(port, () => {
console.log("[Server] localhost:" + port);
});

分享到