# NodeJS 处理 HTTP,req 和 res、定义路由,综合实践

TIP

从本节内容开始学习 NodeJS 如何处理 HTTP 请求,通过前面的学习我们知道前后端交互的关系 是通过 HTTP 请求来做交互的。即可打通前端 和 NodeJS(服务端)

  • Request(req)
  • Response(res)
  • 定义路由
  • querystring
  • Response 返回数据
  • 获取 Request Body

# 一、认识 Request 和 Response

TIP

  • NodeJS 如何监听 HTTP 请求
  • 如何拿到 Request 和 Response
  • 如何使用 Request 和 Response

# 1、NodeJS 如何监听 HTTP 请求

TIP

监听:即 前端发来的 HTTP 请求后,后端得拿到(或 得到监听)

image-20240203161833834

服务端处理 Request 和 Response

注:

前端访问服务端,发起一个 HTTP 请求 到 服务端 -> 服务端拿到 Request 请求(监听)-> 服务端处理任何事情 -> Response(状态码、Content-type、body)返回给前端,整个过程就完成了。

上图也是,前端通过 Ajax 访问服务端的过程

  • 也可简称:req 和 res,依然读作 Request 和 Response
  • 通过 Request 可获取:method、url、body
  • 通过 Response 可设置:状态码、Content-type、body(返回给前端的)

# 2、NodeJS 启动 Web 服务

TIP

启动 Web Server:即 监听 HTTP 请求

  • NodeJS 中使用 http 模块,启动服务
  • 本机的 IP:127.0.0.1
  • 本机的域名:localhost

可通过本机 IP 或 本机域名来访问 Web 服务

新建项目 icoding-node-http 使用 VSCode 打开,在 /src/index.js

// 引入 http 核心模块
const http = require("http"); // commonJS 语法,require 三个层级:1、系统自带模块;2、npm 安装;3、自定义

// 创建 Web 服务
const server = http.createServer(() => {
  // 处理请求
  console.log("已经收到 http 请求"); // 打印证明请求得到了,否则看不出效果来
});

// 监听 http 请求,默认 3000 端口
server.listen(3000);
console.log("http 请求已经被监听,3000 端口,请访问 http://localhost:3000");

使用 node 命令运行

node .\src\index.js

image-20231111160614625

在浏览器中输入 http://localhost:3000 回车

GIF-2023-11-11-16-08-58

发现浏览器选项卡上边一直在加载中 ... 没有任何反应,此时看控制台打印 “已经收到 http 请求”

image-20231111162051132

整个过程

在浏览器中输入 http://localhost:3000 回车 -> 发送 http 请求 -> 浏览器没有任何响应 -> 服务端已经收到了请求,控制台打印了 “已经收到 http 请求”,但并没有返回任何内容,因此浏览器也没有返回任何结果(显示一直加载中 ...)

到目前为止,说明前端的请求已经被服务端监听了

# 3、安装 nodemon

初始化 package.json 文件

npm init -y

安装 nodemon,在代码修改后会自动重启服务

# -D 或 --save-dev 开发依赖
npm i nodemon -D

package.json 中添加 nodemon 运行脚本

{
  "scripts": {
    "dev": "nodemon ./src/index.js"
  }
}

此时就不用再使用 node ./src/index.js 命令启动服务了,直接使用以下命令即可

npm run dev

image-20231113163953575

# 4、req 和 res

TIP

  • req 即 Request,res 即 Response,是 http 请求的关键
  • 如何拿到 req 和 res
  • 如何使用 req 和 res

# 4.1、HTTP 的 GET 和 POST 请求

以下为 HTTP 的 GET 请求过程

image-20240129173010242

以下 HTTP 的 POST 请求过程

image-20240204000024169

# 4.2、获取 Request 和 Response

/src/index.js

// 引入 http 核心模块
const http = require("http");

// 创建 Web 服务
// req,Request 请求
// res,Response 响应
const server = http.createServer((req, res) => {
  // 处理请求
  res.end("<h1>Received request 已收到请求</h1>"); // res 返回信息给前端
  // 目前返回的是字符串,一般我们会返回 JSON
});

// 监听 http 请求,默认 3000 端口
server.listen(3000);
console.log("http 请求已经被监听,3000 端口,请访问 http://localhost:3000");

启动运行,在浏览器中访问服务

image-20231113180334118

# 4.3、处理中文字符

TIP

如何响应里包中文会出现乱码,我们需要在响应头里添加编码格式来处理乱码

/src/index.js

// 引入 http 核心模块
const http = require("http");

// 创建 Web 服务
// req,Request 请求
// res,Response 响应
const server = http.createServer((req, res) => {
  // 设置响应头,设置编码集防止乱码
  res.writeHead(200, {
    "content-type": "text/html;charset=utf-8",
  });
  // 处理请求
  res.end("<h1>Received request 已收到请求</h1>"); // res 返回信息给前端
  // 目前返回的是字符串,一般我们会返回 JSON
});

// 监听 http 请求,默认 3000 端口
server.listen(3000);
console.log("http 请求已经被监听,3000 端口,请访问 http://localhost:3000");

添加完响应头编码后,再次重启服务

image-20231113181402656

# 4.4、使用 Request 获取当前的 URL

/src/index.js

const http = require("http");

