Nodejs-基础-http-处理请求

2,416 阅读9分钟

前言 

在 Nodejs-基础-http 篇介绍了node如何搭一个简单的服务器

Nodejs-基础-fs 篇介绍了node如果根据请求返回静态文件 本篇就来继续介绍一下NodeJS如何处理请求

数据请求

相信看本篇文章的大家还是前端人员比较多,这里稍微说一下,前端数据请求有几种,比如form、ajax、jsonp 这些在页面里写的过程肯定是千差万别,但是在后台处理起来来说,都是一样的,不管多少种。 因为前后端的请求不管怎么样,都是走的HTTP请求。

对后台来说主要是请求方式的区别 比如 GET 和 POST 

GET

首先先建一个 server.js 

 server.js

const http = require('http');

http
  .createServer((req, res) => {
    res.write('success');
    res.end();
  })
  .listen(8080);

然后建一个form.html  form.html

<!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>Document</title>
  </head>
  <body>
    <form action="http://localhost:8080/" method="GET">
      用户:<input type="text" name="user" value="" /><br/>
      密码:<input type="password" name="pass" value="" /><br/> 
      <input type="submit" value="提交">
    </form>
  </body>
</html>

这时候运行 node server.js 

然后打开form.html,表单随便输入点东西,然后点击提交,不出意外的话会看到这样 image.png

这样基本请求数据就已经搞好了,但是这不是重点,重点是得后台能拿到这些数据对不对 其实get数据特别简单,他的数据是在地址里的,既然在地址里是不是就可以找 req.url 里拿呀,如果对这里有问题欢迎观看  Nodejs-基础-http 这篇文章,里面有详细说明

废话不多说,直接打印出来看看 这里先把form的地址随便修改点东西,为了看着方便,像这样 image.png

然后修改一下 server.js 

const http = require('http');

http
  .createServer((req, res) => {
    console.log(req.url);
    res.write('success');
    res.end();
  })
  .listen(8080);

然后重新运行,提交 image.png

image.png

这个 /favicon.ico 之前 Nodejs-基础-http 篇也说过,是浏览器主动管你要的,直接判断一下return就好 但是拿到 /aaa/?user=name&pass=123456 这一堆东西毕竟不能直接用,所以咱们来切一下 (这里肯定是有很多现成的方法的,包括各种基于node的框架,都会把这些参数给封装好,但是这里主要说明原理,毕竟他们也是这么做的) 

const http = require('http');

http
  .createServer((req, res) => {
    if (req.url.indexOf('?') == -1) return;  // 如果请求地址不带get数据的化 直接返回

    let arr = req.url.split('?');

    //arr[0] => 地址 /aaa
    //arr[1] => 数据 user=name&pass=123456
    let url = arr[0];
    let arr2 = arr[1].split('&');

    //arr2 => ['user=name','pass=123456']
    let GET = {};

    for (let i = 0; i < arr2.length; i++) {
      let arr3 = arr2[i].split('=');
      // arr3[0] => key
      // arr3[1] => value
      GET[arr3[0]] = arr3[1];
    }

    console.log(url, GET);
    res.write('success');
    res.end();
  })
  .listen(8080);

这段代码大概意思相信大家都能看明白,就是把字符串切成最终的数据,可以保存然后重新启动提交看一下终端

image.png

地址和请求的数据就都拿到了,是不是很简单?

querystring

当然了,真正项目里肯定不能每次都写着一大坨,相信看到这已经有人想到了,封装成一个函数,对不对? 哈哈,其实也不用,NodeJS官方有这方面的包,叫 queryString ,所谓的查询字符串 拿刚才的例子来稍微修改一下

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

http
  .createServer((req, res) => {
    let GET = {};
    if (req.url.indexOf('?') != -1) {
      var arr = req.url.split('?');
      var url = arr[0];
      GET = querystring.parse(arr[1]);
    } else {
      var url = req.url;
    }

    console.log(url, GET);
    res.write('success');
    res.end();
  })
  .listen(8080);

image.png

是不是方便了许多呢?

URL

当然肯定有人觉得,还是不方便,还是得 split 一刀,怎么说呢,懒确实能推进人类社会前进,是的NodeJS还有另一个包,可以直接帮你把URL地址里的信息全都解析好,真棒对吧~ 我们可以先来试验一下 这里直接用百度随便搜索一条的地址了 image.png

代码如下

const url = require('url');

var obj = url.parse(
  'https://www.baidu.com/s?ie=utf-8&f=8&rsv_bp=1&rsv_idx=1&tn=baidu&wd=111&rsv_pq=859f07e2001f0043&rsv_t=57ad4pr9cVROAk8L%2BYHrRif%2BoJFfODwoMcmk%2Bm4il9T82sQeP%2FbI4nY9Vlc&rqlang=cn&rsv_enter=1&rsv_dl=tb&rsv_sug3=3&rsv_sug1=3&rsv_sug7=100&rsv_sug2=0&prefixsug=111&rsp=1&inputT=491&rsv_sug4=491',
  true,
);

