为了避免在实现简单的异步文件上传功能时候引入一个第三方库文件的尴尬情形(库文件可能造成多余的开销,拉低应用加载速度,尤其是在引入库文件之后仅使用其中一两个功能的情况下,性价比极低),最近了解了一下文件异步上传的实现原理,顺带看了看进度条、图片预览等功能的实现,做一点简单的整理。
本文所用的代码示例可以从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
的文件append
到FormData
对象中,最后调用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); 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); });