最近有一个小需求:上传文件的时候显示上传进度(上传了百分之多少)。这个需求不难,比如以下代码就能实现:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Upload file</title>
<script>
function uploadFile() {
var fd = new FormData(document.forms[0]);
var xhr = new XMLHttpRequest();
xhr.upload.onprogress = function (e) {
console.log('e.loaded:' + e.loaded + ', e.total:' + e.total)
console.log(Math.round(e.loaded / e.total * 100) + '%') // 这里是上传进度
}
xhr.onreadystatechange = function(){
if ( xhr.readyState === 4 ) {
if ( xhr.status >= 200 && xhr.status < 300 || xhr.status === 304 ) {
console.log(xhr.responseText)
}
}
}
xhr.open("POST", "/");
xhr.send(fd);
}
</script>
</head>
<body>
<form action="/" method="POST" enctype="multipart/form-data">
<input type="file" name="file" id="file" required />
<input type="hidden" value="1" name="hidden">
<input type="text" name="text">
<input type="range" name="range">
<button onclick="uploadFile();" type="button" id="upload">Upload</button>
</form>
</body>
</html>
进度条实现方式
监听 onprogress 事件,其中 e.loaded 表示已经上传了多少,e.total 表示总文件大小。如此一来,问题就解决了,进入舒适区。
有一个前提就是我们必须用 FormData 来发送文件,这样才能异步上传不刷新页面。用这种方式,我们来看看浏览器把什么数据发给了服务器:
------WebKitFormBoundaryVWIczxQjgJ1ieO2v Content-Disposition: form-data; name="file"; filename="BaiduNetdisk_6.4.0.6.exe" Content-Type: application/x-msdownload ------WebKitFormBoundaryVWIczxQjgJ1ieO2v Content-Disposition: form-data; name="hidden" 1 ------WebKitFormBoundaryVWIczxQjgJ1ieO2v Content-Disposition: form-data; name="text" ------WebKitFormBoundaryVWIczxQjgJ1ieO2v Content-Disposition: form-data; name="range" 50 ------WebKitFormBoundaryVWIczxQjgJ1ieO2v--
至于怎么理解这种格式,我们稍后再说,可能也不会说。
解析 Form Data 格式数据
我们先在服务器拿到文件。我印象中在 PHP 里文件的数据放在一个全局变量里,直接拿来用就行了,但是在 nodejs 里情况有点复杂,我写了一个错误解析的代码,如下:
function parseFile2 (req, res) {
var body = ''
req.setEncoding('binary')
const boundary = req.headers['content-type'].split('; ')[1].replace(/boundary=-+/, '')
req.on('data', function (chunk) {
body += chunk
})
req.on('end', function () {
res.setHeader('Content-Type', 'application/json')
var reg = new RegExp('-+' + boundary, 'gm')
var bodyArr = body.split(reg)
var obj = {}
fs.writeFileSync('./test.json', JSON.stringify(bodyArr))
bodyArr.forEach((item) => {
var name
var i
var length
var entries
if (item.indexOf('Content-Disposition') > -1) {
var single = querystring.parse(item, '\r\n', ': ')
var disposition = single['Content-Disposition']
if (disposition.indexOf('name="file"') > -1) {
var filename = disposition.match(/filename="(.*)"/)[1]
entries = Object.entries(single)
for (i = 0, length = entries.length; i < length; i++) {
if (!entries[i][1]) {
var binaryData = entries[i][0]
}
}
fs.writeFile('./uploads/' + filename, binaryData, 'binary', function (err) {
if (err) {
console.log(err)
}
})
} else {
name = disposition.match(/name="(.*)"/)[1]
entries = Object.entries(single)
for (i = 0, length = entries.length; i < length; i++) {
if (!entries[i][1]) {
obj[name] = entries[i][0]
}
}
}
}
})
res.end(JSON.stringify(obj))
})
}
上面的代码解析文件会失败,但是字符串没有问题,看来解析二进制的文件没有我想的那么简单。那我就是使用了一个叫 formidable 的第三方依赖,用起来很简洁:
function parseFile3 (req, res) {
var form = new formidable.IncomingForm()
form.uploadDir = './uploads/'
form.parse(req, function (err, fields, files) {
if (err) {
console.log(err)
}
res.writeHead(200, { 'content-type': 'text/plain' })
res.write('received upload:\n\n')
res.end(util.inspect({ fields: fields, files: files }))
})
}
总结
在 node 里,使用像 formidable 这种第三方库,比较方便。想知道怎么具体解析包含二进制数据的 FormData,可以查看参考链接的源码。