const server = http.createServer((req, res) => {
  res.writeHead(200, {
    "content-type": "text/html;charset=utf-8",
  });

  // Request 获取当前 URL
  const url = req.url;
  console.log("url is:", url);

  res.end("<h1>Received request 已收到请求</h1>");
});

server.listen(3000);
console.log("http 请求已经被监听,3000 端口,请访问 http://localhost:3000");

保存后,nodemon 自动重启服务,浏览器访问 http://localhost:3000,控制台打印输出 url 信息

image-20231115005327914

如果浏览器中输入 http://localhost:3000/index.html?a=123 控制台打印输出

image-20231115005841571

# 5、总结

TIP

  • NodeJS 启动服务,127.0.0.1localhost
  • 如何拿到 req 和 res
  • 如何使用 req 和 res

先简单了解即可,后边还会深入

# 二、定义路由

TIP

  • 拿到 Request 中 的 url 和 method
  • 定义路由(模拟评论系统的获取评论列表、创建评论信息)
  • 测试路由(postman)

# 1、路由包含什么

TIP

  • 定义 method,如 GET / POST 等
  • 定义 url 规则,如 /api/list/api/create
  • 定义输入(Request body)和 输出(Response body)格式

# 1.1、前端 Ajax 请求时,需要 URL

TIP

来自 axios 官方文档示例 (opens new window),其中 .get.post 以及 URL 都需要按照服务端的规定来做(需要服务端先实现)

function getUserAccount() {
  // url 规则
  return axios.get("/user/12345");
}

function getUserPermissions() {
  // url 规则
  return axios.get("/user/12345/permissions");
}

axios.all([getUserAccount(), getUserPermissions()]).then(
  axios.spread(function (acct, perms) {
    // 两个请求现在都执行完成
  })
);

# 1.2、路由是什么

TIP

  • 路由:英文叫 router
  • 服务端的入口规则
  • 和前端的约定

路由就像一座城池的城门一样,不同的出入口门有不同的功能,需要按照约定进出

# 1.3、路由 和 URL

TIP

  • GET 请求,/api/list 路由 -> axios.get('/api/list?a=1')
  • POST 请求,'/api/create' 路由 -> axios.post('/api/create', { ... })

路由是规则,url 是具体的形式

# 2、NodeJS 定义路由

TIP

  • 从 req 中获取 url 和 method
  • 判断 method 是否符合
  • 看 url 是否符合规则

获取评论信息列表案例中,请求的 URL 是 /api/list、method 是 GET,到服务端后该如何判断该请求就是获取评论信息列表的呢 ?

需要判断请求的 URL 是否是 /api/list、method 是否是 GET

image-20240204000224884

同理提交评论案例,请求的 URL 是 /api/create、method 是 POST,到服务端后该如何判断该请求就是提交评论的呢 ?

需要判断请求的 URL 是否是 /api/create、method 是否是 POST

image-20240204000331215

# 3、创建路由

icoding-node-http 项目的 /src/index.js

// 引入 http 核心模块
const http = require("http");

// 创建 Web 服务
const server = http.createServer((req, res) => {
  // 设置响应头,设置编码集防止乱码
  res.writeHead(200, {
    "content-type": "text/html;charset=utf-8",
  });

  // 路由定义

  // Request 获取当前 URL
  const url = req.url;
  // 获取 method
  const method = req.method;

  // 打印获取到的 URL 和 method
  console.log(url);
  console.log(method);

  res.end("<h1>测试 url 和 method</h1>"); // 处理请求,返回给前端
});

// 监听 http 请求,默认 3000 端口
server.listen(3000);
console.log("http 请求已经被监听,3000 端口,请访问 http://localhost:3000");

启动服务后,在浏览器中输入 http://localhost:3000/api/list,控制台中会打印输入对应的 URL /api/list

image-20231115014017207

# 3.1、创建获取评论列表路由

/src/index.js

// 引入 http 核心模块
const http = require("http");

// 创建 Web 服务
const server = http.createServer((req, res) => {
  // 设置响应头
  res.writeHead(200, {
    "content-type": "text/html;charset=utf-8",
  });

  // 路由定义

  // Request 获取当前 URL
  const url = req.url;
  // 获取 method
  const method = req.method;

  // 打印获取到的 URL 和 method
  console.log(url); // /api/list
  console.log(method); // GET

  // 定义路由:模拟获取评论列表
  if (url === "/api/list" && method === "GET") {
    // 如果命中路由,Response 返回给前端
    res.end("<h1>这是模拟获取评论列表的路由 !</h1>");
  } else {
    // 未命中路由,Response 返回给前端 404
    res.end("<h1>404</h1>");
  }
});

// 监听 http 请求,默认 3000 端口
server.listen(3000);
console.log("http 请求已经被监听,3000 端口,请访问 http://localhost:3000");

在浏览器中输入 http://localhost:3000/api/list 正确的路由 和 错误的路由,观察返回给前端的信息

GIF-2023-11-15-2-21-09

注:

如果浏览器地址栏输入 http://localhost:3000/api/list?a=123 时,在 /api/list 后边添加参数 ?a=123

此时,浏览器访问返回 404 ,因为获取的 url 地址为 /api/list?a=123 不等于 /api/list

截取掉 ? 包括问号之后的所有参数,再作比较

/src/index.js

// 引入 http 核心模块
const http = require("http");

