# HTTP 协议,请求响应报文、状态码、HTTP 模块实践

TIP

从本节内容开始,系统性学习和介绍 HTTP 协议,网络基础、http 模块 等相关原理、应用实践

  • HTTP 协议,请求响应报文、状态码
  • HTTP 模块

# 一、HTTP 协议

TIP

深入浅出 HTTP 协议、HTTP 报文、请求报文结构、请求行、请求头、请求体、响应报文结构 与 响应行,响应头 与 响应体 等。

之前我们在 前后端数据交互 与 HTTP 协议 (opens new window) 的章节,有学习过相关知识。

# 1、HTTP 协议是什么

TIP

HTTP 全称:Hyper Text Transfer Protocol 超文本传输协议(HTTP 是这几个英文单词的首字母)

HTTP 协议也是互联网应用最广泛的协议之一,在 Web 中(无论前后端)都是非常重要的,且必须掌握

# 1.1、协议的本质

TIP

  • 协议:双方必须共同遵从的一组约定,协议的本质就是 “约定”
  • 如:保密协议(对员工和公司的行为做了约束,员工按照企业要求保守商业秘密、公司也要按照协议的要求支付员工一定的酬劳);离婚协议(对夫妻双方的行为的一种约束,父亲按要求支付孩子抚养费、母亲按协议要求对孩子进行抚养,按照协议平分家庭资产)等

# 1.2、HTTP 协议的约束对象

TIP

HTTP 协议是对浏览器 和 服务器之间的通信进行了约束。

  • 浏览器 给 服务器发送数据,这个行为称之为 “请求”
  • 服务器 给 浏览器返回结果,这个行为称之为 “响应”
  • 浏览器 给 服务器发送的数据,对应的内容 称之为 “请求报文”
  • 服务器 给 浏览器返回的结果内容,称之为 “响应报文”

image-20240202122819613

注:

日常使用过程中,在浏览器地址栏中输入 https://www.baidu.com 回车,此时

  • 给服务器发送了一个 HTTP 请求报文
  • 然后,服务器也返回了一个 HTTP 响应
  • 准确来说整个过程中不是一次请求,而是多次请求,包括请求报文 和 响应后边详细来解读

image-20231211173738043

# 2、查看 HTTP 报文

TIP

查看 HTTP 报文内容,需要安装一个客户端软件 Fiddler

Fiddler 官方下载地址:https://www.telerik.com/download/fiddler (opens new window)(直接默认下一步安装即可)安装包也可在裙文件中下载

# 2.1、Fiddler 的工作原理

TIP

Fiddler 是位于客户端和服务器端的 HTTP 代理,也是目前最常用的 http 抓包工具之一 。 它能够记录客户端和服务器之间的所有 HTTP 请求,可以针对特定的 HTTP 请求,分析请求数据、设置断点、调试 web 应用、修改请求的数据,甚至可以修改服务器返回的数据。

既然是代理,也就是说:客户端的所有请求都要先经过 Fiddler,然后转发到相应的服务器,反之,服务器端的所有响应,也都会先经过 Fiddler 然后发送到客户端。

所以 web 客户端和服务器的请求如下图

image-20240202123837390

# 2.2、Fiddler 的基础配置

TIP

安装完成后,在 Windows 菜单栏中输入 “Fiddler ” 点击即可打开

①、打开 Fiddler 后,选择 “Cancel”

image-20231212184412332

②、添加 Windows 信任的 CA 列表

image-20231212185448824

点击 “是”

image-20231212192815875

image-20231212192849441

③、关闭 Fiddler 软件,重新打开(重启)

image-20231212193253058

④、查看浏览器的请求

为了只查看浏览器的请求,点击 Fiddler 底部状态栏中的 “All Processes” 即:监听所有的进程 -> 选择 “Web Browsers” 即:只会监听浏览器发送的请求

image-20231212193900410

# 2.3、使用 Fiddler 查看请求报文

①、在浏览器中输入 “www.baidu.com” ,在 Fiddler 可以看到很多 HTTP 请求

image-20231212194619113

其中第一个 HTTP 请求,即:我们输入 “www.baidu.com” 网址时 发送的请求

②、双击 Fiddler 列表中的 “www.baidu.com” 网址,即可查看请求报文 和 响应报文

image-20231212200136008

③、响应返回结果乱码问题,点击 “黄色警告提示” 默认响应体会编码,点击进行解码即可

image-20231212200608313

解码后就正常显示了

image-20231212200934150

# 3、HTTP 请求报文结构

TIP

将 Fiddler 获取到的百度的 HTTP 请求报文信息拿到,如下

GET https://www.baidu.com/ HTTP/1.1
Host: www.baidu.com
Connection: keep-alive
Cache-Control: max-age=0
sec-ch-ua: "Google Chrome";v="119", "Chromium";v="119", "Not?A_Brand";v="24"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Cookie: BD_UPN=12314753; PSTM=1670490582;BIDUPSID=68EEAE691F2B30B2B80666CE4FAA26DF; ...

请求报文结构解读

image-20231213091055646

HTTP 请求报文主要包括三部分

  • 第一部分:请求行,是请求报文的第一行内容;
  • 第二部分:从报文的第二行开始 -> 到空行之前 为 请求头;
  • 第三部分:请求体(以上给百度发送请求的请求体目前是空的,后边演示非空清空下的请求体);

注:请求头 与 请求体 之间是有一个空行的。以上就是 HTTP 请求报文的基本组成部分

# 4、HTTP 请求报文 - 请求行

TIP

请求行相关内容

GET https://www.baidu.com/ HTTP/1.1

请求行结构解读

image-20231213092324986

# 4.1、HTTP 请求方法

TIP

请求行中的第一个部分:HTTP 协议的常见请求方法

请求方法 作用
GET 主要用于获取数据
POST 主要用于新增数据
PUT / PATCH 主要用于更新数据
DELETE 主要用于删除数据
OPTIONS、TRACE、CONNECT、HEAD 很少用到

# 4.2、URL

TIP

  • 请求行中的第二个部分:URL(Uniform Resource Locator)统一资源定位符
  • URL 它本身也是一个字符串,用来定位服务器中的资源
  • 服务器上会存在很多资源,浏览器向服务器发送请求获取其中的某一个资源,此时就需要通过 URL 来定位其中的某一个资源。将请求发送到服务器后,服务器就会通过 URL 去定位,找到后再把结果返回给浏览器

因此,我们将 URL 是用来定位资源的

# 4.3、URL 的组成部分

以当前请求的 URL 为例

https://www.baidu.com/

注:

  • https 协议名称
  • www.baidu.com 主机名,可以是域名 或 IP 地址。主机名用来定位网络上的计算机
  • / 路径,用来定位服务器中的某一个资源
  • :// 协议的固定组成部分,不论什么协议都是必须得有的

# 4.4、复杂 URL 的组成部分

相对复杂的 URL 组成部分

https://www.icodingedu.com:443/search?q=web&type=course

注:

  • https 协议名称
  • :// 协议的固定组成部分,不论什么协议都是必须得有的
  • www.icodingedu.com 主机名(域名)
  • :443 端口号(部分情况下可省略不写)
  • /search 路径,以 / 开头 与 ? 之前的部分为 路径。用来定位服务器中的某一个资源
  • ?q=web&type=course 查询字符串,是键名、键值的结构,& 为分隔符。用于向服务器传递额外的参数

# 4.5、HTTP 版本号

TIP

常见的 HTTP 版本号

版本号 发布时间
1.0 1996 年
1.1 1999 年
2 2015 年
3 2018 年

# 5、HTTP 请求报文 - 请求头

