纵横研究院NodeJS技术专题社区

node原生处理前端传送的数据

2020-01-01  本文已影响0人  Ange_057c

简单的 node 服务

const http = require('http'); // 引入 http 模块
let server = http.createServer(() => { // 创建服务,生成实例
  console.log('请求来了')
});
server.listen('8000'); // 监听服务

启动 node 服务后,每次前端访问8000端口,后台都可以监控到。

接收浏览器的 GET 数据

在 createServer方法 的回调函数中,会有 request(请求对象) 和 response(响应对象) 两个参数供我们使用。

requerst 对象的属性有很多。

名称 含义
complete 前端请求是否发送完成
httpVersion http 协议版本
method http 请求方式
url 原始的请求路径
headers http 请求头
trailers http 请求尾
connection 当前 http 连接套接字,为net.Socket的实例
socket connection 属性别名
client client 属性别名

在前端页面中用一个表单来模拟 GET 请求

<form method="GET" action="http://localhost:8000/login">
  用户名: <input type="text" name="username" /><br />
  密码: <input  type="password" name="password" /><br />
  <input type="submit" value="提交" />
</form>

request 对象中我们比较常用的是 url 和 method。点击提交后,打印出对应的结果。

http.createServer((req, res) => {
  console.log(req.method) // GET
  console.log(req.url);   // /login?username=ang&password=123
}).listen('8000');

可以引入 url 模块对 req.url 进行解析

let { pathname, query } = url.parse(req.url, true);
console.log(pathname);  // /login
console.log(query);     // { username: 'ang', password: '123' }

接收浏览器的 POST 数据(不涉及文件上传)

post 方式传送数据和 get 是有区别的,当数据量较大时不能同时全部传过去,而是分块传送。

http.createServer((req, res) => {
  let body = []; // 接收 post 数据

  // 当有数据传送过来时,将其推入数组
  req.on('data', chuck => { // chuck 是二进制数据
    body.push(chuck);
  });

  // 所有的数据接收完成触发
  req.on('end', () => {
    // 将二进制数组拼接成一个 Buffer 对象
    let buffer = Buffer.concat(body);
    // 因为没有文件流,所以可以先将其转换成字符串
    console.log(buffer.toString()); // username=ang&password=123
    // 引入 querystring 模块解析数据
    let data = queryString.parse(buffer.toString());
    console.log(data); // { username: 'ang', password: '123' }
  });
}).listen('8000');

接收浏览器的文件上传数据(带有文件上传)

使用表单上传文件时需要将表单的编码方式更改为 multipart/form-data,在这里我们先使用一个 1.txt 的纯文本文件,这样我们依旧可以对二进制数据使用 toString方法 从而看到传送的内容。

<form method="GET" action="http://localhost:8000/upload" enctype="multipart/form-data">
  用户: <input type="text" name="username" /><br />
  密码: <input  type="password" name="password" /><br />
  <input type="file" name="f1" />
  <input type="submit" value="提交" />
</form>

1.txt 文件中的内容

1234
abcd

服务端的代码跟之前接受 post 数据的代码一样,将二进制数据转换成字符串后可以打印出:

------WebKitFormBoundary24CM1eLiEK67ErB2
Content-Disposition: form-data; name="username"

ang
------WebKitFormBoundary24CM1eLiEK67ErB2
Content-Disposition: form-data; name="password"

123
------WebKitFormBoundary24CM1eLiEK67ErB2
Content-Disposition: form-data; name="f1"; filename="1.txt"
Content-Type: text/plain

1234
abcd

------WebKitFormBoundary24CM1eLiEK67ErB2--

可以明显地看到,传送过来的数据是一个包含分隔符、参数描述信息等内容的构造体,文件的数据与普通字段的数据多出了源文件的 filename 和文件形式 content-type。接下来我们就需要跟之前一样解析数据。

首先观察分隔符,分隔符的后面是一串随机的数字字母组合,每次上传文件都有所不同,要确定分隔符只能在 request 的数据中找,在 request 的 headers 中有一个 content-type 字段中有这个分隔符。

 'content-type':
   'multipart/form-data; boundary=----WebKitFormBoundary24CM1eLiEK67ErB2',