// 创建 Web 服务
const server = http.createServer((req, res) => {
  // 设置响应头
  res.writeHead(200, {
    "content-type": "text/html;charset=utf-8",
  });

  // 路由定义

  // Request 获取当前 URL
  const url = req.url;
  // 截取 ? 号前边的 url
  const path = url.split("?")[0]; // /api/list
  // 获取 method
  const method = req.method;

  // 打印获取到的 URL 和 method
  // console.log(url) // /api/list?a=123
  // console.log(method) // GET

  // 定义路由:模拟获取评论列表
  if (path === "/api/list" && method === "GET") {
    // 如果命中路由,Response 返回给前端
    res.end("<h1>这是模拟获取评论列表的路由 !</h1>");
  } else {
    // 未命中路由,Response 返回给前端 404
    res.end("<h1>404</h1>");
  }
});

// 监听 http 请求,默认 3000 端口
server.listen(3000);
console.log("http 请求已经被监听,3000 端口,请访问 http://localhost:3000");

此时,浏览器地址栏输入 http://localhost:3000/api/list?a=123 即可正常返回了

image-20231118212539893

# 3.2、创建评论路由

/src/index.js

// 引入 http 核心模块
const http = require("http");

// 创建 Web 服务
const server = http.createServer((req, res) => {
  // 设置响应头
  res.writeHead(200, {
    "content-type": "text/html;charset=utf-8",
  });

  // 路由定义

  // Request 获取当前 URL
  const url = req.url;
  // 截取 ? 号前边的 url
  const path = url.split("?")[0]; // /api/list
  // 获取 method
  const method = req.method;

  // 打印获取到的 URL 和 method
  console.log("url:" + url); // /api/list?a=123
  // console.log(method) // GET
  console.log("path:" + path);

  // 定义路由:模拟获取评论列表
  if (path === "/api/list" && method === "GET") {
    // 如果命中路由,Response 返回给前端
    res.end("<h1>这是模拟获取评论列表的路由 !</h1>");
    // 定义路由:模拟创建评论
  } else if (path === "/api/create" && method === "POST") {
    res.end("<h1>这是创建评论的路由 !</h1>");
  } else {
    // 未命中路由,Response 返回给前端 404
    res.end("<h1>404</h1>");
  }
});

// 监听 http 请求,默认 3000 端口
server.listen(3000);
console.log("http 请求已经被监听,3000 端口,请访问 http://localhost:3000");

在浏览器地址栏中输入 http://localhost:3000/api/create ,服务端返回 404

image-20231118225331279

原因是:

浏览器访问是 GET 请求,而 判断条件为 POST 请求。可使用 postman 模拟 POST 请求

# 3.3、路由测试

TIP

在 postman 中使用 POST 请求访问创建评论路由,即可成功返回对应的内容

image-20231119000139458

# 4、总结

TIP

  • 再次复习理由的概念
  • NodeJS 如何定义路由
  • 安装 和 使用 postman

# 三、querystring

TIP

  • 什么是 querystring
  • NodeJS 获取 querystring
  • 论结构化与非结构化

# 1、什么是 querystring

TIP

  • querystring 查询字符串(也叫 URL 参数)
  • https://search.jd.com/Search?keyword=寸衫&enc=utf-8
  • url 问号 ? 后面的都是 querystring
  • & 分割,key = value 形式,可继续扩展

# 2、querystring 的作用

TIP

  • 动态网页的基石(web1.0 静态 -> web2.0 动态)
  • https://search.jd.com/Search?keyword=寸衫&lang=en
  • https://search.jd.com/Search?keyword=运动鞋&lang=zh-CN

以上 keywordlang 参数当传入不同的参数时,页面显示的结果就会不一样(即:动态)

# 3、如何利用 querystring 实现动态网页

TIP

  • 服务端拿到 querystring
  • 根据不同的 querystring,返回不同的内容
  • 即:变化 querystring,变换内容(只要服务端支持)

# 3.1、解析 querystring

TIP

在浏览器中输入 http://localhost:3000/api/list?a=123&b=111 解析 ? 问号后面的参数

/src/index.js 中解析 querystring

const http = require("http");

const server = http.createServer((req, res) => {
  res.writeHead(200, {
    "content-type": "text/html;charset=utf-8",
  });

  const url = req.url;
  const path = url.split("?")[0]; // /api/list

  // 获取 ? 后面的参数
  const queryStr = url.split("?")[1]; // a=123
  // console.log('queryStr', queryStr) // 当 url 为 /favicon.ico 时,queryStr 的值为 undefined

  const method = req.method;

  // console.log("url:" + url) // /api/list?a=123&b=111
  // console.log("path:" + path)

  // 解析 querystring
  const query = {};
  // /api/list?a=123&b=111 对问号后边的参数解析
  // 由于浏览器访问时,获取 URL 会有 /favicon.ico 图标,需要判断,否则会报错
  // 判断当 queryStr 为 undefined 时,判断是否为空,当然也可以使用 if 条件语句 或 以下方式
  queryStr &&
    queryStr.split("&").forEach((item) => {
      // item 即:a=123 的形式
      const key = item.split("=")[0]; // 'a'
      const val = item.split("=")[1]; // '123'
      query[key] = val; // { a: '123', b: '111' }
    });

  // 解析后的参数对象
  console.log(query); // { a: '123', b: '111' }

  // 定义路由:模拟获取评论列表
  if (path === "/api/list" && method === "GET") {
    res.end("<h1>这是模拟获取评论列表的路由 ! </h1>");
    // 定义路由:模拟创建评论
  } else if (path === "/api/create" && method === "POST") {
    res.end("<h1>这是模拟创建评论的路由 ! </h1>");
  } else {
    // 未命中路由,Response 返回给前端 404
    res.end("<h1>404</h1>");
  }
});

