七、http 全面攻略
1、前言
小病一场,问题不大;收拾心情,继续出发~~~
一直都说 Node 是一门服务端语言,现在终于到了打 boss 的时候,全面攻略 Node.js 的 http 模块的时候到了~
本节的内容会涵盖两个部分
- http 模块相关 api 的使用,如 http 常用方法、response、request 等
- http 模块的使用,如调用第三方 api、实现接口、接口跨域等
2、http 常用方法
http.request
使用语法:http.request(url[, options][, callback])
说明:用来发起 http 请求;Node.js 内部不存在跨域的改变,这也就意味着我们可以利用 http.request
来无障碍请求其他接口,如微信支付回调、第三方开源 API 接口
http.get
使用基本与 http.request
一致,只是在其基础上设置方法为 GET 并自动调用 req.end()
http.createServer
使用语法: http.createServer([options][, requestListener])
说明:用来创建一个服务 server,监听端口就后就会立刻启动 HTTP 服务器;requestListener
是自动添加到 server 的 request 事件的函数。
const http = require('http');
const server = http.createServer(() => {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
data: 'Hello World!',
}));
}).listen(3003);
我们一般会看到这样的启动服务及处理请求的代码示例;它的实质是对 server 的操作,等价于以下代码
const http = require('http');
const server = http.createServer();
server.on('request', (req, res) => {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
data: 'Hello World!',
}));
});
server.listen(3003, () => {
console.log(`服务启动成功: http://localhost:3003`)
});
这个 request 事件函数回调有两个参数,request
和 response
- listen 监听还可以接受一个成功回调,由于服务的启动没有提示,可以给出打印
- 此时就可以打开 http://localhost:3003 页面,如果没有结束响应 res.end(),页面将一直加载
- 可以在 end 中返回数据,也可以通过 res.write 多次写入数据
3、req 请求信息
客户端向 node 服务发起请求,会携带许多有用的信息
- req.url:请求地址及请求参数
- req.method: 请求类型,GET、POST 等等
- req.headers: 请求头信息
注意,server 的 request 事件回调参数的 req 并不是 node 官网的 http.ClientRequest
类的实例,http.ClientRequest
是在 http.request
内部创建处理请求的类;而这里的 req 则是客户端的请求对象
我通过 console.log 打印上述 req.url、req.method、req.headers 来看看这究竟是个什么东西?不出意外的话,所有内容都打印了两遍。这是因为每一次请求都会默认发起一次 favicon 的请求
3.1、处理 favicon 请求
favicon 是啥?就是每个网站左上角的那个小 logo;
对于这个请求,我们可以返回一张本地图片的内容即可,结合 fs 模块核心代码如下
if (req.url === '/favicon.ico') {
// 返回一个本地图片作为 favicon
let favicon = await fs.readFile('../static/logo.png');
res.setHeader('Content-Type', 'image/png');
res.end(favicon)
return;
}
注意设置一个 Content-Type 为图片即可,并无难点;但问题是每个请求都会默认发起一次这个 favicon 的请求,岂不是很浪费,即使有缓存也抗不住,怎么办呢?
所有网站,我们查看 Network 都只会发起一次 favicon 的请求,他是如何实现的?很简单,HTML 文档的标签中添加一个标签来引用 favicon.ico,并将 rel 属性设置为 "shortcut icon"。这样,浏览器在加载页面时就会自动请求并缓存 favicon.ico,该网站发送请求时就不会重复请求 /favicon.ico 了
3.2、请求头
req.headers 包含了客户端发起请求的一些信息,重要的有
- user-agent,客户端代理信息,与浏览器中 navigate 拿到 UA 信息一致,包含平台类型,浏览器版本等信息
- accept,代表客户端允许接收的格式;比如这里就包含了 image/webp;表示浏览器是支持 webp 格式,这是我们对请求的图片就可以返回 webp 格式,不支持则返回 png 格式
- accept-encoding,客户端支持的编码格式,这里支持 gzip、deflate、br;也就表示我们可以对响应内容使用 zlib 压缩
3.3、处理请求地址及参数
比如我们发起一个请求 http://localhost:3008/user?id=1
携带参数过来,根据参数做不同的逻辑处理;处理参数的方式有许多
- 正则匹配
- 字符串分割
querystring
模块url
模块
这里我们只介绍 node 的内置模块处理方式,也是我们在 node 中推荐的形式
querystring 处理
说明:当对性能要求较高,可以使用 querystring
模块处理参数;但它不属于标准 API,兼容性较差
const querystring = require('querystring')
let reqUrl = '/api/user?id=1'
let paramsStr = reqUrl.split('?')[1];
console.log(querystring.parse(qs).id); // 1
处理的是请求参数部分,即 ? 后面的那些字符串;通过 parse 方法转化为对象
url 模块(推荐)
与浏览器中的 URL、URLSearchParams 功能基本一致,兼容性高
const { URL } = require('url');
let reqUrl = '/api/user?id=1';
reqUrl = 'http://localhost:3003/api/user?id=1';
console.log(new URL(reqUrl))
console.log(new URL(reqUrl).searchParams.get('id'))
new URL 接收一个完整的 url 作为参数;new URLSearchParams 则接受请求的 search 部分作为参数
URL 对象内部有一个 searchParams 属性,返回一个 URLSearchParams 对象
4、response
在 server 的 request 回调函数中,第二个参数是我们的请求响应对象,一般为 response 或 res;遵照上述代码,我们将使用 res
4.1、响应状态
我们给客户端返回不同的状态码来表示请求的不同状态
- [推荐]通过
res.statuCode = 400;
的形式设置响应状态 - 通过
res.writeHead(200);
的形式设置响应状态
res.statuCode = 400;
理解响应状态,能很好的帮助我们进行接口处理,常见的状态码有
状态码 | 描述 | 说明 | 场景 |
---|---|---|---|
101 | Switch Protocol | 服务器正在根据客户端的指定,将协议切换成 Update 首部所列的协议 | 一般用在 ws 连接,告知服务器由 http 协议切换到 WebSocket |
200 | OK | 请求正常接收并响应 | 日常接口响应成功 |
201 | Created | 服务器创建资源成功 | 一般用于 POST 请求创建资源,成功后返回 201 并在响应头中包含 Location 字段,该字段指示新资源的 URL |
202 | Accepted | 请求正常接收,但未处理完成 | 一般用于大数据导出等消耗时间的请求,告知客户端请求还在处理,可以通过轮询的方式获取最终结果 |
204 | No Content | 请求正常接收,但没有返回内容 | 如数据更新、删除;服务器并不需要返回任何内容 |
206 | Partial Content | 服务器已经成功处理了部分请求,返回了部分响应数据 | 常见于分段下载,客户端通过 Range 头字段指定了只请求文件的一部分内容(如某个字节范围)时,服务器可以返回 206 状态码,并返回请求的部分数据 |
301 | Move Permanently | 永久性重定向 | 响应头中包含 Location 字段,并指定新的 URL 地址,客户端在接收到响应后会自动重新发送 GET 请求到新地址(即使原来是 POST);如资源重定向、域名重定向、http 自动跳转 https |
302 | Found | 临时重定向 | 与 301 基本相同(Location 包含临时 URL,自动重新请求临时地址),不同是客户端会记住原始 URL,并在后续请求中继续原始 URL;302 在 AB 测试,网站维护临时页等场景下非常常见 |
304 | Not Modify | 资源未改变,可以直接使用缓存 | 常用于缓存验证,协商缓存,CDN 缓存等;客户端在发起请求时,会发送一个包含 If-None-Match 或 If-Modified-Since 等条件的头字段;服务器根据这些条件判断资源是否发生了变化,然后在响应头中指定 ETag、Last-Modified 等字段来帮助客户端进行缓存验证和更新操作 |
307 | Temporary Redirect | 临时重定向 | 该状态码与 302 Found 有着相同的含义,唯一的区别就是重新发送的请求不会将 POST 改为 GET |
400 | Bad Request | 请求存在语法错误 | 常见的有参数错误、格式不匹配、请求大小超出限制、访问未被授权的内容等 |
401 | Unauthorized | 未经授权,需要身份验证才能访问资源 | 如登录失效发起请求,服务端可以返回 401 要求客户端重新登录 |
403 | Forbidden | 禁止访问 | 因权限、访问控制和安全等原因被禁止访问站点;在某些情况下,服务器可能会根据IP地址、地理位置对资源进行访问控制,这也就是有时候我们 CDN 地址报错 403 的原因 |
404 | Not Found | 服务器上无法找到对应的资源 | 访问的路径不存在、资源被移动或删除、未发布等原因都将导致 404 |
405 | Method Not Allowed | 使用的 http 方法不支持 | 如限定了资源只能通过 GET 请求,使用 DELETE 则服务器将报错 405 并在 Allowed 字段中告知客户端支持的请求方式 |
406 | Not Accepted | 请求资源的响应格式不可被客户端所接受 | 如客户端在请求头中指定了 Accept 字段,但服务端没有对应格式的内容返回给它 |
500 | Internal Server Error | 服务器内部错误 | 代码问题、资源问题、网络问题导致服务端无法正常响应请求 |
502 | Bad Geteway | 错误的网关 | 表示作为代理或网关的服务器从上游服务器接收到无效的响应 |
503 | Service Unavailable | 服务不可用 | 表示服务器暂时处于超负载或正在进行停机维护,如果知道恢复时间,可以在响应首部中添加 Retry-After |
- 1xx 信息提示
- 2xx 请求成功
- 3xx 重定向
- 4xx 客户端发生错误,一般由浏览器处理为 404
- 5xx 服务端内部发生错误
4.2、响应头
服务端可以通过响应头来告知客户端关于响应结果的一些信息,如果缓存控制,跨域访问等, node 中设置响应头的方式有
- 通过 res.setHeader 一次添加一个头部字段
- [推荐]通过 res.writeHead 一次写入多个头部字段
res.writeHead(200, {
'Content-Type': 'application/json;charset=UTF-8',
})
Content-Type 用来告知客户端响应内容的媒体类型,常用到的值有
- text/plain 纯文本
- text/html HTML 文档
- text/css CSS 样式表
- image/png PNG 图片
- application/json JSON 数据格式
比如我们要返回给浏览器一段 html 并希望它解析时,如果我们不指定 Content-Type 或者指定按文本解析
res.writeHead(200, {
'Content-Type': 'text/plain;charset=UTF-8',
})
res.write(
`<h1>我是测试标题</h1>`
)
res.end();
但是,如果我们设置 content-Type 为 text/html 时, 它将正确解析标题元素
res.writeHead(200, {
'Content-Type': 'text/html;charset=UTF-8',
})
res.write(
`<h1>我是测试标题</h1>`
)
res.end();
4.3、响应结果
给客户端响应结果,可以通过 res.write、res.end 方法来响应
- 直接 res.end('test') 返回数据并关闭写入流
- 通过 res.write('test') 写入响应内容,可以多次调用;写入完成后调用 res.end() 关闭写入流
res.write(JSON.stringify({
data: '测试 Hello World!',
}))
res.end();
响应结果必须调用 res.end() 来告知客户端响应结束,不然客户端会一直等待,直到超时(这也是为什么所有的网络请求都会设置一个超时时间的原因,一般是 10s)
5、http 模块使用
至此,我们已经掌握了使用 http 模块来搭建服务的过程;作为服务端,一般与我们前端交互最多就是与页面进行接口交互,与直接在浏览器中请求 node 接口不一样,页面中发起请求,首先要解决的就是跨域问题
5.1、接口跨域
我们的页面和 node 服务并不在同一个端口下,浏览器安全限制导致接口请求跨域报错;现在后台由我们自己控制,我们完全可以通过 cors 的形式解决跨域
'access-control-allow-origin': '*', // cors头,允许所有的域通过控制
数据库模拟
使用一个 user.js 文件来模拟用户列表数据库(比较麻烦,读取出来后需要能 JSON.parse 转化为对象,因此结尾不能有 ,)
[ { "id": "001", "name": "louzb", "sex": "男", "desc": "爱做菜的程序员" }, { "id": "002", "name": "hxn", "sex": "女", "desc": "身份未知的美女" }]
http 处理接口请求
依然是 http 模块启动一个服务,处理 user/info
接口请求;处理页面请求
const http = require('http')
const fs = require('fs/promises')
const { URL } = require('url')
const server = http.createServer();
server.on('request', async (req, res) => {
if (req.url === '/favicon.ico') {
return;
}
if (req.url.includes('/user/info')) {
// 请求用户列表
res.writeHead(200, {
'Content-Type': 'application/json;charset=UTF-8',
'access-control-allow-origin': '*', // cors头,允许所有的域通过控制
})
let userInfo = await fs.readFile('./user.js', { encoding: 'utf8' });
res.write(JSON.stringify({
code: 0,
msg: '成功',
data: JSON.parse(userInfo),
}))
res.end();
}
});
server.listen('3008', () => {
console.log(`服务启动成功: http://localhost:3008`)
});
页面调用接口获取
创建一个 html 文件,引入 jQuery 来发起 http 请求(也可以用浏览器的 fetch)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div>我是测试页面</div>
<script src="./jquery-3.6.0.js"></script>
<script>
$.ajax({
url: 'http://localhost:3008/user/info',
method: 'GET', // 请求方法
data: {}, // 请求参数
success: function (res) {
// 请求成功的回调函数
console.log(res);
},
});
</script>
</body>
</html>
没错,咱们已经成功的解决页面与接口的跨域问题;并通过页面请求拿到用户列表数据,只是这个用户列表是存储在本地 js 文件中
5.2、处理 post 请求
我们拿到了用户列表,现在想修改其中某用户的信息,我们就可以利用 post 请求来修改
页面中 Ajax 提交 post 请求并携带参数
// 修改用户数据
$.ajax({
url: 'http://localhost:3008/user/change',
method: 'POST', // 请求方法
data: {
id: '001',
desc: '我要把你的描述修改为 iKUN',
}, // 请求参数
success: function (res) {
// 请求成功的回调函数
console.log(res);
},
});
然后在 node 服务中处理这个 post 请求
if (req.url.includes('/user/change')) {
// 修改用户信息
let params = ''
req.on('data', (chunk) => {
// 监听请求体数据流,并将数据拼接
params += chunk
})
req.on('end', async () => {
// 请求体数据接收完毕,处理响应
let searchParams = new URLSearchParams(params);
console.log(searchParams)
// 读取用户列表,根据 ID 更改 desc
let userInfo = await fs.readFile('./user.js', { encoding: 'utf8' });
let users = JSON.parse(userInfo);
let changeUsers = users.map(item => {
if (item.id === searchParams.get('id')) {
item.desc = searchParams.get('desc');
}
return item;
})
// 覆盖写入文件
fs.writeFile('./user.js', JSON.stringify(changeUsers));
// 结束响应
res.writeHead(204, {
'Content-Type': 'application/json;charset=UTF-8',
'access-control-allow-origin': '*', // cors头,允许所有的域通过控制
})
res.end();
})
}
与 GET 请求不同(GET 请求的参数携带在 req.url 中),想要处理 post 携带的请求,必须通过监听 req 的 data 时间,将接收到的数据进行拼接;然后在 end 数据接收完毕事件中处理;我们可以利用 url 模块的 URLSearchParams 来处理
拿到 post 参数后,那么就是根据逻辑触发修改数据,然后覆盖写入了~
没错,你的 post 请求处理成功了,本地的 user.js 文件,对应 id 为 001 的用户描述已经修改为了 iKUN
5.3、聚合第三方 API
之前就说过 node 中不存在跨域一说;也就是说,一些没有权限限制的接口,node 可以随意访问,这就允许我们可以通过 node 服务聚合其他第三方的 API 来进行开发
5.4、配合爬虫记录数据
有了 node 之后,我们收集到的数据,就可以通过接口的形式保存到本地文件;
6、结束语
这里是匆匆结尾,诸如压缩、加密、响应头等内容并没有展开;
要问为什么?周五周五,快乐不添堵~
周五了,该摆一摆了。偶尔的摆烂,是为了以后能摆的更加熟练。干杯~