最近有一个小需求:上传文件的时候显示上传进度(上传了百分之多少)。这个需求不难,比如以下代码就能实现:
<!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,可以查看参考链接的源码。