带文件的表单数据体长什么样你见过吗?

1,101 阅读5分钟

作者: 何梦洋

本文为原创内容转载请联系授权

微信:HMYang33

灵感闪现

我今天坐地铁时突然脑子里蹦出一个奇怪的问题,如果表单的method为get,那它还能发送一个5mb的文件吗?我问了几个QQ群给出的答案各不相同,有人说可以,有人说不行,还有说只能发一点点的。虽然给出的回答各不相同,但相同的是,我们都没有这么玩过。

同时我还好奇比较后端接受到的post表单数据体是什么样,如果涉及到多文件多字段,数据体是什么样的,如果不引入第三方npm模块,又该如何如何处理整个数据体。

问题列举

为了条理清晰,本文是我一番实验之后才开始撰写的。有些问题不需要研究,因为测试某个前者条件时就已经出错了。所以本文只撰写以下三个问题。

  1. post数据体长什么样?
  2. 如何处理一个表单数据体?
  3. 把method设置成get还能不能发文件,会出现什么情况?

1.post数据体长什么样?

对于只有ajax请求,post数据体一般是一个JSON文本,这个只要是做过前端的,都应该见过,在这里不多赘述。本文我们关注一下表单的数据体,它和JSON其实关系不大,甚至可以说没有关系。

  //使用http创建一个服务器,监听request事件
  //把首页设置为一个form表单
  let formHTML=`
  <!DOCTYPE html>
  <html>
    <head>
    </head>
    <body>
    <form action="/upload" method="post" enctype="multipart/form-data">
      <input type="file" name="fileA" >
      <input type="submit">
    </form>
    </body>
  </html>
  `
  if (req.url === "/") {
    res.setHeader("Content-Type", "text/html; charset=utf-8")
    return res.end(formHTML);
  }

然后我们要使用String.prototype.startsWith方法判断url是否为/upload开头,并在判断的分支中的代码块中接受表单的分片数据,把这些数据写到一个文件中。

  if (req.url.startsWith("/upload")) {
    let data = [];
    req.on("data", chunk => { data.push(chunk) })
    req.on("end", () => {
      fs.writeFileSync("recivedData", Buffer.concat(data))
      data = [];
    })
    return res.end("Upload success!")
  }

为什么我不用全等判断req.url?作为习惯了express框架的我最开始是用全等的,后来想起来get请求中会把form表单中的字段拼接到url上面,这样实际请求的url是/upload?a=b&c=d导致走不了判断。为了避免这种情况,我选择startsWith

为了测试方便,把服务器监听在80端口,启动之后我先是在桌面新建了一个文本文档,在里面写入了我的名字,选择文件点击提交之后查看recivedData文件内容如下图所示。

Snipaste_2022-12-12_15-16-38.png

粗略一看好像并不是很难理解,仔细一看确实不难理解

其中第一行和倒数第二行是分隔行,对于不同的浏览器生成的分隔文本也不太一样,但大多数都是几个短横线开头的。第二行是在简单的信息描述,包括字段名,文件名。第三行是文件的类型,对于新建文本文档.txt给出的文件类型就是text/plain。然后第四行是一个空行,之后是文件内容。

对于喜欢瞎搞研究的我肯定不会到此结束的,我还想看看多文件,文件加字段,纯字段的数据体是什么样的。

<input type="file" name="fileA" multiple>上传多文件,我选择了一个css文件和helloworld.vue

Snipaste_2022-12-12_15-16-38.png

细心的伙伴看到这张图可能就发现了什么,75行的末尾有两个短横线,和1行,12行不一样。没错,末尾带两个短横线的分隔行就表示数据传输完毕

添加两个文本字段试试

  <input type="text" name="myname">
  <input type="password" name="mypassword">

Snipaste_2022-12-12_15-16-38.png

那如果只留下文本字段呢,会出现JSON格式的数据吗?

答案是:保留文档格式

Snipaste_2022-12-12_15-16-38.png

为什么不是熟知的JSON?

这个问题就要说一下form标签中的enctype属性了,它表示编码类型的意思。有以下几种值可以设置。

  • application/x-www-form-urlencoded: 初始的默认值
  • multipart/form-data: 适用于使用<input> 标签上传文件
  • text/plain: HTML5 引入的类型

从创建这个html开始,本文测试使用的一直都是multipart/form-data,对于其他两个设置接受到的数据体,我也相应做了测试。

Snipaste_2022-12-12_15-16-38.png

注意:使用这两种方式上传文件时,会出问题,后端只能收到一个文件名,而且这个文件名是被encodeURI编码过的,对于文件内容,是收不到的。

如何处理这个表单数据?

上一个问题的研究中已经浏览过数据的内容,清楚了数据结构,所以做出一个像formidable那样的解码器并不难,当然本文中我并不打算做一个车轮,而是研究一下formidable的源码,了解一下重要的处理过程即可。

首先是我最感兴趣的,文件分割 源码中使用了Buffer.from创建了一个boundary,其中str是在请求头中获取的boundary字符串根据这个字符串去做分割。

  initWithBoundary(str) {
    this.boundary = Buffer.from(`\r\n--${str}`);
    this.lookbehind = Buffer.alloc(this.boundary.length + 8);
    this.state = STATE.START;
    this.boundaryChars = {};

    for (let i = 0; i < this.boundary.length; i++) {
      this.boundaryChars[this.boundary[i]] = true;
    }
  }

获取文件名源码中获取文件名的方式是正则匹配 反斜杠b表示单词的边界。如果获取不到,就说明这可能是一个普通的文本字段,不是文件。

  headerValue.match(
      /\bfilename=("(.*?)"|([^()<>{}[\]@,;:"?=\s/\t]+))($|;\s)/i,
  );

3.把method设置成get还能不能发文件,会出现什么情况?

123.gif get请求是不会触发req的data事件的,所以根本不会有数据体写入进来。同时就算设置了multipart/form-data,也还是会把表单数据拼接到url里面当作一个正常的get请求来发送。对于文件,只可以接收到文件名。