console.log(obj);

注意,这个url.parse方法 如果你要解析query的化,给第二个参数加个true就好,否则还是一长坨字符串

好的,可以看到把咱们能想到的信息差不多已经切得差不多了 然后咱们再来修改一下代码

const http = require('http');
const urlLib = require('url');

http
  .createServer((req, res) => {
    let obj = urlLib.parse(req.url, true);

    let url = obj.pathname;
    let GET = obj.query;

    console.log(url, GET);
    res.write('success');
    res.end();
  })
  .listen(8080);

image.png

很简单,对不对?

POST

POST数据跟GET稍微有点区别,因为前端不管你是怎么发过来的,只要是get请求,他肯定都在url里 不过post不一样,它很大,至少它可以很大,正常一个post数据也还好,但是如果你要是上传个图片,上传个音频,甚至是视频什么乱七八遭的,肯定多大的都有。 这是有就有一个问题了,比如我要上传一个视频,1个g,那这个过程要怎么发,如果就是正常发的话,是不是只要有一个字节错了,或者丢包了,就要全部重来啊,这个就很低效,所以post数据是分段发送的 既然它是分段发送的,那么咱们肯定也要分段来接收

好的,接下来就要介绍另外一个东西,在咱们的 req 上有个 on ,相信熟悉前台的的大家都能反应过来,有点像事件,在这也差不多是事件的意思,后续的文章我会提到 NodeJS Events 的概念

这里就直接来写两个 一个是 data 还有一个 end  ,这个data会发生很多次,至于到底多少,这得看你文件多大,这个end也很简单,只发生一次,就是数据全部发完的时候 咱们来直接改造一下之前的代码

 server.js

const http = require('http');

http
  .createServer((req, res) => {
    var str = ''; // 接收数据用的 ?

    var i = 0;
    req.on('data', data => {
      console.log(`第${i++}次接收请求`);
      str += data;
    });

    req.on('end', () => {
      console.log(str);
    });
  })
  .listen(8080);

注意⚠️,然后把之前的form.html中的method 改成 POST,然后运行,提交一下数据,可以回来看控制台

提交过后之所以会一直转因为咱们什么都没返回 可以看到控制台

image.png

可能这时候有人会有疑问,不是分段多次么,这咋就一次,因为咱们数据太少了,如果就这两个字他就分。。。那不是脑子有病么

如果就是想看看数据量大会怎么样,咱们可以直接写一个 textarea 来试试

当然了,大家测试要有个度,要不然vscode可能受不了 image.png 像我这样直接爆炸,找不到第几次了该

image.png

最后我放弃了,vscode实在是承受不住,终端真香

当然了,大家细心的话可能看到上面代码我在那个 str的注释给了一个 ?,这是为什么呢,因为咱们现在是做实验,如果要是文件的化二进制肯定就不行了,那么怎么办呢? 嘿嘿,我不说

放下手中的西瓜刀哈,下文就有,先别着急

当然了,咱们现在拿到的数据是不是又是这一坨东西呀,好的废话不多说,直接 querystring 伺候 image.png

改造一下代码

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

http
  .createServer((req, res) => {
    var str = ''; // 接收数据用的 ?

    var i = 0;
    req.on('data', data => {
      console.log(`第${i++}次接收请求`);
      str += data;
    });

    req.on('end', () => {
      console.log(querystring.parse(str));
    });
  })
  .listen(8080);

重新发请求会看到 image.png

就ok了,是不是很简单~

文件上传

接下来,咱们来处理一下上传文件

咱们一步一步来,先来改一下 form.html 

<!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>Document</title>
  </head>
  <body>
    <form action="http://localhost:8080/aaa" method="POST">
      用户:<input type="text" name="user" value="" />

      密码:<input type="password" name="pass" value="" />

      <input type="file" name="file" value="文件" />
      <input type="submit" value="提交" />
    </form>
  </body>
</html>

加一个input file,然后咱们直接来试试,万一行了呢,当然,哪有那么好的万一呢 image.png

emmmm,有没有感觉不对劲,上传的只有文件的名字,并没有任何数据,当然了,相信大家也都知道,普通的form表单是没办法直接传文件的,得加一个enctype 可能有人对这东西不太熟,这里简单介绍一下 application/x-www-form-urlencoded  // 这个也就是咱们平常的 name=321&pass=321这种格式的数据 multipart/form-data  // 这个multipart就是分割成多个部分,因为你有可能有多个文件,form-data就是上传的是真正表单的数据 text/plain  //这个就是纯文本

这么一看肯定就知道了,肯定是form-data 好的这里来改一下 form.html 