// 监听 http 请求,默认 3000 端口
server.listen(3000);
console.log("http 请求已经被监听,3000 端口,请访问 http://localhost:3000");

启动 node 服务

node .\src\index.js

在浏览器中输入 http://localhost:3000/api/list?a=123&b=111

image-20231123022848521

此时,成功获取解析后的参数对象

# 3.2、判断 querystring 参数

TIP

在浏览器地址栏中访问 http://localhost:3000/api/list?type=1

  • 如:type=1 表示全部评论
  • type=2 表示当前用户的评论信息

判断如果 type=1 做什么,type=2 做什么

/src/index.js

const http = require("http");

const server = http.createServer((req, res) => {
  res.writeHead(200, {
    "content-type": "text/html;charset=utf-8",
  });

  const url = req.url;
  const path = url.split("?")[0]; // /api/list

  // 获取 ? 后面的参数
  const queryStr = url.split("?")[1]; // a=123
  // console.log('queryStr', queryStr) // 当 url 为 /favicon.ico 时,queryStr 的值为 undefined

  const method = req.method;

  // console.log("url:" + url) // /api/list?a=123&b=111
  // console.log("path:" + path)

  // 解析 querystring
  const query = {};
  // /api/list?a=123&b=111 对问号后边的参数解析
  // 由于浏览器访问时,获取 URL 会有 /favicon.ico 图标,需要判断,否则会报错
  // 判断当 queryStr 为 undefined 时,判断是否为空,当然也可以使用 if 条件语句 或 以下方式
  queryStr &&
    queryStr.split("&").forEach((item) => {
      // item 即:a=123 的形式
      const key = item.split("=")[0]; // 'a'
      const val = item.split("=")[1]; // '123'
      query[key] = val; // { a: '123', b: '111' }
    });

  // 解析后的参数对象
  console.log(query); // { a: '123', b: '111' }

  // 定义路由:模拟获取评论列表
  if (path === "/api/list" && method === "GET") {
    // ---------------------------------

    // type=1 表示全部评论
    if (query.type === "1") {
      res.end("<h1>这是模拟获取评论列表的路由,【所有评论信息】");
    }
    // type=2 表示当前用户的评论信息
    if (query.type === "2") {
      res.end("<h1>这是模拟获取评论列表的路由,【当前用户的评论信息】");
    }

    // 定义路由:模拟创建评论
  } else if (path === "/api/create" && method === "POST") {
    res.end("<h1>这是模拟创建评论的路由 ! </h1>");
  } else {
    // 未命中路由,Response 返回给前端 404
    res.end("<h1>404</h1>");
  }
});

// 监听 http 请求,默认 3000 端口
server.listen(3000);
console.log("http 请求已经被监听,3000 端口,请访问 http://localhost:3000");

启动 node 服务

node .\src\index.js

在浏览器中输入 http://localhost:3000/api/list?type=1 或 修改参数 type=2

image-20231123025956879

注:

以上就是 querystring 的作用,无论什么路由当前返回给前端的内容先暂时不管,后面再做讲解

同时,以上解析 querystring 的过程本质上也是 NodeJS 中querystring 模块的源码实现

# 4、使用 querystring 模块

修改 /src/index.js,使用 querystring 模块,解析 querystring 的部分

const http = require("http");
// 引入 querystring 模块
const querystring = require("querystring");

const server = http.createServer((req, res) => {
  res.writeHead(200, {
    "content-type": "text/html;charset=utf-8",
  });

  const url = req.url;
  const path = url.split("?")[0]; // /api/list

  // 获取 ? 后面的参数
  const queryStr = url.split("?")[1]; // a=123
  // console.log('queryStr', queryStr) // 当 url 为 /favicon.ico 时,queryStr 的值为 undefined

  const method = req.method;

  // console.log("url:" + url) // /api/list?a=123&b=111
  // console.log("path:" + path)

  // 解析 querystring
  // const query = {}
  // // /api/list?a=123&b=111 对问号后边的参数解析
  // // 由于浏览器访问时,获取 URL 会有 /favicon.ico 图标,需要判断,否则会报错
  // // 判断当 queryStr 为 undefined 时,判断是否为空,当然也可以使用 if 条件语句 或 以下方式
  // queryStr && queryStr.split('&').forEach(item => {
  //     // item 即:a=123 的形式
  //     const key = item.split('=')[0] // 'a'
  //     const val = item.split('=')[1] // '123'
  //     query[key] = val // { a: '123', b: '111' }
  // })

  const query = querystring.parse(queryStr);
  // 解析后的参数对象
  console.log(query); // { type: '1' }

  // 定义路由:模拟获取评论列表
  if (path === "/api/list" && method === "GET") {
    // ---------------------------------

    // type=1 表示全部评论
    if (query.type === "1") {
      res.end("<h1>这是模拟获取评论列表的路由,【所有评论信息】");
    }
    // type=2 表示当前用户的评论信息
    if (query.type === "2") {
      res.end("<h1>这是模拟获取评论列表的路由,【当前用户的评论信息】");
    }

    // 定义路由:模拟创建评论
  } else if (path === "/api/create" && method === "POST") {
    res.end("<h1>这是模拟创建评论的路由 ! </h1>");
  } else {
    // 未命中路由,Response 返回给前端 404
    res.end("<h1>404</h1>");
  }
});

