文件上传进度和解析 Form Data

最近有一个小需求:上传文件的时候显示上传进度(上传了百分之多少)。这个需求不难,比如以下代码就能实现:

<!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,可以查看参考链接的源码。

参考链接

https://houbean.github.io/2017/02/22/Node-js-HTTP%E6%9C%8D%E5%8A%A1%E5%99%A8%E4%B8%AD%E7%9A%84%E6%96%87%E4%BB%B6%E3%80%81%E5%9B%BE%E7%89%87%E4%B8%8A%E4%BC%A0/

我们也可以看 formidable 的解析源码

Html5 File Upload with Progress

How does HTTP file upload work?

我要分享

曾小乱

作者: 曾小乱

喜欢写点有意思的东西

发表评论

电子邮件地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据