TIP

  • HTTP 请求报文中的请求头是由一些列的键值对组成的
  • 冒号前面是 键名(如:Host),冒号空格 后边是 键值(如:www.baidu.com

image-20231213163753531

关于请求头相关键值的解读

字段 解释
Host 指定请求的主机名,这里是www.baidu.com
Connection 保持连接,值为keep-alive,表示在完成当前请求后保持连接打开状态。
Cache-Control 控制缓存行为,max-age=0表示不使用缓存,每次请求都去服务器获取最新的内容。
sec-ch-ua 安全性用户代理字段,包含浏览器类型和版本等信息。
sec-ch-ua-mobile 指示是否为移动设备,这里值为?0,表示不是移动设备。
sec-ch-ua-platform 指定操作系统和平台,这里是"Windows"。
Upgrade-Insecure-Requests 指示是否支持将不安全的 HTTP 请求升级为更安全的 HTTPS 请求,这里值为 1,表示支持。
User-Agent 用户代理字符串,包含浏览器类型、版本、操作系统等信息。
Accept 指定客户端可以接受的响应内容类型,例如text/html表示 HTML 文档。
Sec-Fetch-Site 安全上下文中的站点类型,这里是none表示没有特定站点上下文。
Sec-Fetch-Mode 安全上下文中的请求模式,这里是navigate表示导航请求。
Sec-Fetch-User 指示是否为用户的请求,这里值为?1表示是用户的请求。
Sec-Fetch-Dest 指定请求的目标类型,这里是document表示请求的是文档。
Accept-Encoding 指定客户端可以接受的压缩编码格式,包括gzip, deflate, 和 br等。
Accept-Language 指定客户端希望接收的语言内容,这里是简体中文。
Cookie 服务端发送到客户端的 cookie 信息,通常用于保存用户的登录状态或其他相关信息。

# 6、HTTP 请求报文 - 请求体

TIP

之前我们在浏览器地址栏中输入百度网址,发送请求时,请求体是空的。

  • 请求体的内容格式是非常灵活的,可以设置任意内容
  • 只要与后端预定好即可,格式不限

接下来,我们来查看有请求体的请求报文(请求体不为空的情况)

image-20231214233153373

注:

  • 请求方式是 “POST”
  • 请求体为一个 JSON 字符串(由前后端约定好的数据格式决定)以上为登录时的用户名 和 密码
  • 请求体的格式是不限的,可尝试查看不同的平台

也可拿任意网站的 POST 请求,请求体不为空时来作为演示。如:各个平台的表单提交

# 7、HTTP 响应报文

TIP

在浏览器中输入 www.baidu.com ,即可在 Fiddler 中查看 HTTP 响应报文

image-20231215002811845

响应报文结构解读

image-20231215003517969

# 8、HTTP 响应报文 - 响应行

TIP

响应行是由三部分组成

HTTP/1.1   200   OK
  • HTTP/1.1 HTTP 协议版本号
  • 200 响应状态码
  • OK 响应状态的描述

# 8.1、响应状态码

常见的状态码如下

状态码 描述
200 请求成功
403 禁止请求
404 找不到资源
500 服务器内部错误

注:HTTP 协议对状态码进行了分类,共分为 5 大类。如下

# 8.2、状态码分类

状态码 描述
1xx 信息响应
2xx 成功响应
3xx 重定向响应
4xx 客户端错误响应
5xx 服务端错误响应

# 8.3、响应状态的描述

状态码 状态描述 含义
200 OK 成功
403 Forbidden 被禁止的
404 Not Found 不存在
500 Internal Server Error 内部服务器错误

注:

更多状态查阅官方文档:HTTP 响应状态码 (opens new window) ,可在浏览器中 Ctrl + F 搜索对应状态码

# 9、HTTP 响应报文 - 响应头

TIP

响应头:记录了与服务器相关的内容

image-20231216020927661

响应头信息解读

响应头名称 响应头值 描述
HTTP/1.1 200 OK 表示请求成功,返回的状态码是 200。使用的 HTTP 版本是 1.1。
Connection keep-alive 建议客户端保持连接,以便后续的请求可以重用同一个 TCP 连接。
Content-Security-Policy frame-ancestors 'self' https://chat.baidu.com http://mirror-chat.baidu.com ... 限制哪些源可以嵌入当前页面的内容。允许从同源的 URL 加载内容,并列出了一些允许的 URL。
Content-Type text/html; charset=utf-8 响应的内容类型是 HTML,字符集是 UTF-8。
Date Fri, 15 Dec 2023 18:10:25 GMT 响应的日期和时间。
Isprivate 1 表示响应是私有的或需要某种特定的处理。
Server BWS/1.1 表示服务器类型或版本,可能是百度 Web 服务器的版本。
Set-Cookie H_PS_PSSID=...; path=/; expires=Sat, 14-Dec-24 18:10:25 GMT; domain=.baidu.com 设置 cookie,通常用于跟踪用户的会话信息或保存一些特定的用户信息。
Traceid 1702663825041124890613909208681376480177 可能用于追踪或调试的 ID,帮助开发者识别请求的来源或其他相关信息。
X-Ua-Compatible IE=Edge,chrome=1 指示浏览器使用 Edge 模式(针对 Internet Explorer)或 Chrome 模式来渲染页面。
Content-Length 459259 表示响应体的长度是 459259 字节。

更多响应头信息查阅官方文档:HTTP 标头(header) (opens new window)

# 10、HTTP 响应报文 - 响应体

TIP

响应体 与 请求体类似

image-20231216022155155

注:

响应体的内容格式非常灵活,常见的响应体格式有

  • HTML
  • CSS
  • JavaScript
  • 图片
  • 视频
  • JSON

HTML、CSS、JavaScript 在网络中传输时,都是放在响应报文的响应体中返回给浏览器的

image-20231216023342760

可点击以上不同类型的响应体,查看到不同格式的具体内容

# 二、HTTP 模块

TIP

深入浅出 NodeJS 的 HTTP 模块,创建 HTTP 服务,查看、获取、设置 HTTP 请求和响应报文应用实践,搭建静态资源服务、错误处理等。

# 1、创建 HTTP 服务

TIP

HTTP 模块可用来创建 HTTP 服务,有了 HTTP 服务后可处理浏览器发送的请求,并还可给浏览器返回响应。

从本节开始我们需要转换身份,原来我们前端项目开发的主战区是在 浏览器(HTML、CSS、JavaScript)主要是进行前端开发,即:页面构建、样式的控制、页面的交互这些都是前端的范畴。

现在我们开始进行后端的开发,主要是处理浏览器发送的请求,服务器返回对应的响应结果

/src/http-server.js 中,创建 HTTP 服务(需要 3 步)

// 1、导入 http 模块
const http = require("http");

// 2、创建服务对象
// http 对象
// createServer(创建服务)http 对象的 方法
// 返回结果:对象,使用 server 接收
// 接收一个实参:箭头函数、function 函数都可
// request(请求)和 response(响应)为形参,名称可以随意定义
// (request, response) => {} 该箭头函数在内部调用时 会接收两个实参
// request:是对请求报文的一个封装对象,可获取到请求报文中的请求内容(请求行、请求头、请求体 都可获取)
// response:对响应报文的封装对象,借助 response 对象可为浏览器设置响应结果(响应行、响应头、响应体 都可获取)
// 该回调函数执行时间:当服务接收到 HTTP 请求时就会执行
const server = http.createServer((request, response) => {
  // 设置响应体
  response.end("<h1>Received request</h1>"); // 收到请求
});

// 3、监听端口,启动服务
server.listen(3000, () => {
  console.log(
    "服务已启动,http 请求已经被监听,3000 端口,请访问 http://localhost:3000"
  );
});

在命令行终端中,通过 node 命令运行

node .\src\http-server.js

# 1.1、HTTP 服务请求响应过程

TIP

当我们在命令行窗口中运行完以上命令后,3000 的端口就会被 NodeJS 脚本占用了

image-20231230005859264

使用 Fiddler 查看请求和响应报文

image-20231230015140893

# 1.2、HTTP 服务注意事项

  • ①、命令行 Ctrl + C 停止服务
  • ②、当服务启动后,更新代码必须重启服务才能生效
  • ③、响应体内容有中文时会乱码,解决方法
// 在 createServer 方法的箭头函数体中新增如下代码

// 设置响应头,设置编码集防止乱码
response.setHeader("content-type", "text/html;charset=utf-8");

// 含义:服务端在返回结果时,添加一个响应头,值为 text/html;charset=utf-8
  • ④、端口号被占用

image-20231230230856967

解决办法

  • 关闭当前正在运行监听端口的额服务(常用)
  • 修改其他端口号

⑤、HTTP 协议默认端口为 80 端口

TIP

HTTP 服务在日常开发中常用的端口有 3000、8080、8090、9000、3128、8081、9080 等

如端口被其他程序占用,可使用 资源监视器 (windows 中) 找到占用端口的程序,然后使用任务管理器关闭对应的程序。

点击 “开始” -> 搜索 “资源监视器” -> 选择网络 “侦听端口” -> 点击按 “端口” 排序 -> 找到对应端口的 PID

image-20240101171012136

打开 “任务管理器” -> 选择 “详细信息” -> 找到对应的 PID -> 点击右键 “结束进程” 即可

image-20240101171405190

# 2、浏览器查看 HTTP 报文

TIP

在 Google 浏览器中,点击右键 “检查” 或 “F12” -> 打开 “NetWork” -> 点击 “当前 URL”

image-20240101233828529

注:

  • 点击请求标头,其中所列出的信息都是请求头
  • 勾选 Raw(查看源代码)可看到请求行 和 请求头 所有的内容
  • 此时,请求体为空

# 2.1、查看请求体内容

TIP

查看请求体,可通过发送 POST 请求来查看

/src/index.html 中发送 POST 请求

<!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>查看请求体内容</title>
  </head>
  <body>
    <form action="http://localhost:3000" method="post">
      用户名:<input type="text" name="username" /> <br /><br />&nbsp;&nbsp;&nbsp;码:<input type="password" name="password" />
      <br /><br />
      <input type="submit" value="登 录" />
    </form>
  </body>
</html>

GIF-2024-1-2-0-14-07

注:

当请求中有请求体的内容时,NetWork 中就会多一个 Payload(载荷)的选项

  • 默认的 Form Data 是格式化后的请求体内容(有键名 和 键值 组成)
  • 原始的请求体内容,可点击 “view source” 查看

直接访问 “http://localhost:3000” 时,没有请求体内容,同时 NetWork 中也不会有 Payload(载荷)的选项

# 2.2、查看 URL 中的查询字符串

TIP

也可以通过 Payload(载荷)的选项来查看

  • “Query String Parameters” 查询字符串格式化后的键值
  • “view source” 查询字符串的原始内容

在浏览器中输入 “http://localhost:3000/?type=1&count=10”

GIF-2024-1-2-0-28-38

# 3、浏览器查看响应报文

TIP

在 Google 浏览器中,点击右键 “检查” 或 “F12” -> 打开 “NetWork” -> 点击 “当前 URL”

GIF-2024-1-2-0-40-39

注:

  • 点击 “Response Headers” 查看相应头信息
  • 勾选 Raw(查看源代码)可看到响应行 和 响应头 所有的内容
  • 点击 “Response” 查看响应体的原始内容

通过以上演示,我们借助浏览器控制台即可查看到请求报文 和 响应报文的所有内容,不需要再打开额外的其他软件。

因此,后续项目开发中我们都会使用浏览器的控制台来查看 HTTP 报文。

# 4、获取 HTTP 请求报文

TIP

为什么需要获取 HTTP 请求的报文中的内容 ?

浏览器向服务器发送 HTTP 请求,获取请求报文的目的是为了服务器能给浏览器返回一个正确的结果(本质上就是为了明确服务器返回给浏览器的需求是什么)即想要返回浏览器想要的结果,就必须从请求报文中提取到相关的数据才行。(类似去饭店点餐,你得告诉服务员你的需求,才能迟到你想要的菜品)

获取请求的数据,需要通过 request 对象来实现

方法 描述 掌握程度
request.method 请求方法 ***
request.httpVersion 请求版本 *
request.url 请求路径 ***
require('url').parse(request.url).pathname URL 路径 ***
require('url').parse(request.url, true).query URL 查询字符串 ***
request.headers 请求头 ***
request.on('data', function() { })
request.on('end', function() { })
请求体 *

注:

  • request.url 只能获取路径以及查询字符串,无法获取 URL 中的域名以及协议的内容
  • request.headers 将请求信息转化成一个对象,并将属性名都转化成了 小写
  • 关于路径:如果访问网站的时候,只填写了 IP 地址或者是域名信息,此时请求的路径为/
  • 关于 favicon.ico:这个请求是属于浏览器自动发送的请求

# 4.1、获取请求报文的过程

/src/http-server.js

// 1、导入 http 模块
const http = require("http");

// 2、创建服务对象
const server = http.createServer((request, response) => {
  // 设置响应头,设置编码集防止乱码
  response.setHeader("content-type", "text/html;charset=utf-8");
  // 设置响应体
  response.end("<h1>Received request 收到请求</h1>"); // 收到请求
});

// 3、监听端口,启动服务
server.listen(3000, () => {
  console.log(
    "服务已启动,http 请求已经被监听,3000 端口,请访问 http://localhost:3000"
  );
});

在命令行终端中,通过 node 命令运行

node .\src\http-server.js

此时,在浏览器地址栏输入 http://localhost:3000 访问,在 “NetWork” 中查看请求报文

image-20240103182359638

注:

在浏览器输入 URL 回车后,就会将在 “NetWork” 中看到的原始请求报文 发送给服务端的 9000 端口,由 http.createServer() 中的回调函数 (request, response) => {} 来处理该请求报文,同时还会返回响应报文。

因此,要获取请求报文中的内容,就必须在 回调函数 (request, response) => {} 里边才能获取到。

# 4.2、获取请求报文中的内容

/src/http-server.js

// 1、导入 http 模块
const http = require("http");

// 2、创建服务对象
const server = http.createServer((request, response) => {
  // 设置响应头,设置编码集防止乱码
  response.setHeader("content-type", "text/html;charset=utf-8");

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

  // 提取 HTTP 请求报文

  // 获取请求方法
  // console.log(request.method) // GET GET 控制台会打印两个 GET 是因为浏览器发送了两次请求,其中一个是 favicon.ico 图标请求

  // 获取请求的 URL
  // console.log(request.url) // 只包含 url 中的路径 与 查询字符串

  // 获取 HTTP 协议的版本号
  // console.log(request.httpVersion) // 1.1

  // 获取 HTTP 的请求头
  // console.log(request.headers) // 对象,其中键值是请求头中的内容
  // 获取请求头其中的主机名
  console.log(request.headers.host); // localhost:3000

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

  // 设置响应体
  response.end("<h1>Received request 收到请求</h1>"); // 收到请求
});

// 3、监听端口,启动服务
server.listen(3000, () => {
  console.log(
    "服务已启动,http 请求已经被监听,3000 端口,请访问 http://localhost:3000"
  );
});

# 5、获取 HTTP 请求报文 - 请求体中的内容

TIP

获取 HTTP 报文的请求体内容

/src/request-body.js

// 1、导入 http 模块
const http = require("http");

// 2、创建服务对象
const server = http.createServer((request, response) => {
  // 设置响应头,设置编码集防止乱码
  response.setHeader("content-type", "text/html;charset=utf-8");

  // 获取请求体内容,共 3 步

  // ①、声明一个变量,用该变量接收请求体的结果
  let body = "";
  // ②、绑定 data 事件,在可读流对象中,获取可读流对象中的内容,就需要绑定 data 事件,然后一点点将内容取出来(而 request 对象本身就是一个可读流对象,因此可通过可读流的方式将请求体中的内容一点点的取出来)
  // chunk 表示每次取出一部分
  request.on("data", (chunk) => {
    // 将取出的一部分存储在 body 变量中
    body += chunk;
  });
  // ③、绑定 end 事件,当我们把数据取完后所触发的事件(即:将可读流中的数据读完后,就会触发 end 事件)
  request.on("end", () => {
    // 打印输出
    console.log(body);
    // 浏览器端的响应结果
    response.end("<h1>Received request 收到请求</h1>"); // 收到请求
  });
});

// 3、监听端口,启动服务
server.listen(3000, () => {
  console.log(
    "服务已启动,http 请求已经被监听,3000 端口,请访问 http://localhost:3000"
  );
});

在命令行终端中,通过 node 命令运行

node .\src\request-body.js

在浏览器地址栏输入 http://localhost:3000 回车,此时查看 VSCode 控制台打印输出的结果

image-20240104010634294

为什么打印输出结果为空呢 ?

因为,在浏览器中输入 URL,回车后发送的 HTTP 请求为 GET 请求,而 GET 请求的请求体一般都是空的,所以在 VSCode 的控制台中输出的请求体内容也为空。

要想在控制台输出请求体内容,就必须要发送一个请求体不为空的请求才可以。

/src/index.html 中通过 form 表单发送一个 POST 请求

<!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>查看请求体内容</title>
  </head>
  <body>
    <form action="http://localhost:3000" method="post">
      用户名:<input type="text" name="username" /> <br /><br />&nbsp;&nbsp;&nbsp;码:<input type="password" name="password" />
      <br /><br />
      <input type="submit" value="登 录" />
    </form>
  </body>
</html>

在浏览器中输入 http://127.0.0.1:5500/src/index.html 访问 form 请求页面,点击登录

GIF-2024-1-4-1-17-06

注:

此时, VSCode 的控制台中就会输出请求体的内容了。以上获取请求体内容的方法了解即可,后边还有更简洁的方法来提取报文的请求体。

# 6、获取 HTTP 请求报文 - 请求路径与查询字符串

TIP

提取 HTTP 请求报文中的 请求路径 和 查询字符串这个两个部分,很重要

image-20240104153004949

注:

很重要的原因在于,服务器端返回不同的结果是根据 URL 路径 或 查询字符串来判定的。

不同的 URL 路径返回不同的结果

  • https://baike.baidu.com/vbaike
  • https://baike.baidu.com/calendar
  • https://baike.baidu.com/museum

不同的查询字符串返回不同的结果,即根据 wd 参数的不同来返回不同结果的

  • https://www.baidu.com/s?wd=ibc
  • https://www.baidu.com/s?wd=abc
  • https://www.baidu.com/s?wd=123

因此,我们作为服务提供者会根据 URL 路径 和 查询字符串来返回用户想要的结果

# 6.2、获取请求路径

/src/url-query-string.js

// 导入 http 模块
const http = require("http");
// 1、导入 url 模块
const url = require("url");

// 创建服务对象
const server = http.createServer((request, response) => {
  // 2、解析 request.url (该结果中包含了 url 路径 和 查询字符串)
  // console.log(request.url) // /search/?type=1&count=10
  // 只需要 url 路径 或 查询字符串(parse 方法用来解析 url)
  let res = url.parse(request.url); // 解析完成后是一个对象
  // 3、获取路径(对象中的属性 pathname 为 url 路径)
  let pathname = res.pathname;
  console.log(pathname); // /search/

  // 浏览器端的响应结果
  response.end("<h1>URL - QueryString</h1>"); // 收到请求
});

// 监听端口,启动服务
server.listen(3000, () => {
  console.log(
    "服务已启动,http 请求已经被监听,3000 端口,请访问 http://localhost:3000"
  );
});

在命令行终端中,通过 node 命令运行

node .\src\url-query-string.js

在浏览器的地址栏中输入 http://localhost:3000/search/?type=1&count=10 回车,查看 VSCode 命令行终端的打印输出

image-20240105005954954

# 6.3、获取查询字符串

/src/url-query-string.js

// 导入 http 模块
const http = require("http");
// 1、导入 url 模块
const url = require("url");

// 创建服务对象
const server = http.createServer((request, response) => {
  // 2、解析 request.url (该结果中包含了 url 路径 和 查询字符串)
  // 只需要 url 路径 或 查询字符串(parse 方法用来解析 url)
  let res = url.parse(request.url);
  console.log(res);

  // 浏览器端的响应结果
  response.end("<h1>URL - QueryString</h1>"); // 收到请求
});

// 监听端口,启动服务
server.listen(3000, () => {
  console.log(
    "服务已启动,http 请求已经被监听,3000 端口,请访问 http://localhost:3000"
  );
});

启动服务,在浏览器的地址栏中输入 http://localhost:3000/search/?type=1&count=10 回车,查看 VSCode 命令行终端的打印输出

image-20240105010622917

如果我们需要获取查询字符串中 typecount 的值就会非常麻烦

此时,我们可以使用 url.parse() 方法中的第二个参数 parseQueryString ,将该参数设置为 trueparse() 方法就会将 查询字符串的值 返回为对象

image-20240105011038674

/src/url-query-string.js

// 导入 http 模块
const http = require("http");
// 1、导入 url 模块
const url = require("url");

// 创建服务对象
const server = http.createServer((request, response) => {
  // 2、解析 request.url (该结果中包含了 url 路径 和 查询字符串)
  // 只需要 url 路径 或 查询字符串(parse 方法用来解析 url)
  let res = url.parse(request.url, true);
  console.log(res);

  // 3、获取查询字符串 中的 type 和 count 属性的值
  let type = res.query.type;
  let count = res.query.count;
  console.log(type, count); // 1 10

  // 浏览器端的响应结果
  response.end("<h1>URL - QueryString</h1>"); // 收到请求
});

// 监听端口,启动服务
server.listen(3000, () => {
  console.log(
    "服务已启动,http 请求已经被监听,3000 端口,请访问 http://localhost:3000"
  );
});

启动服务,在浏览器的地址栏中输入 http://localhost:3000/search/?type=1&count=10 回车,查看 VSCode 命令行终端的打印输出

image-20240105012413865

注:

url.parse() (opens new window) 在 Node.js V20.x 版本中已弃用,改用 WHATWG URL (opens new window) API

# 7、获取请求路径与查询字符串 - 新方法

TIP

与上边实现同样的需求:获取 HTTP 请求报文中的 请求路径 与 查询字符串的值,本次使用另一种方法来实现。

详细查阅官方文档 new URL(input[, base]) (opens new window)

/src/url-query-string-new.js

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

// 创建服务对象
const server = http.createServer((request, response) => {
  // 使用新方法 URL 来提取查询字符串
  // 实例化 URL 的对象(实例化是可接收一个参数 也可 接收两个参数)
  let url = new URL("https://www.ibc.com/search?type=1&count=10");
  // 打印输出 url 对象
  console.log(url);

  // 浏览器端的响应结果
  response.end("<h1>URL - QueryString - New</h1>"); // 收到请求
});

// 监听端口,启动服务
server.listen(3000, () => {
  console.log(
    "服务已启动,http 请求已经被监听,3000 端口,请访问 http://localhost:3000"
  );
});

启动服务,在浏览器的地址栏中输入 http://localhost:3000/search/?type=1&count=10 回车,查看 VSCode 命令行终端的打印输出

image-20240105161947927

注:

在以上控制台输出的 URL 对象中可以看到相关的信息,要获取 请求路径 和 查询字符串就变的容易了

/src/url-query-string-new.js 中,使用 实例化 URL 对象,两个参数的情况

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

// 创建服务对象
const server = http.createServer((request, response) => {
  // 使用新方法 URL 来提取查询字符串
  // 实例化 URL 的对象(实例化是可接收一个参数 也可 接收两个参数)
  let url = new URL("/search?type=1&count=10", "https://www.ibc.com");
  // 打印输出 url 对象
  console.log(url);

  // 浏览器端的响应结果
  response.end("<h1>URL - QueryString - New</h1>"); // 收到请求
});

// 监听端口,启动服务
server.listen(3000, () => {
  console.log(
    "服务已启动,http 请求已经被监听,3000 端口,请访问 http://localhost:3000"
  );
});

启动服务,在浏览器的地址栏中输入 http://localhost:3000/search/?type=1&count=10 回车,查看 VSCode 命令行终端的打印输出,与一个参数输出的结果一样。

image-20240105180610228

/src/url-query-string-new.js 中,使用 实例化 URL 对象,两个参数的情况(动态获取 请求路径 和 查询字符串)

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

// 创建服务对象
const server = http.createServer((request, response) => {
  // 使用新方法 URL 来提取查询字符串
  // 实例化 URL 的对象(实例化是可接收一个参数 也可 接收两个参数)
  // let url = new URL('/search?type=1&count=10', 'https://www.ibc.com')

  // 将以上参数替换为 动态获取 request.url
  let url = new URL(request.url, "http://127.0.0.1:3000");

  // 打印输出 url 对象
  console.log(url);

  // 浏览器端的响应结果
  response.end("<h1>URL - QueryString - New</h1>"); // 收到请求
});

// 监听端口,启动服务
server.listen(3000, () => {
  console.log(
    "服务已启动,http 请求已经被监听,3000 端口,请访问 http://localhost:3000"
  );
});

启动服务,在浏览器的地址栏中输入 http://localhost:3000/search/?type=1&count=10 回车,查看 VSCode 命令行终端的打印输出,动态获取的 请求路径 和 查询字符串 。

image-20240105181120719

通过 searchParams 属性的 get() 方法获取查询字符串 typecount 的值

/src/url-query-string-new.js

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

// 创建服务对象
const server = http.createServer((request, response) => {
  // 使用新方法 URL 来提取查询字符串
  // 实例化 URL 的对象(实例化是可接收一个参数 也可 接收两个参数)
  // let url = new URL('/search?type=1&count=10', 'https://www.ibc.com')

  // 将以上参数替换为 动态获取 request.url
  let url = new URL(request.url, "http://127.0.0.1:3000");
  // 获取 请求路径
  console.log(url.pathname); // /search/
  // 获取查询字符串 type 和 count 的值
  console.log(url.searchParams.get("type"), url.searchParams.get("count")); // 1 10

  // 打印输出 url 对象
  console.log(url);

  // 浏览器端的响应结果
  response.end("<h1>URL - QueryString - New</h1>"); // 收到请求
});

// 监听端口,启动服务
server.listen(3000, () => {
  console.log(
    "服务已启动,http 请求已经被监听,3000 端口,请访问 http://localhost:3000"
  );
});

启动服务,在浏览器的地址栏中输入 http://localhost:3000/search/?type=1&count=10 回车,查看 VSCode 命令行终端的打印输出,动态获取的 请求路径 和 查询字符串 。

image-20240106020754043

# 8、HTTP 请求 - 应用实践

TIP

需求:搭建一个 HTTP 服务,根据不同的请求地址,返回不同的响应结果

  • 当浏览器向服务发送请求类型为 get ,请求地址为 /list ,则返回响应结果为 “列表页面”
  • 当浏览器向服务发送请求类型为 get ,请求地址为 /detail ,则返回响应结果为 “详情页面”

/src/http-application.js

// 1、导入 http 模块
const http = require("http");

// 2、创建服务对象
const server = http.createServer((request, response) => {
  // 设置响应头,设置编码集防止乱码
  response.setHeader("content-type", "text/html;charset=utf-8");

  // 获取请求类型
  let { method } = request;
  // 获取请求 URL 路径
  let { pathname } = new URL(request.url, "http://127.0.0.1:3000");
  console.log(method, pathname);

  // 判断请求类型 和 请求 URL 路径
  if (method === "GET" && pathname === "/list") {
    // 设置响应体 - 返回列表页面
    response.end("<h1>列表页面</h1>");
  } else if (method === "GET" && pathname === "/detail") {
    // 设置响应体 - 返回详情页面
    response.end("<h1>详情页面</h1>");
  }
});

// 3、监听端口,启动服务
server.listen(3000, () => {
  console.log(
    "服务已启动,http 请求已经被监听,3000 端口,请访问 http://localhost:3000"
  );
});

启动服务,在浏览器的地址栏中输入 http://localhost:3000/listhttp://localhost:3000/detail 回车,都可以正常访问,但 favicon.ico 图标请求处于 “待处理” 状态

image-20240107234931932

注:

待处理 状态的请求会一直处于请求待处理状态,服务端也并没有返回资源,这样持续建立连接只会一直占用资源,并不会产生任何效果

解决办法,增加 else 返回响应

// 1、导入 http 模块
const http = require("http");

// 2、创建服务对象
const server = http.createServer((request, response) => {
  // 设置响应头,设置编码集防止乱码
  response.setHeader("content-type", "text/html;charset=utf-8");

  // 获取请求类型
  let { method } = request;
  // 获取请求 URL 路径
  let { pathname } = new URL(request.url, "http://127.0.0.1:3000");
  console.log(method, pathname);

  // 判断请求类型 和 请求 URL 路径
  if (method === "GET" && pathname === "/list") {
    // 设置响应体 - 返回列表页面
    response.end("<h1>列表页面</h1>");
  } else if (method === "GET" && pathname === "/detail") {
    // 设置响应体 - 返回详情页面
    response.end("<h1>详情页面</h1>");
  } else {
    response.end("<h1>Not Found</h1>");
  }
});

// 3、监听端口,启动服务
server.listen(3000, () => {
  console.log(
    "服务已启动,http 请求已经被监听,3000 端口,请访问 http://localhost:3000"
  );
});

此时,再次访问 favicon.ico 就没问题了

# 9、设置 HTTP 响应报文

TIP

如何在 Node.js 中设置 HTTP 的响应报文

语法 作用
response.statusCode 设置响应状态码
response.statusMessage 设置响应状态描述
response.setHeader('头名称', '头值') 设置响应头信息
response.write('xxx')
response.end('xxx')
设置响应体

/src/http-response-message.js 中,设置 HTTP 响应报文

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

// 创建服务对象
const server = http.createServer((request, response) => {
  // 1、设置响应状态码(根据需求设置不同的状态码)
  // response.statusCode = 200
  // response.statusCode = 500

  // 2、响应状态描述(实际开发中,很少用到,默认和状态码同步)
  response.statusMessage = "icodingedu";

  // 3、响应头(设置编码集防止乱码)
  response.setHeader("content-type", "text/html;charset=utf-8");
  // 也可以设置其他的头信息(名称可自定义)
  response.setHeader("Server", "Node.js");
  // 测试
  response.setHeader("myHeader", "Test response header settings");
  // 响应头也可以设置多个同名的名称,如 set-Cookie
  response.setHeader("myTest", ["aaa", "bbb", "ccc"]);

  // 4、响应体的设置(可应用 write 也可用 end,设置了 write 就不再设置 end 了)
  // write 是可以多次调用的 并 返回响应体
  response.write("<p>我是 write 响应体 </p>");
  response.write("<p>我是 write 响应体 </p>");
  response.write("<p>我是 write 响应体 </p>");
  response.write("<p>我是 write 响应体 </p>");
  response.write("<p>我是 write 响应体 </p>");

  // 注:每一个请求,在执行回调函数的过程中 必须得有一个 end 方法(有且只能有一个 end 方法执行,不能有多个)
  response.end("我是 end 响应体 - Received request 收到请求"); // 设置响应体
  // response.end('我是 end 响应体 - Received request 收到请求') // 设置响应体
});

// 监听端口,启动服务
server.listen(3000, () => {
  console.log(
    "服务已启动,http 请求已经被监听,3000 端口,请访问 http://localhost:3000"
  );
});

image-20240109224700523

# 10、HTTP 响应 - 应用实践

TIP

需求:搭建一个 HTTP 服务,响应一个 5 行 4 列的表格,并且要求表格有 隔行换色的效果,并且点击单元格能高亮显示。

/src/response-table.js

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

// 创建服务对象
const server = http.createServer((request, response) => {
  response.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>table 表格</title>
            <style>
            table {
                border-collapse: collapse;
                width: 80%;
                margin: 50px auto;
            }
            table tr th,
            tbody tr td,
            tfoot tr td {
                text-align: center;
                line-height: 40px;
                border: 1px solid #000;
            }
            /* nth-child(2n) */
            table tbody tr:nth-child(even) {
                background-color: azure;
            }
            /* nth-child(2n + 1) */
            table tbody tr:nth-child(odd) {
                background-color: beige;
            }
            </style>
        </head>
        <body>
            <table>
            <thead>
                <tr>
                <th>专业</th>
                <th>姓名</th>
                <th>性别</th>
                <th>年龄</th>
                <th>所在城市</th>
                </tr>
            </thead>
        
            <tbody class="box">
                <tr>
                <td>计算机</td>
                <td>arry</td>
                <td>男</td>
                <td>18</td>
                <td>北京</td>
                </tr>
                <tr>
                <td>外语</td>
                <td>豆豆</td>
                <td>女</td>
                <td>21</td>
                <td>上海</td>
                </tr>
                <tr>
                <td>市场营销</td>
                <td>翠花</td>
                <td>19</td>
                <td>男</td>
                <td>深圳</td>
                </tr>
                <tr>
                <td>软件工程</td>
                <td>光头强</td>
                <td>21</td>
                <td>男</td>
                <td>广州</td>
                </tr>
            </tbody>
        
            <tfoot>
                <tr>
                <th>备注</th>
                <td colspan="4"></td>
                </tr>
            </tfoot>
            </table>
        
            <script>
            // 获取所有的 td
            let tds = document.querySelectorAll(".box td");
            let sel;
            // 遍历每一个 td
            tds.forEach((item) => {
                item.onclick = function (e) {
                // 阻止事件冒泡
                e.stopPropagation();
                if (sel) {
                    sel.style.background = "";
                }
                this.style.background = "coral";
                sel = this;
                };
            });
            // 点击
            document.body.onclick = function () {
                sel.style.background = "";
            };
            </script>
        </body>
        </html>
    `);
});

// 监听端口,启动服务
server.listen(3000, () => {
  console.log(
    "服务已启动,http 请求已经被监听,3000 端口,请访问 http://localhost:3000"
  );
});

启动服务,在浏览器的地址栏中输入 http://localhost:3000 回车查看效果,点击单元格即可选中

image-20240110131306084

注:

以上虽然已经实现我们想要的效果了,但我们在 JS 的文件中书写 HTML、CSS、前端 JS 是非常痛苦的,既没有代码提示也没有代码高亮,体验非常不好。

思考:有没有一种方法,在 NodeJS 中书写 HTML、CSS、前端 JS 代码时 有语法提示 和 代码高亮呢 ?

# 11、优化 NodeJS 中的前端代码

TIP

在 NodeJS 中书写 HTML、CSS、前端 JS 代码时,让代码有语法提示 和 代码高亮

实现方法:将 前端代码部分提取到一个单独的 HTML 文件中

新建 /src/table.html 文件

<!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>table 表格</title>
    <style>
      table {
        border-collapse: collapse;
        width: 80%;
        margin: 50px auto;
      }
      table tr th,
      tbody tr td,
      tfoot tr td {
        text-align: center;
        line-height: 40px;
        border: 1px solid #000;
      }
      /* nth-child(2n) */
      table tbody tr:nth-child(even) {
        background-color: azure;
      }
      /* nth-child(2n + 1) */
      table tbody tr:nth-child(odd) {
        background-color: beige;
      }
    </style>
  </head>
  <body>
    <table>
      <thead>
        <tr>
          <th>专业</th>
          <th>姓名</th>
          <th>性别</th>
          <th>年龄</th>
          <th>所在城市</th>
        </tr>
      </thead>

      <tbody class="box">
        <tr>
          <td>计算机</td>
          <td>arry</td>
          <td></td>
          <td>18</td>
          <td>北京</td>
        </tr>
        <tr>
          <td>外语</td>
          <td>豆豆</td>
          <td></td>
          <td>21</td>
          <td>上海</td>
        </tr>
        <tr>
          <td>市场营销</td>
          <td>翠花</td>
          <td>19</td>
          <td></td>
          <td>深圳</td>
        </tr>
        <tr>
          <td>软件工程</td>
          <td>光头强</td>
          <td>21</td>
          <td></td>
          <td>广州</td>
        </tr>
      </tbody>

      <tfoot>
        <tr>
          <th>备注</th>
          <td colspan="4"></td>
        </tr>
      </tfoot>
    </table>

    <script>
      // 获取所有的 td
      let tds = document.querySelectorAll(".box td");
      let sel;
      // 遍历每一个 td
      tds.forEach((item) => {
        item.onclick = function (e) {
          // 阻止事件冒泡
          e.stopPropagation();
          if (sel) {
            sel.style.background = "";
          }
          this.style.background = "coral";
          sel = this;
        };
      });
      // 点击
      document.body.onclick = function () {
        sel.style.background = "";
      };
    </script>
  </body>
</html>

/src/response-table.js 中,读取 /src/table.html 文件的内容

// 导入 http 模块
const http = require("http");
const fs = require("fs");

// 创建服务对象
const server = http.createServer((request, response) => {
  // 读取 table.html 文件内容
  const html = fs.readFileSync(__dirname + "/table.html");

  response.end(html); // 设置响应体
});

// 监听端口,启动服务
server.listen(3000, () => {
  console.log(
    "服务已启动,http 请求已经被监听,3000 端口,请访问 http://localhost:3000"
  );
});

注:

通过将前端代码单独拆分出来,并通过 fs 模块读取进来是可行的。这样更新前端部分的代码会非常的快捷、方便,NodeJS 服务也不用重启就能直接访问。

# 12、网页资源加载的过程

TIP

以京东、小米为例,观察网页相关资源的加载过程

image-20240110231951862

image-20240110232208517

注:

以上是京东 和 小米的网站,在浏览器地址栏中输入 URL 时,浏览器中可以看到

  • 京东向服务器发送了 189 次请求,点击查看第一个请求时 301 重定向,第二个请求是用来获取 HTML 内容
  • 小米向服务器发送了 60 次请求,点击查看第一个请求时 301 重定向,第二个请求是用来获取 HTML 内容

都是服务器先返回 HTML 内容,然后再去加载 CSS、JS 、图片等内容,整个加载的过程就是网页相关资源的加载过程。

# 13、HTTP 响应实现网页引入外部资源

TIP

table.html 中的 CSS、JS 单独提取出来,项目目录结构如下

icoding-http
└─ src
   ├─ table
   │  ├─ response-table.js
   │  ├─ table.css
   │  ├─ table.html
   │  └─ table.js

/src/table.html

<!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>table 表格</title>
    <link rel="stylesheet" href="./table.css" />
  </head>
  <body>
    <table>
      <thead>
        <tr>
          <th>专业</th>
          <th>姓名</th>
          <th>性别</th>
          <th>年龄</th>
          <th>所在城市</th>
        </tr>
      </thead>

      <tbody class="box">
        <tr>
          <td>计算机</td>
          <td>arry</td>
          <td></td>
          <td>18</td>
          <td>北京</td>
        </tr>
        <tr>
          <td>外语</td>
          <td>豆豆</td>
          <td></td>
          <td>21</td>
          <td>上海</td>
        </tr>
        <tr>
          <td>市场营销</td>
          <td>翠花</td>
          <td>19</td>
          <td></td>
          <td>深圳</td>
        </tr>
        <tr>
          <td>软件工程</td>
          <td>光头强</td>
          <td>21</td>
          <td></td>
          <td>广州</td>
        </tr>
      </tbody>

      <tfoot>
        <tr>
          <th>备注</th>
          <td colspan="4"></td>
        </tr>
      </tfoot>
    </table>

    <script src="./table.js"></script>
  </body>
</html>

/src/table.css

table {
  border-collapse: collapse;
  width: 80%;
  margin: 50px auto;
}
table tr th,
tbody tr td,
tfoot tr td {
  text-align: center;
  line-height: 40px;
  border: 1px solid #000;
}
/* nth-child(2n) */
table tbody tr:nth-child(even) {
  background-color: azure;
}
/* nth-child(2n + 1) */
table tbody tr:nth-child(odd) {
  background-color: beige;
}

/src/table.js

// 获取所有的 td
let tds = document.querySelectorAll(".box td");
let sel;
// 遍历每一个 td
tds.forEach((item) => {
  item.onclick = function (e) {
    // 阻止事件冒泡
    e.stopPropagation();
    if (sel) {
      sel.style.background = "";
    }
    this.style.background = "coral";
    sel = this;
  };
});
// 点击
document.body.onclick = function () {
  sel.style.background = "";
};

/src/response-table.js

// 导入 http 模块
const http = require("http");
const fs = require("fs");

// 创建服务对象
const server = http.createServer((request, response) => {
  // 读取 table.html 文件内容
  const html = fs.readFileSync(__dirname + "/table.html");

  response.end(html); // 设置响应体
});

// 监听端口,启动服务
server.listen(3000, () => {
  console.log(
    "服务已启动,http 请求已经被监听,3000 端口,请访问 http://localhost:3000"
  );
});

启动服务,在浏览器的地址栏中输入 http://localhost:3000 回车查看效果,发现并没有 CSS 样式 和 JS 效果

GIF-2024-1-15-0-09-37

注:

当我们将 table.html 中的 CSS、JS 单独拆分出来后,样式 和 JS 效果都没了,原因是当 <link><script> 标签被解析时,向服务器发请求,一旦请求到服务端就会给响应 HTML(以上代码中的 response.end(html)),不论什么请求(以上的 CSS 和 JS 、包括 favicon.ico 图标的请求)到达端口 3000 后,它的返回结果都是 HTML。

因此,主要的问题就在于无论任何请求,响应的结果都是 HTML。要想解决这个问题,就需要调整回调函数中的代码,即:根据请求的 URL 路径来返回 和 响应对应的结果。

/src/response-table.js

// 导入 http 模块
const http = require("http");
const fs = require("fs");

// 创建服务对象
const server = http.createServer((request, response) => {
  // 获取请求 URL 的路径
  let { pathname } = new URL(request.url, "http://127.0.0.1");

  // 根据请求的 URL 路径来响应对应的结果

  if (pathname === "/") {
    // 读取 table.html 文件内容
    const html = fs.readFileSync(__dirname + "/table.html");
    response.end(html); // 设置响应体
  } else if (pathname === "/table.css") {
    // 读取 table.css 文件内容
    const css = fs.readFileSync(__dirname + "/table.css");
    response.end(css); // 设置响应体
  } else if (pathname === "/table.js") {
    // 读取 table.js 文件内容
    const js = fs.readFileSync(__dirname + "/table.js");
    response.end(js); // 设置响应体
  } else {
    response.statusCode = 404;
    response.end("<h1>404 Not Found</h1>");
  }
});

// 监听端口,启动服务
server.listen(3000, () => {
  console.log(
    "服务已启动,http 请求已经被监听,3000 端口,请访问 http://localhost:3000"
  );
});

启动服务,在浏览器的地址栏中输入 http://localhost:3000 回车查看效果,此时已经 OK

GIF-2024-1-18-2-20-01

注:

此时,每一个响应的结果就对应上了

  • localhost 对应 HTML 结构
  • table.css 对应 CSS 样式
  • table.js 对应 JS 代码

以上将每一个单独的资源拆分开后效果出来了,同时也存在问题,在资源很少的情况下 /src/response-table.js 中的可以,但资源多(项目中很多 CSS、JS)的情况下就会非常麻烦了(就需要写很多 if ... else ... 判断)。后边会讲对应解决方案来简化该操作。

# 14、静态资源 和 动态资源

TIP

  • 静态资源:内容长时间不发生改变的资源,例如图片、视频、CSS 文件、JS 文件、 HTML 文件、字体文件等(注:这里的静态资源是指项目在运行过程中的图片、视频、CSS 文件、JS 文件、 HTML 文件等 ... 而非在 项目开发中的这些资源)
  • 动态资源:内容经常更新的资源,例如天猫首页,京东搜索列表结果页面等

# 15、搭建静态资源服务

TIP

静态资源服务:可以很方便的给客户端浏览器响应静态资源(HTML、CSS、JS、图片、JSON 等)

以下应用中,将会解决以上案例中的痛点(即:为了响应 HTML、CSS、JS 做了很多 if ... else 判断,再复杂的还需要写更多的 if ... else

需求:创建一个 HTTP 服务,端口为 3000,满足如下需求

  • 浏览器发送 GET 请求 ,请求路径为 /index.html ,服务端响应 page/index.html 的文件内容
  • 浏览器发送 GET 请求,请求路径为 /css/index.css ,服务端响应 page/css/index.css 的文件内容
  • 浏览器发送 GET 请求,请求路径为 /images/banner.png,服务端响应 page/images/banner.png 的文件内容

项目目录结构

icoding-http
└─ src
   ├─ server
   │  ├─ page
   │  │  ├─ css
   │  │  │  └─ index.css
   │  │  ├─ images
   │  │  │  └─ banner.png
   │  │  ├─ index.html
   │  │  └─ js
   │  │     └─ index.js
   │  └─ static-services.js

/src/server/page/index.html

<!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>搭建静态资源服务</title>
  </head>
  <body>
    <h1>HTML 静态页面</h1>
  </body>
</html>

/src/server/page/css/index.css

html {
  color: #000;
  overflow-y: scroll;
  overflow: -moz-scrollbars;
}

/src/server/page/js/index.js

console.log("JS 输出的内容 !");

/src/server/static-services.js

// 导入 http 模块
const http = require("http");
const fs = require("fs");

// 创建服务对象
const server = http.createServer((request, response) => {
  // 获取请求 URL 的路径
  let { pathname } = new URL(request.url, "http://127.0.0.1");

  // 根据请求的 URL 路径来响应对应的结果

  if (pathname === "/index.html") {
    // 读取文件内容
    const html = fs.readFileSync(__dirname + "/page/index.html");
    response.end(html); // 设置响应体
  } else if (pathname === "/css/index.css") {
    // 读取文件内容
    const css = fs.readFileSync(__dirname + "/page/css/index.css");
    response.end(css); // 设置响应体
  } else if (pathname === "/images/banner.png") {
    // 读取文件内容
    const images = fs.readFileSync(__dirname + "/page/images/banner.png");
    response.end(images); // 设置响应体
  } else {
    response.statusCode = 404;
    response.end("<h1>404 Not Found</h1>");
  }
});

// 监听端口,启动服务
server.listen(3000, () => {
  console.log(
    "服务已启动,http 请求已经被监听,3000 端口,请访问 http://localhost:3000"
  );
});

启动服务,在浏览器的地址栏中输入对应的请求地址,回车查看效果

GIF-2024-1-20-18-18-07

以上效果已经实现了,但有一个问题 !以上项目中有一个 /src/server/page/js/index.js 如果直接给服务端发请求路径为 http://localhost:3000/js/index.js 能否响应 index.js 中的内容 ?

image-20240120182728014

注:

显然不能获取 index.js 中的内容,原因是并没有对应 JS 的 if ... else判断 ,但我们又需要得到 JS 文件中的效果,如果再使用 if ... else 判断就重蹈覆辙了,并没有意义。有没有简化操作的方法呢 ?

# 16、搭建静态资源服务 - 优化

TIP

观察 URL 的请求 和 最终响应的 文件路径是有关联的(即:找规律)将相同的路径部分提取出来

/src/server/static-services.js

// 导入 http 模块
const http = require("http");
const fs = require("fs");

// 创建服务对象
const server = http.createServer((request, response) => {
  // 获取请求 URL 的路径
  let { pathname } = new URL(request.url, "http://127.0.0.1");

  // 拼接文件路径(提取相同的路径部分)
  let filePath = __dirname + "/page" + pathname;
  // 读取文件(使用 fs 异步 API)
  fs.readFile(filePath, (err, data) => {
    if (err) {
      response.statusCode = 500;
      response.end("文件读取失败 !");
      return;
    }
    // 响应文件内容
    response.end(data);
  });

  // 根据请求的 URL 路径来响应对应的结果

  // if (pathname === '/index.html') {
  //     // 读取文件内容
  //     const html = fs.readFileSync(__dirname + '/page/index.html')
  //     response.end(html) // 设置响应体
  // } else if (pathname === '/css/index.css') {
  //     // 读取文件内容
  //     const css = fs.readFileSync(__dirname + '/page/css/index.css')
  //     response.end(css) // 设置响应体
  // } else if (pathname === '/images/banner.png') {
  //     // 读取文件内容
  //     const images = fs.readFileSync(__dirname + '/page/images/banner.png')
  //     response.end(images) // 设置响应体
  // } else {
  //     response.statusCode = 404
  //     response.end('<h1>404 Not Found</h1>')
  // }
});

// 监听端口,启动服务
server.listen(3000, () => {
  console.log(
    "服务已启动,http 请求已经被监听,3000 端口,请访问 http://localhost:3000"
  );
});

启动服务,在浏览器的地址栏中输入对应的请求地址,回车查看效果,即便再往 js 文件夹中新增文件也能同样访问(乱码问题后边再解决)

GIF-2024-1-20-23-23-00

# 17、设置 MIME 类型

TIP

通过设置 MIME 类型来完善以上的静态资源服务

媒体类型:Multipurpose Internet Mail Extensions(直译:多用途的邮件扩展,一般不会用直译)会称之为 “媒体类型”、“资源类型” 或 MIME 类型

它是一种标准,用来表示文档、文件 或 字节流的性质和格式。

mime 类型结构:[type] / [subType]  即:[主类型] / [子类型]

如:text/html  text/css  image/jpeg  image/png  application/json

HTTP 服务可以设置响应头 Content-Type 来表明响应体的 MIME 类型,浏览器会根据该类型决定如何处理资源。如下是常见文件对应的 MIME 类型

html: 'text/html',
css: 'text/css',
js: 'text/javascript',
png: 'image/png',
jpg: 'image/jpeg',
gif: 'image/gif',
mp4: 'video/mp4',
mp3: 'audio/mpeg',
json: 'application/json'

对未知的资源类型,可以选择 application/octet-stream 类型,浏览器在遇到该类型响应时,会对响应体内容进行独立存储,也就是我们常见的 下载 效果

/src/server/static-services.js

// 导入 http 模块
const http = require("http");
const fs = require("fs");
const path = require("path");

// 在实际项目开发中不会这么做,会有单独的模块来实现 MIME 类型的设置
// 声明一个变量,来保存自定义的 MIME 类型
let mimes = {
  html: "text/html",
  css: "text/css",
  js: "text/javascript",
  png: "image/png",
  jpg: "image/jpeg",
  gif: "image/gif",
  mp4: "video/mp4",
  mp3: "audio/mpeg",
  json: "application/json",
};

// 创建服务对象
const server = http.createServer((request, response) => {
  // 获取请求 URL 的路径
  let { pathname } = new URL(request.url, "http://127.0.0.1");

  // 拼接文件路径
  let filePath = __dirname + "/page" + pathname;
  // 读取文件(使用 fs 异步 API)
  fs.readFile(filePath, (err, data) => {
    if (err) {
      response.statusCode = 500;
      response.end("文件读取失败 !");
      return;
    }

    // 2、获取文件的后缀名
    // let ext = path.extname(filePath) // .html .css .js .png ......
    let ext = path.extname(filePath).slice(1); // 截取文件后缀名中的 点 .
    console.log(ext); // html css js png ......

    // 3、获取后缀对应的类型
    let type = mimes[ext];
    // 有可能获取到的类型不存在,需要判断
    if (type) {
      // 存在(匹配到了)
      response.setHeader("content-type", type);
    } else {
      // 不存在(没有匹配到)
      response.setHeader("content-type", "application/octet-stream");
    }

    // ------------------------
    // 1、在读取文件内容后,设置响应头
    // 当前位置的响应头显然不能写死,因为不确定它响应的是 HTML、CSS 还是 JS
    // 因此,我们可以根据请求文件的后缀来决定 MIME 类型
    // response.setHeader('content-type', '')

    // 响应文件内容
    response.end(data);
  });
});

// 监听端口,启动服务
server.listen(3000, () => {
  console.log(
    "服务已启动,http 请求已经被监听,3000 端口,请访问 http://localhost:3000"
  );
});

启动服务,在浏览器的地址栏中输入对应的请求地址,回车查看效果,在 NetWork 中查看响应头信息 content-type 的值

GIF-2024-1-22-12-03-11

注:

你会发现以上 MIME 类型设置部分的代码,添加 和 不添加 貌似对最终结果显示并没有任何影响,但添加上会更为规范。

不添加也能正常显示的原因是:浏览器有一个能力它会根据响应回来的内容来判定该资源的类型,因此不添加类型也是可以的。但添加上更为规范 !

# 18、乱码问题

TIP

在添加完 MIME 类型后,会出现中文乱码问题(在 CSS、JS 出现中文都会乱码)

解决方法:只需要在设置 MIME 类型后边,添加上 urf-8 的字符集即可

GIF-2024-1-22-12-19-42

/src/server/static-services.js 中,添加字符集编码

const http = require("http");
const fs = require("fs");
const path = require("path");

let mimes = {
  html: "text/html",
  css: "text/css",
  js: "text/javascript",
  png: "image/png",
  jpg: "image/jpeg",
  gif: "image/gif",
  mp4: "video/mp4",
  mp3: "audio/mpeg",
  json: "application/json",
};

const server = http.createServer((request, response) => {
  let { pathname } = new URL(request.url, "http://127.0.0.1");

  let filePath = __dirname + "/page" + pathname;
  fs.readFile(filePath, (err, data) => {
    if (err) {
      response.statusCode = 500;
      response.end("文件读取失败 !");
      return;
    }

    let ext = path.extname(filePath).slice(1);
    console.log(ext);

    let type = mimes[ext];
    if (type) {
      // 在设置 MIME 类型后边,添加上 urf-8 的字符集即可
      response.setHeader("content-type", type + ";charset=utf-8");
    } else {
      response.setHeader("content-type", "application/octet-stream");
    }
    response.end(data);
  });
});

server.listen(3000, () => {
  console.log(
    "服务已启动,http 请求已经被监听,3000 端口,请访问 http://localhost:3000"
  );
});

启动服务,在浏览器的地址栏中输入对应的请求地址,回车查看效果

GIF-2024-1-22-12-24-25

注:

添加完 utf-8 字符集后,CSS 和 JS 里边的中文乱码问题已解决,如果在不设置响应头中的编码集时,CSS 和 JS 中出现中文会乱码,但 /index.html 中的中文是不会乱码的

原因是:在 HTML 文件中有一个 <meta charset="UTF-8" /> 标签,已经设置了 HTML 网页的字符集,因此 HTML 文件不会乱码。

由此,我们可以看出设置网页字符集的方式有两种:一种是 设置响应头,一种是 <meta charset="UTF-8" /> 标签。那么他们的优先级谁更高呢 ?

# 18.1、测试优先级

TIP

调换以上代码 <meta charset="UTF-8" /> 标签 和 response.setHeader('content-type', type + ';charset=utf-8')charset 值,将其设置为 gbkutf-8 互换,测试

由此,可得出结论 设置为响应头 response.setHeader('content-type', type + ';charset=utf-8') 中的字符集优先级更高

在绝大多数情况下,两者会保持统一(两者的值一致),不会搞差异化

# 18.2、网页外部资源的字符集

TIP

对于网页外部资源(CSS、JS、图片)在设置响应时,是没有必要设置字符集的。这些资源在进入到网页中后,都会根据网页的字符集来对响应结果做处理

GIF-2024-1-22-15-31-32

注:

以上京东 www.jd.com 请求中 content-type: text/html;charset=utf-8 是有字符集的,在 CSS、JS 中没有字符集。

而这些资源在进入到网页中后,都是按照网页的字符集 即:utf-8 的字符集来对最终的结果做处理,因此不会产生问题。

当然,在没有设置字符集的 CSS、JS 中有中文出现时也会乱码,即便是乱码也不会影响在网页中的执行。因为这些资源在回到网页中后它会以网页的字符集来解析,这样就不会出现问题。

做到跟京东一模一样的字符集设置,在 /src/server/static-services.js 中进行判断

const http = require("http");
const fs = require("fs");
const path = require("path");

let mimes = {
  html: "text/html",
  css: "text/css",
  js: "text/javascript",
  png: "image/png",
  jpg: "image/jpeg",
  gif: "image/gif",
  mp4: "video/mp4",
  mp3: "audio/mpeg",
  json: "application/json",
};

const server = http.createServer((request, response) => {
  let { pathname } = new URL(request.url, "http://127.0.0.1");

  let filePath = __dirname + "/page" + pathname;
  fs.readFile(filePath, (err, data) => {
    if (err) {
      response.statusCode = 500;
      response.end("文件读取失败 !");
      return;
    }

    let ext = path.extname(filePath).slice(1);
    console.log(ext);

    let type = mimes[ext];
    if (type) {
      // ---------------------------
      // 判断文件是否是 HTML 文件(添加 utf-8 字符集) 达到京东的效果
      if (ext === "html") {
        response.setHeader("content-type", type + ";charset=utf-8");
      } else {
        // 如果不是 HTML,就不添加 字符集
        response.setHeader("content-type", type);
      }
    } else {
      response.setHeader("content-type", "application/octet-stream");
    }
    response.end(data);
  });
});

server.listen(3000, () => {
  console.log(
    "服务已启动,http 请求已经被监听,3000 端口,请访问 http://localhost:3000"
  );
});

# 19、静态资源错误处理

TIP

以上我们都是将错误处理统一为 500 错误,即 服务器内部错误。其实并不合适 !我们应该不同的错误来返回不同的错误编号 和 错误提示

/src/server/static-services.js 中,先打印输出 err

const http = require("http");
const fs = require("fs");
const path = require("path");

let mimes = {
  html: "text/html",
  css: "text/css",
  js: "text/javascript",
  png: "image/png",
  jpg: "image/jpeg",
  gif: "image/gif",
  mp4: "video/mp4",
  mp3: "audio/mpeg",
  json: "application/json",
};

const server = http.createServer((request, response) => {
  let { pathname } = new URL(request.url, "http://127.0.0.1");

  let filePath = __dirname + "/page" + pathname;
  fs.readFile(filePath, (err, data) => {
    if (err) {
      // ---------------
      // 为了清晰看出错误信息,先打印输出 err
      console.log(err);
      response.statusCode = 500;
      response.end("文件读取失败 !");
      return;
    }

    let ext = path.extname(filePath).slice(1);
    console.log(ext);

    let type = mimes[ext];
    if (type) {
      // 判断文件是否是 HTML 文件(添加 utf-8 字符集) 达到京东的效果
      if (ext === "html") {
        response.setHeader("content-type", type + ";charset=utf-8");
      } else {
        // 如果不是 HTML,就不添加 字符集
        response.setHeader("content-type", type);
      }
    } else {
      response.setHeader("content-type", "application/octet-stream");
    }
    response.end(data);
  });
});

server.listen(3000, () => {
  console.log(
    "服务已启动,http 请求已经被监听,3000 端口,请访问 http://localhost:3000"
  );
});

启动服务,在浏览器的地址栏中输入一个不存在的请求地址,回车查看效果

image-20240123224706011

在 VSCode 控制台中,查看错误信息

image-20240123225032599

报错信息 Error: ENOENT: no such file or directory 找不到文件 或 文件夹,该错误信息是一个对象

{
  // 错误编号
  "errno": -4058,
  // 错误代码
  "code": "ENOENT",
  // 系统调用:打开文件时出错
  "syscall": "open",
  // 文件路径:寻找该文件时报错
  "path": "D:\\web\\node\\icoding-http\\src\\server\\page\\111.html"
}

以上报错即:文件找不到的错误 !服务端应该给客户端返回一个 404 的响应

/src/server/static-services.js 中 err 部分做判断(根据 err.code 的值判断 给出对应的响应)

const http = require("http");
const fs = require("fs");
const path = require("path");

let mimes = {
  html: "text/html",
  css: "text/css",
  js: "text/javascript",
  png: "image/png",
  jpg: "image/jpeg",
  gif: "image/gif",
  mp4: "video/mp4",
  mp3: "audio/mpeg",
  json: "application/json",
};

const server = http.createServer((request, response) => {
  let { pathname } = new URL(request.url, "http://127.0.0.1");

  let filePath = __dirname + "/page" + pathname;
  fs.readFile(filePath, (err, data) => {
    if (err) {
      // ---------------
      // 统一设置字符集
      response.setHeader("content-type", "text/html;charset=utf-8");

      // 根据 err.code 的值判断 给出对应的响应
      // 判断错误的代号
      switch (err.code) {
        case "ENOENT":
          response.statusCode = 404;
          response.end("<h1>404 Not Found</h1>");
      }
      return;
    }

    let ext = path.extname(filePath).slice(1);
    console.log(ext);

    let type = mimes[ext];
    if (type) {
      // 判断文件是否是 HTML 文件(添加 utf-8 字符集) 达到京东的效果
      if (ext === "html") {
        response.setHeader("content-type", type + ";charset=utf-8");
      } else {
        // 如果不是 HTML,就不添加 字符集
        response.setHeader("content-type", type);
      }
    } else {
      response.setHeader("content-type", "application/octet-stream");
    }
    response.end(data);
  });
});

server.listen(3000, () => {
  console.log(
    "服务已启动,http 请求已经被监听,3000 端口,请访问 http://localhost:3000"
  );
});

启动服务,在浏览器的地址栏中输入一个不存在的请求地址,回车查看效果

image-20240123230352650

# 19.1、权限错误 - 403

/src/server/static-services.js 中打印输出 err

const http = require("http");
const fs = require("fs");
const path = require("path");

let mimes = {
  html: "text/html",
  css: "text/css",
  js: "text/javascript",
  png: "image/png",
  jpg: "image/jpeg",
  gif: "image/gif",
  mp4: "video/mp4",
  mp3: "audio/mpeg",
  json: "application/json",
};

const server = http.createServer((request, response) => {
  let { pathname } = new URL(request.url, "http://127.0.0.1");

  let filePath = __dirname + "/page" + pathname;
  fs.readFile(filePath, (err, data) => {
    if (err) {
      // 打印输出 err
      console.log(err);
      // ---------------
      // 统一设置字符集
      response.setHeader("content-type", "text/html;charset=utf-8");

      // 根据 err.code 的值判断 给出对应的响应
      // 判断错误的代号
      switch (err.code) {
        case "ENOENT":
          response.statusCode = 404;
          response.end("<h1>404 Not Found</h1>");
      }
      return;
    }

    let ext = path.extname(filePath).slice(1);
    console.log(ext);

    let type = mimes[ext];
    if (type) {
      // 判断文件是否是 HTML 文件(添加 utf-8 字符集) 达到京东的效果
      if (ext === "html") {
        response.setHeader("content-type", type + ";charset=utf-8");
      } else {
        // 如果不是 HTML,就不添加 字符集
        response.setHeader("content-type", type);
      }
    } else {
      response.setHeader("content-type", "application/octet-stream");
    }
    response.end(data);
  });
});

server.listen(3000, () => {
  console.log(
    "服务已启动,http 请求已经被监听,3000 端口,请访问 http://localhost:3000"
  );
});

修改 /src/server/page/index.html 文件的权限,选择 拒绝 “读取 和 执行”

image-20240124192841044

启动服务,在浏览器的地址栏中输入请求地址 http://localhost:3000/index.html ,回车查看效果

image-20240124194945624

在 VSCode 控制台中查看错误信息 operation not permitted 即:不允许操作

image-20240124195258035

查看错误信息,可在 NodeJS 官方文档 - 常见系统错误 (opens new window) 搜索 "错误代码“:EPERM 可看到

image-20240124200426537

/src/server/static-services.js 中打印响应该错误信息

const http = require("http");
const fs = require("fs");
const path = require("path");

let mimes = {
  html: "text/html",
  css: "text/css",
  js: "text/javascript",
  png: "image/png",
  jpg: "image/jpeg",
  gif: "image/gif",
  mp4: "video/mp4",
  mp3: "audio/mpeg",
  json: "application/json",
};

const server = http.createServer((request, response) => {
  let { pathname } = new URL(request.url, "http://127.0.0.1");

  let filePath = __dirname + "/page" + pathname;
  fs.readFile(filePath, (err, data) => {
    if (err) {
      console.log(err);
      // ---------------
      // 统一设置字符集
      response.setHeader("content-type", "text/html;charset=utf-8");

      // 根据 err.code 的值判断 给出对应的响应
      // 判断错误的代号
      switch (err.code) {
        case "ENOENT":
          response.statusCode = 404;
          response.end("<h1>404 Not Found</h1>");
        // ------------------------
        // 响应权限错误信息
        case "EPERM":
          response.statusCode = 403;
          response.end("<h1>403 Forbidden 被禁止的 !</h1>");
      }
      return;
    }

    let ext = path.extname(filePath).slice(1);
    console.log(ext);

    let type = mimes[ext];
    if (type) {
      // 判断文件是否是 HTML 文件(添加 utf-8 字符集) 达到京东的效果
      if (ext === "html") {
        response.setHeader("content-type", type + ";charset=utf-8");
      } else {
        // 如果不是 HTML,就不添加 字符集
        response.setHeader("content-type", type);
      }
    } else {
      response.setHeader("content-type", "application/octet-stream");
    }
    response.end(data);
  });
});

server.listen(3000, () => {
  console.log(
    "服务已启动,http 请求已经被监听,3000 端口,请访问 http://localhost:3000"
  );
});

启动服务,在浏览器的地址栏中输入请求地址 http://localhost:3000/index.html ,回车查看效果

image-20240124224241992

此时,再次恢复 /src/server/page/index.html 文件的权限,去掉 拒绝 “读取 和 执行” 的选项,在浏览器的地址栏中输入请求地址 http://localhost:3000/index.html ,回车查看效果

image-20240124224541563

# 19.2、请求方式错误

TIP

以上创建服务的请求方式必须是 GET 请求,不能用 POST 发送请求获取资源,即:如果请求方式错误也需要返回一个错误信息。

因此,需要对请求方式做判断 !

/src/server/static-services.js 中判断请求方式

const http = require("http");
const fs = require("fs");
const path = require("path");

let mimes = {
  html: "text/html",
  css: "text/css",
  js: "text/javascript",
  png: "image/png",
  jpg: "image/jpeg",
  gif: "image/gif",
  mp4: "video/mp4",
  mp3: "audio/mpeg",
  json: "application/json",
};

const server = http.createServer((request, response) => {
  // ------------------------------
  // 判断请求方式
  if (request.method !== "GET") {
    response.statusCode = 405;
    response.end("<h1>405 Method Not Allowed</h1>"); // 不允许的方法 !
    return;
  }

  let { pathname } = new URL(request.url, "http://127.0.0.1");

  let filePath = __dirname + "/page" + pathname;
  fs.readFile(filePath, (err, data) => {
    if (err) {
      console.log(err);

      // 统一设置字符集
      response.setHeader("content-type", "text/html;charset=utf-8");

      // 根据 err.code 的值判断 给出对应的响应
      // 判断错误的代号
      switch (err.code) {
        case "ENOENT":
          response.statusCode = 404;
          response.end("<h1>404 Not Found</h1>");
        // 响应权限错误信息
        case "EPERM":
          response.statusCode = 403;
          response.end("<h1>403 Forbidden 被禁止的 !</h1>");
      }
      return;
    }

    let ext = path.extname(filePath).slice(1);
    console.log(ext);

    let type = mimes[ext];
    if (type) {
      // 判断文件是否是 HTML 文件(添加 utf-8 字符集) 达到京东的效果
      if (ext === "html") {
        response.setHeader("content-type", type + ";charset=utf-8");
      } else {
        // 如果不是 HTML,就不添加 字符集
        response.setHeader("content-type", type);
      }
    } else {
      response.setHeader("content-type", "application/octet-stream");
    }
    response.end(data);
  });
});

server.listen(3000, () => {
  console.log(
    "服务已启动,http 请求已经被监听,3000 端口,请访问 http://localhost:3000"
  );
});

启动服务,在 /src/server/page/index.html 表单中发送 POST 请求,进行测试

<!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>查看请求体内容</title>
  </head>
  <body>
    <!-- 发送 POST 请求至 http://localhost:3000/index.html -->
    <form action="http://localhost:3000/index.html" method="post">
      用户名:<input type="text" name="username" /> <br /><br />&nbsp;&nbsp;&nbsp;码:<input type="password" name="password" />
      <br /><br />
      <input type="submit" value="登 录" />
    </form>
  </body>
</html>

在 VSCode 中通过 Open with Live Server 打开 http://127.0.0.1:5500/src/index.html 并发送 POST 请求测试,查看状态码 和 响应内容

GIF-2024-1-25-19-38-14

# 19.3、其他未知错误

TIP

将其他未知错误归为 default 中即可

/src/server/static-services.js

const http = require("http");
const fs = require("fs");
const path = require("path");

let mimes = {
  html: "text/html",
  css: "text/css",
  js: "text/javascript",
  png: "image/png",
  jpg: "image/jpeg",
  gif: "image/gif",
  mp4: "video/mp4",
  mp3: "audio/mpeg",
  json: "application/json",
};

const server = http.createServer((request, response) => {
  // 判断请求方式
  if (request.method !== "GET") {
    response.statusCode = 405;
    response.end("<h1>405 Method Not Allowed</h1>"); // 不允许的方法 !
    return;
  }

  let { pathname } = new URL(request.url, "http://127.0.0.1");

  let filePath = __dirname + "/page" + pathname;
  fs.readFile(filePath, (err, data) => {
    if (err) {
      console.log(err);

      // 统一设置字符集
      response.setHeader("content-type", "text/html;charset=utf-8");

      // 根据 err.code 的值判断 给出对应的响应
      // 判断错误的代号
      switch (err.code) {
        case "ENOENT":
          response.statusCode = 404;
          response.end("<h1>404 Not Found</h1>");
        // 响应权限错误信息
        case "EPERM":
          response.statusCode = 403;
          response.end("<h1>403 Forbidden 被禁止的 !</h1>");
        // ---------------
        // 其他未知错误
        default:
          response.statusCode = 500;
          response.end("<h1>500 Internal Server Error !</h1>"); // 服务器内部错误
      }
      return;
    }

    let ext = path.extname(filePath).slice(1);
    console.log(ext);

    let type = mimes[ext];
    if (type) {
      // 判断文件是否是 HTML 文件(添加 utf-8 字符集) 达到京东的效果
      if (ext === "html") {
        response.setHeader("content-type", type + ";charset=utf-8");
      } else {
        // 如果不是 HTML,就不添加 字符集
        response.setHeader("content-type", type);
      }
    } else {
      response.setHeader("content-type", "application/octet-stream");
    }
    response.end(data);
  });
});

server.listen(3000, () => {
  console.log(
    "服务已启动,http 请求已经被监听,3000 端口,请访问 http://localhost:3000"
  );
});

# 20、GET 和 POST 请求的应用场景

GET 请求

GET 方法通常用于数据检索,即获取资源的请求。适用于无副作用的操作,例如展示网页内容、读取数据、搜索等。它常用于浏览器 URL 的地址栏中。

  • 在地址栏直接输入 url 访问
  • 点击 a 链接
  • link 标签引入 css
  • script 标签引入 js
  • video 与 audio 引入多媒体
  • img 标签引入图片
  • form 标签中的 method 为 get(不区分大小写)
  • ajax 中的 get 请求

POST 请求

POST 方法通常用于数据提交,即向服务器发送数据。适用于有副作用的操作,例如提交表单、处理用户注册、发送邮件等。它常用于表单的提交。

  • form 标签中的 method 为 post (不区分大小写)
  • Ajax 的 POST 请求

# 21、GET 和 POST 请求区别

TIP

GET 和 POST 是 HTTP 协议请求的两种方式,主要区别如下

  • 作用: GET 主要用来获取数据,POST 主要用来提交数据
  • 数据传输位置: GET 带参数请求是将参数缀到 URL 之后,POST 带参数请求是将参数放到请求体中。GET 方法将请求参数附加在 URL 的查询字符串中,可见于请求 URL 中;而 POST 方法将请求参数作为 HTTP 消息的实体部分发送给服务器,不可见于请求 URL。
  • 安全性: POST 请求相对 GET 安全一些,因为在浏览器中参数会暴露在地址栏。GET 方法的参数通过 URL 传输,容易被拦截和嗅探;而 POST 方法将参数放在请求体中,相对安全,但仍需使用 HTTPS 等方式进行加密保护。
  • 数据传输限制: 由于 URL 长度限制或浏览器的限制,GET 方法对数据传输的长度有限制(一般为 2K);而 POST 方法对数据传输的长度没有严格限制(没有大小限制)。
  • 对数据的操作: GET 方法用于获取资源,不应产生副作用;而 POST 方法用于提交数据,可能对服务器和资源产生影响,如存储、更新或删除数据。
  • 传送方式: GET 通过地址栏传输,产生一个 TCP 数据包;而 POST 通过报文传输,产生两个数据包。
  • 执行效率: GET 安全性低,但执行效率高;而 POST 安全性较高,但执行效率比 GET 方法稍低。
  • 数据格式: 使用 GET 方式提交的数据会显示在地址栏上,因此一般对数据的格式有所限制(例如只接受 ASCII 字符)。而 POST 方式提交的数据不会显示在地址栏上,因此对数据的格式没有特别限制。
上次更新时间: 2/2/2024, 11:35:57 PM

大厂最新技术学习分享群

大厂最新技术学习分享群

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

X