// 监听 http 请求,默认 3000 端口
server.listen(3000);
console.log("http 请求已经被监听,3000 端口,请访问 http://localhost:3000");

启动 node 服务

node .\src\index.js

在浏览器中输入 http://localhost:3000/api/list?type=1 或 修改参数 type=2

image-20231123025956879

# 5、NodeJS 中 querystring 弃用的三种解决方法

TIP

在引入 NodeJS 的 querystring 模块后,发现会有删除线。即:官方 “已弃用”

image-20231124205104757

# 5.1、升级 NodeJS 版本,修改引入方式

升级到 18.x 版本后修改引入方式即可

const querystring = require("node:querystring");

# 5.2、官方推荐 URLSearchParams 替代

const query = {};
// 使用 URLSearchParams 解析
for (const [name, value] of new URLSearchParams(queryStr)) {
  query[name] = value;
}

console.log(query); // { type: '1' }

注:

另一种方式就是用我们上边最原始,手动解析 和 判断 querystring 参数的方式来实现,这也是使用插件 或 官方推荐的 URLSearchParams 方法的底层实现。

# 5.3、使用 querystringify 插件

TIP

由于 URLSearchParams 确实没有那么方便,也可以在项目里引入 querystringify 插件

首先安装插件

npm i querystringify

/src/index.js 中导入插件

// 导入插件,就和使用 querystring 一摸一样了
const querystring = require("querystringify");
// 解析参数
const query = querystring.parse(queryStr); // { type: '1' }

使用插件后的完整代码

const http = require("http");
// 引入 querystringify 模块
const querystring = require("querystringify");

const server = http.createServer((req, res) => {
  res.writeHead(200, {
    "content-type": "text/html;charset=utf-8",
  });

  const url = req.url;
  const path = url.split("?")[0]; // /api/list

  // 获取 ? 后面的参数
  const queryStr = url.split("?")[1]; // a=123
  // console.log('queryStr', queryStr) // 当 url 为 /favicon.ico 时,queryStr 的值为 undefined
  const method = req.method;

  // 解析参数
  const query = querystring.parse(queryStr);
  // 解析后的参数对象
  console.log(query); // { type: '1' }

  // 定义路由:模拟获取评论列表
  if (path === "/api/list" && method === "GET") {
    // ---------------------------------

    // type=1 表示全部评论
    if (query.type === "1") {
      res.end("<h1>这是模拟获取评论列表的路由,【所有评论信息】");
    }
    // type=2 表示当前用户的评论信息
    if (query.type === "2") {
      res.end("<h1>这是模拟获取评论列表的路由,【当前用户的评论信息】");
    }

    // 定义路由:模拟创建评论
  } else if (path === "/api/create" && method === "POST") {
    res.end("<h1>这是模拟创建评论的路由 ! </h1>");
  } else {
    // 未命中路由,Response 返回给前端 404
    res.end("<h1>404</h1>");
  }
});

// 监听 http 请求,默认 3000 端口
server.listen(3000);
console.log("http 请求已经被监听,3000 端口,请访问 http://localhost:3000");

# 6、url 的 hash 能否有同样的作用

TIP

url 的 hash(哈希)路由,类似 Vue 中的哈希路由,即:# 后边的的部分

  • http://xxx.com/index.html/#/home
  • http://xxx.com/index.html/#/user/1

url 中 # 后边的部分就是 哈希

注:

hash 不能让服务端获取来实现动态网页,后端只能获取到 url 中 # 之前的部分,# 后边的部分是不能获取到的。

因此,hash 是不能传入到服务端。测试如下

image-20231124230815753

# 7、结构化 与 非结构化

TIP

  • 结构化的数据:易于通过程序访问和分析,如对象和数组
  • 非结构化的数据:不易通过程序分析,如字符串
  • 在编程中的数据,都尽量使用结构化的数据

如:上面学习的 querystring 就是非结构化的数据 a=123&b=111

  • 对于字符串形式这样非结构化的数据,是不好确定其中是否有哪个参数的。如:确定 a 或 b
  • 如果要将其变成结构化的形式需要通过原生解析 或 querystring 模块的形式 最终解析成 对象后,再来进行判断。

# 8、总结

TIP

  • querystring 是 url? 后面的参数,格式 key=val&key=val
  • querystring 是动态网页的基石(hash 就不行)
  • NodeJS 处理 querystring,并将其结构化

# 四、Response 返回数据

TIP

  • 使用 res 设置返回的状态码,Content-type ,Body
  • 如何返回 JSON 数据 ?
  • 如何返回 HTML 数据 ?

# 1、前后端请求的过程

HTTP 的 GET 请求过程

image-20240129173010242

注:

  • 请求发送请求,通过浏览器 或 postman -> 服务端 ->
  • 服务端通过 url 和 method 判断是否符合路由规则 ->
  • 符合路由规则后 -> 服务端直接返回(返回字符串)