<!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>Document</title>
  </head>
  <body>
    <form action="http://localhost:8080/aaa" method="POST" enctype="multipart/form-data" >
      用户:<input type="text" name="user" value="" />

      密码:<input type="password" name="pass" value="" />

      <input type="file" name="file" value="文件" />
      <input type="submit" value="提交" />
    </form>
  </body>
</html>

这时候再提交一下,可以看到 image.png 像那么回事了对不对,当然了,这一堆东西肯定也是没法直接用的

这里就要介绍一个模块了 叫 busboy  废话不说,一直接来按一下 yarn init -y  yarn add busboy 

这个处理文件稍微有点复杂,咱们先上代码,然后再解释

先把from.html除了上传文件的其他input删掉,因为真是项目中,具体原因后续文章详细讲文件的和其他框架的时候会提到原因

const http = require('http');
const querystring = require('querystring');
const Busboy = require('busboy');

http
  .createServer((req, res) => {
    if (req.method === 'POST') {
      var busboy = new Busboy({ headers: req.headers });
      busboy.on('file', function(fieldname, file, filename, encoding, mimetype) {
        console.log('File [' + fieldname + ']: filename: ' + filename + ', encoding: ' + encoding + ', mimetype: ' + mimetype, 0);
        file.on('data', function(data) {
          console.log('File [' + fieldname + '] got ' + data.length + ' bytes', 1);
        });
        file.on('end', function() {
          console.log('File [' + fieldname + '] Finished', 2);
        });
      });
      busboy.on('field', function(fieldname, val, fieldnameTruncated, valTruncated, encoding, mimetype) {
        console.log('Field [' + fieldname + ']');
      });
      busboy.on('finish', function() {
        console.log('Done parsing form!');
        res.end();
      });
      req.pipe(busboy);
    }
  })
  .listen(8080);

其实根据咱们上面几个模块和之前的文章都看名字大概都已经能猜的八九不离十了,监听文件,分段接收,结束,车结束返回,失败了直接结束 唯一特殊点的可能是那个 req.pipe,这个pipe是 NodeJS stream 里的方法,这块的话题展开就有点大了, 暂时可以简单理解为 busboy 写了文件流的操作方法,这个req.pipe把文件交给busboy

这时候可以上传一下文件然后看控制台 image.png其实字段名字大概猜都已经能猜到了,无非就是一下名字,大小,后缀等等

好的,知道了这些api之后咱们稍微删一删,然后做一个最基本的出来上传文件

首先,先建一个static目录,用于装咱们上传的文件,然后改造一下代码 image.png

const http = require('http');
const querystring = require('querystring');
const Busboy = require('busboy');
const path = require('path');
const fs = require('fs');

http
  .createServer((req, res) => {
    if (req.method === 'POST') {
      var busboy = new Busboy({ headers: req.headers });
      busboy.on('file', function(fieldname, file, filename) {
        var saveTo = path.join(__dirname, 'static', path.basename(fieldname));
        console.log(saveTo, filename);
        file.pipe(fs.createWriteStream(saveTo));
      });
      busboy.on('finish', function() {
        res.writeHead(200, { Connection: 'close' });
        res.end("That's all folks!");
      });
      return req.pipe(busboy);
    }
    res.writeHead(404);
    res.end();
  })
  .listen(8080);

这时候上传一下文件, 可以看到 image.png

image.png

好的,基本上已经成功了,不过现在还是有很多很多问题,咱们先来解释一下代码,然后再看问题

首先是 __dirname 这个是当前的绝对路径,一个魔术变量,大家可以直接console出来看一眼, 然后这个path.join 可以把路径拼成一个完整的绝对路径 然后这个file.pipe跟咱们上面的req.pipe一样,同样是把流交给别人处理,这里是直接交给fs,来写入一个流文件 是不是很简单 然后咱们来看一下问题,首先,咱们这个名字,不应该直接用用户传过来的名字,因为 0.png 这种名字重复率实在是太高了,还有就是咱们后续肯定是应该放到公用的接口,然后把名字返回给前端,并且存到数据库里

并且,咱们仅仅知道这些对文件操作还是远远不够的,比如说,视频,发上来可以,怎么拿呢?一口气给用户返回去?这肯定不行,毕竟5g还没普及呢对吧,那怎么发呢?分段?,快进什么的需要怎么操作呢?以及怎么加密等等等等

这也算是留给大家的思考,名字很简单,愿意的化可以直接用uuid来生成随机的,这个不会重复,准确的是概率足够低,fs有 rename方法,直接修改一下再存就可以

不过像multer这种库使用的 sha 这个可以在NodeJScrypto 里创建,按照流来生成,相同的内容永远都一样,这也符合咱们的项目,相同的文件就应该直接替换了,还能减少垃圾文件