let boundary = `--${req.headers['content-type'].split('; ')[1].split('=')[1]}`;
conosle.log(boundary) // ------WebKitFormBoundary24CM1eLiEK67ErB2

除此之外,在 http 协议中的换行是 \r\n,通过这些我们可以对数据进行切分。但是为了保证文件数据,我们并不能对 toString 后的数据进行处理,而是要操作原始的 buffer 数据。

req.on('end', () => {
  let buffer = Buffer.concat(body);
  let arr1 = [];
  let n = 0;
  let len = boundary.length;
  while ((n = buffer.indexOf(boundary)) != -1) {
    arr1.push(buffer.slice(0, n));
    buffer = buffer.slice(n + len);
  }
  arr1.push(buffer); // arr1 是将 buffer 数据通过分隔符切分后得到的数组
  arr1.pop(); // 去除头部的空数据
  arr1.shift(); // 去除尾部的 -- 
  arr1.forEach(buffer => {
    buffer = buffer.slice(2, buffer.length - 2);  // 去除头尾的 /r/n
    let n = buffer.indexOf('\r\n\r\n');
    let info = buffer.slice(0, n).toString(); // info 是字段描述信息,可以转换成字符串查看
    let data = buffer.slice(n + 4); // data 是字段数据
    // 对文件和普通字段的数据做最后一步的区别处理
    if (info.indexOf('\r\n') != -1) {
      // 处理文件数据
      let arr2 = info.split('\r\n')[0].split('; ');
      let name = arr2[1].split('=')[1];
      let filename = arr2[2].split('=')[1];
      name = name.substring(1, name.length - 1); 
      filename = filename.substring(1, filename.length - 1);
      console.log(name, filename);
      // 引入 fs 模块,当前目录创建 upload 文件夹
      fs.writeFile(`upload/${filename}`, data, err => {
        if(err) {
          console.log(err);
        } else {
          console.log('上传成功');
        }
      })
    } else {
      // 处理普通字段
      let name = info.split('; ')[1].split('=')[1];
      name = name.substring(1, name.length - 1);
      console.log(name, data.toString()); // 文本文件数据,可以转换成字符串查看
    }
  })
})

最后文件上传到了 upload 目录下,打印出的结果是:

username ang
password 123
f1 1.txt
上传成功

当然,上面的代码只是能让我们对原理上的解析有一个认识,真正的上传过程中碰到的问题还有很多,比如把所有的数据都放在一个数组中等接收完再处理,当文件过大时,服务器的内存开销也会过大,这就需要分段处理了。而且,对于这种比较基础的 node 模块,不可能所有的东西都用原生的自己来写,更多的是引用第三方库来处理,这里我们可以使用到 multiparty 这个包。这是专门解析有 content-type 的 http 请求 multipart/form-data。这个不是 node 的系统包,所以需要安装。

const http = require('http');
const multiparty = require('multiparty');

http.createServer((req, res) => {
  let form = new multiparty.Form({
    uploadDir: './upload' //文件上传的目录
  });
  form.parse(req); // 解析表单
  // 普通字段
  form.on('field', (name, value) => {
    console.log(name, value);
  })
  // 文件字段
  form.on('file', (name, file) => {
    console.log(name, file);
  });
  // 全部解析完成
  form.on('close', () => {
    console.log('表单解析完成');
  })
}).listen('8008');

最终的打印结果是

username ang
password 123
f1 { fieldName: 'f1',
  originalFilename: '1.txt',
  path: 'upload\\amaOyu-6u1_XlQUaxaNEhY0A.txt',
  headers:
   { 'content-disposition': 'form-data; name="f1"; filename="1.txt"',
     'content-type': 'text/plain' },
  size: 12 }
表单解析完成

当然,node 使用更多的是 express 和 koa 框架,但是分析原生的处理方式,能帮助我们了解 http 协议以及框架原理。

上一篇下一篇

猜你喜欢

热点阅读