接下来就学习如何 设置状态码、Content-type、Body 及 相关信息

HTTP 的 POST 请求过程

image-20240204000024169

注:

  • 用 postman 发送一个 POST 请求 ->
  • 通过 url 和 method 匹配路由,同时通过 body 携带数据 -> 服务端 ->
  • 服务端返回成功状态 及 相关消息

接下来就学习如何 设置状态码、Content-type、Body 及 相关信息

# 2、res 返回 JSON 数据

TIP

  • 定义路由:模拟获取评论列表 - 返回 JSON 数据
  • 定义路由:模拟创建评论 - 返回 JSON 数据
  • 未命中路由,返回 404 - 字符串形式

/src/index.js

const http = require("http");

const server = http.createServer((req, res) => {
  const url = req.url;
  const path = url.split("?")[0]; // /api/list
  const method = req.method;

  // 定义路由:模拟获取评论列表
  if (path === "/api/list" && method === "GET") {
    // res.end('<h1>这是评论列表的路由')

    // 返回结果
    const result = {
      errno: 0,
      data: [
        { username: "allen", content: "评论内容 1" },
        { username: "ibc", content: "评论内容 2" },
      ],
    };
    // 返回 JSON 数据
    res.writeHead(200, { "Content-type": "application/json" });
    // end() 方法中只能是字符串,需要通过 JSON.stringify() 转换
    res.end(JSON.stringify(result)); // body

    // 定义路由:模拟创建评论
  } else if (path === "/api/create" && method === "POST") {
    // res.end("<h1>这是模拟创建评论的路由 ! </h1>")

    // 返回结果
    const result = {
      errno: 0,
      message: "创建成功",
    };
    // 返回 JSON 数据
    res.writeHead(200, { "Content-Type": "text/json" });
    res.end(JSON.stringify(result));
  } else {
    // 未命中路由,Response 返回给前端 404(字符串形式)
    res.writeHead(404, { "Content-Type": "text/plain" });
    // 返回字符串
    res.end("404 Not Found");
  }
});

// 监听 http 请求,默认 3000 端口
server.listen(3000);
console.log("http 请求已经被监听,3000 端口,请访问 http://localhost:3000");

启动 node 服务

node .\src\index.js

在浏览器中输入 http://localhost:3000/api/list 获取评论列表,返回 JSON 数据

image-20231125023519310

在 postman 中输入 http://localhost:3000/api/create 创建评论,返回 JSON 数据

image-20231125023726051

在浏览器中输入任意不存在的 url 地址 http://localhost:3000/list/123456 返回 404 字符串

image-20231125024028337

# 3、res 返回 HTML

TIP

在实际开发不会用到 “返回 HTML 格式”,NodeJS 本身做的是数据服务,而不是静态服务。因此如何返回 HTML、CSS、图片之类的不会涉及到,为了更好的学习和理解还是来研究下,知道即可。

方式方法 和 返回 JSON 数据类似

  • Content-Type: 'text/html'
  • res.end( ... )
  • 浏览器会根据 Content-Type 识别出 HTML 格式

/src/index.js 中,将未命中路由的数据返回格式改成 HTML 格式

const http = require("http");

const server = http.createServer((req, res) => {
  const url = req.url;
  const path = url.split("?")[0]; // /api/list
  const method = req.method;

  // 定义路由:模拟获取评论列表
  if (path === "/api/list" && method === "GET") {
    // res.end('<h1>这是评论列表的路由')

    // 返回结果
    const result = {
      errno: 0,
      data: [
        { username: "allen", content: "评论内容 1" },
        { username: "ibc", content: "评论内容 2" },
      ],
    };
    // 返回 JSON 数据
    res.writeHead(200, { "Content-type": "application/json" });
    // end() 方法中只能是字符串,需要通过 JSON.stringify() 转换
    res.end(JSON.stringify(result));

    // 定义路由:模拟创建评论
  } else if (path === "/api/create" && method === "POST") {
    // res.end("<h1>这是模拟创建评论的路由 ! </h1>")

    // 返回结果
    const result = {
      errno: 0,
      message: "创建成功",
    };
    // 返回 JSON 数据
    res.writeHead(200, { "Content-Type": "text/json" });
    res.end(JSON.stringify(result));
  } else {
    // 未命中路由,Response 返回给前端 404(字符串形式)
    // res.writeHead(404, { 'Content-Type': 'text/plain' })
    // // 返回字符串
    // res.end('404 Not Found')

    // 将 404 返回字符串格式,改为返回 HTML 网页
    res.writeHead(404, { "Content-Type": "text/html" });
    res.end(`
            <!DOCTYPE html>
            <html lang="en">
            <head>
                <meta charset="UTF-8" />
                <meta http-equiv="X-UA-Compatible" content="IE=edge" />
                <meta name="viewport" content="width=device-width, initial-scale=1.0" />
                <title>404</title>
            </head>
            <body>
                <h1>404 Not Found</h1>
            </body>
            </html>
        `);
  }
});

// 监听 http 请求,默认 3000 端口
server.listen(3000);
console.log("http 请求已经被监听,3000 端口,请访问 http://localhost:3000");

在浏览器中输入任意不存在的 url 地址 http://localhost:3000/list/123456 返回 404 HTML 格式

GIF-2023-11-25-2-53-36

# 4、总结

TIP

  • 使用 res 设置返回的状态码,Content-type ,Body
  • 返回 JSON 数据、字符串、HTML 数据

在前端输入什么 url 及 参数 ,返回的结果 和 前端输入了什么没有关系,只跟服务端的控制有关系。

# 五、获取 Request Body

TIP

  • 流 stream 的概念
  • 如何获取 Request Body
  • 使用 postman 测试

# 1、流(stream)的表现

TIP

  • 观看视频时,一边加载一边观看
  • 上传文件时,有进度条
  • 网速较慢时,打开网页会先显示一部分,然后继续加载

以上的表现形式就很像流,类似水在水管中慢慢的流动。

  • 视频:就类似于水,网络就类似水管,视频在水管中(网络上)慢慢的流动到我们本地来
  • 上传文件:文件就是水,网络就是水管,文件顺着网络慢慢地流到服务端去

# 2、流(stream)解读

TIP

非常经典的关于流的图解

  • 类似两个水桶:原始的源发地 -> 顺着水管 -> 流到目的地(从始发地 流到 目的地)
  • 生活中这样的案例很多:抽水、鱼缸 ...

image-20231125031219987

# 3、下载

TIP

下载的场景就是流的过程体现

  • source 即:服务端,dest 即:客户端(前端)
  • 下载的过程就是:从服务端 下载到 前端,类似水从 source 流到 dest 是一样的
  • 下载过程,水 即:下载的数据,水管 即:网络(网络带宽不确定,有时快、有时慢)水管有粗有细,最终都会流过去只是时间长短问题
  • 下载不仅指文件相关的,泛指所有的 Response Body(如:访问一个请求返回数据,可能是 JSON、字符串、HTML 等格式的数据)

# 3.1、浏览器能接收 流(stream)数据

TIP

  • 服务端 res.end(...) 会自动以 流 的形式返回
  • 浏览器会识别到 流,并持续接收信息(如果文件比较大,还会有进度条)这个过程不用我们管,但需要知道
  • 待全部接收完,再做显示或处理(视频是一段一段的播放)

最终,服务器 到 浏览器是以流的形式返回去的。如下将 JSON 数据通过 res.end( ... ) 返回给前端,这个过程就是以流(stream)的形式返回。数据量小时很快,大的时候会慢一些。

// 定义路由:模拟获取评论列表
if (path === "/api/list" && method === "GET") {
  // res.end('<h1>这是评论列表的路由')

  // 返回结果
  const result = {
    errno: 0,
    data: [
      { username: "allen", content: "评论内容 1" },
      { username: "ibc", content: "评论内容 2" },
    ],
  };
  // 返回 JSON 数据
  res.writeHead(200, { "Content-type": "application/json" });
  // end() 方法中只能是字符串,需要通过 JSON.stringify() 转换
  res.end(JSON.stringify(result)); // body
}

# 4、上传

TIP

上传不仅指上传大文件,泛指:创建评论信息(表单提交)、所有的浏览器前端往服务端提交的数据都属于上传

  • source 即:客户端(前端),dest 即:服务端
  • 水 即:上传的数据,水管 即:网络(带宽不确定)
  • 上传不仅指大文件,泛指所有的 Request Body

# 4.1、服务端如何接收 流(stream)数据

TIP

  • 前端使用 Ajax (或 Postman)提交数据 Request Body
  • 服务端需要识别 流,并接收数据
  • 还要知道何时才能接收完成

因此,我们接下来就要重点来学习 服务端如何识别 流,怎么知道流接收完成 ?

# 4.2、服务端如何接收 Request Body

先回顾一下 HTTP 的 POST 请求过程

image-20240204000024169

注:

  • 前端发送数据 ->
  • 通过 url 和 method = POST 匹配到路由,同时通过 body 携带数据
  • 但,body 数据还没有发送过,之前只是做了路由的匹配
  • 如何发送 body 数据 ?如何设置 Content-type ?接下来会通过代码来实践

# 4.3、postman 上传数据 - 实践

TIP

/src/index.js 中,假设现在 postman 已经具备数据发送的能力,已发送了数据给 服务端,服务端该如何接收呢 ?

即:服务端识别到流(stream),并接受数据 !

在模拟创建评论路由中,才会接收前端发送过来的数据

const http = require("http");

const server = http.createServer((req, res) => {
  // ... 省略部分代码

  // 定义路由:模拟获取评论列表
  if (path === "/api/list" && method === "GET") {
    // ... 省略部分代码
    // 定义路由:模拟创建评论
  } else if (path === "/api/create" && method === "POST") {
    // res.end("<h1>这是模拟创建评论的路由 ! </h1>")

    // 定义接受前端数据
    let bodyStr = "";
    // 1、服务端怎么识别流,并接收数据
    // chunk 即:流 的每一段数据(数据小一次就流过去了,数据大时就多流几次)
    req.on("data", (chunk) => {
      // 通过 chunk 来接受数据,每次流过来的数据都累加上(字符串格式)
      bodyStr += chunk.toString();
    });
    // 2、服务端怎么知道流完了
    req.on("end", () => {
      // 打印输出从前端接收到的数据(输出的是一个字符串格式,因为上边用到的是 .toString() 方法累加的)
      console.log("bodyStr 是服务端接收到的数据:", bodyStr);
      // 先简单返回字符串
      res.end("接收完成");
    });

    // 返回结果
    // const result = {
    //     errno: 0,
    //     message: '创建成功'
    // }
    // // 返回 JSON 数据
    // res.writeHead(200, { 'Content-Type': 'text/json' })
    // res.end(JSON.stringify(result))
  } else {
    // 未命中路由,Response 返回给前端 404(字符串形式)
    // ... 部分省略
  }
});

// 监听 http 请求,默认 3000 端口
server.listen(3000);
console.log("http 请求已经被监听,3000 端口,请访问 http://localhost:3000");

启动 node 服务

node .\src\index.js

在 postman 中发送 POST 请求,在 body 中定义 JSON 格式的内容

image-20231126225305783

注:发送数据时,还有一个 Content-Type: application/json

image-20231126225755414

查看服务端接收到的数据

image-20231126230004564

将前端接收数据,JSON 字符串转换为 “对象格式”

// 2、服务端怎么知道流完了
req.on("end", () => {
  // 将接收到的 bodyStr JSON 字符串转换成 对象格式
  const body = JSON.parse(bodyStr);
  console.log("body 是服务端接收到的数据:", body);
  // }
  // 先简单返回字符串
  res.end("接收完成");
});

转换数据格式后,再次用 postman 发送请求,服务端接收到的数据变为 “对象格式”

image-20231126234305278

注:

  • 我们目前是强制将 JSON 字符串变成了 对象格式,但这样并不靠谱
  • 如果前端发送请求时(postman)Content-Type 并没有设置为 JSON 的数据格式,我们像上边代码中用 JSON.parse(...) 方法来解析就会报错
  • 因此,我们需要在服务端代码中先判断 前端传递的数据是不是 JSON 格式

/src/index.js 中判断前端传递的数据是不是 JSON 格式

const http = require("http");

const server = http.createServer((req, res) => {
  const url = req.url;
  const path = url.split("?")[0]; // /api/list
  const method = req.method;

  // 定义路由:模拟获取评论列表
  if (path === "/api/list" && method === "GET") {
    // res.end('<h1>这是评论列表的路由')

    // 返回结果
    const result = {
      errno: 0,
      data: [
        { username: "allen", content: "评论内容 1" },
        { username: "ibc", content: "评论内容 2" },
      ],
    };
    // 返回 JSON 数据
    res.writeHead(200, { "Content-type": "application/json" });
    // end() 方法中只能是字符串,需要通过 JSON.stringify() 转换
    res.end(JSON.stringify(result));

    // 定义路由:模拟创建评论
  } else if (path === "/api/create" && method === "POST") {
    // res.end("<h1>这是模拟创建评论的路由 ! </h1>")

    // -------------------------------------

    // 获取接收前端数据的类型
    const reqType = req.headers["content-type"];
    // 打印输出接收前端的数据类型
    console.log(reqType);

    // 定义接受前端数据
    let bodyStr = "";
    // 1、服务端怎么识别流,并接收数据
    // chunk 即:流 的每一段数据(数据小一次就流过去了,数据大时就多流几次)
    req.on("data", (chunk) => {
      // 通过 chunk 来接受数据,每次流过来的数据都累加上
      bodyStr += chunk.toString();
    });
    // 2、服务端怎么知道流完了
    req.on("end", () => {
      // 判断前端传来的数据是否是 JSON 格式(解析前要确保你的数据是标准的 JSON 格式,否则会解析出错)
      if (reqType === "application/json") {
        // 将接收到的 bodyStr JSON 字符串转换成 对象格式
        const body = JSON.parse(bodyStr);
        console.log("body 是服务端接收到的数据:", body);
      }
      // 先简单返回字符串
      res.end("接收完成");
    });

    // 返回结果
    // const result = {
    //     errno: 0,
    //     message: '创建成功'
    // }
    // // 返回 JSON 数据
    // res.writeHead(200, { 'Content-Type': 'text/json' })
    // res.end(JSON.stringify(result))
  } else {
    // 未命中路由,Response 返回给前端 404(字符串形式)
    // res.writeHead(404, { 'Content-Type': 'text/plain' })
    // // 返回字符串
    // res.end('404 Not Found')

    // 将 404 返回字符串格式,改为返回 HTML 网页
    res.writeHead(404, { "Content-Type": "text/html" });
    res.end(`
            <!DOCTYPE html>
            <html lang="en">
            <head>
                <meta charset="UTF-8" />
                <meta http-equiv="X-UA-Compatible" content="IE=edge" />
                <meta name="viewport" content="width=device-width, initial-scale=1.0" />
                <title>404</title>
            </head>
            <body>
                <h1>404 Not Found</h1>
            </body>
            </html>
        `);
  }
});

// 监听 http 请求,默认 3000 端口
server.listen(3000);
console.log("http 请求已经被监听,3000 端口,请访问 http://localhost:3000");

TIP

启动 Node 服务后,在 postman 中发送请求,并观察服务端的打印输出信息

  • 根据传入数据的类型判断是否要将 JSON 字符串转换为 对象格式
  • 在 postman 中,如果请求传入的数据为 非 JSON 字符串,服务端就不解析
  • 如果是 JSON 字符串格式,即 解析

GIF-2023-11-27-2-47-41

上次更新时间: 2/4/2024, 1:20:27 AM

大厂最新技术学习分享群

大厂最新技术学习分享群

微信扫一扫进群,获取资料

X