likes
comments
collection
share

七、http 全面攻略

作者站长头像
站长
· 阅读数 13

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 事件函数回调有两个参数,requestresponse

  • 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 的请求

七、http 全面攻略

3.1、处理 favicon 请求

favicon 是啥?就是每个网站左上角的那个小 logo;

七、http 全面攻略

对于这个请求,我们可以返回一张本地图片的内容即可,结合 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、请求头

七、http 全面攻略

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 部分作为参数

七、http 全面攻略

URL 对象内部有一个 searchParams 属性,返回一个 URLSearchParams 对象

4、response

在 server 的 request 回调函数中,第二个参数是我们的请求响应对象,一般为 response 或 res;遵照上述代码,我们将使用 res

4.1、响应状态

我们给客户端返回不同的状态码来表示请求的不同状态

  • [推荐]通过 res.statuCode = 400; 的形式设置响应状态
  • 通过 res.writeHead(200); 的形式设置响应状态
res.statuCode = 400;

理解响应状态,能很好的帮助我们进行接口处理,常见的状态码有

状态码描述说明场景
101Switch Protocol服务器正在根据客户端的指定,将协议切换成 Update 首部所列的协议一般用在 ws 连接,告知服务器由 http 协议切换到 WebSocket
200OK请求正常接收并响应日常接口响应成功
201Created服务器创建资源成功一般用于 POST 请求创建资源,成功后返回 201 并在响应头中包含 Location 字段,该字段指示新资源的 URL
202Accepted请求正常接收,但未处理完成一般用于大数据导出等消耗时间的请求,告知客户端请求还在处理,可以通过轮询的方式获取最终结果
204No Content请求正常接收,但没有返回内容如数据更新、删除;服务器并不需要返回任何内容
206Partial Content服务器已经成功处理了部分请求,返回了部分响应数据常见于分段下载,客户端通过 Range 头字段指定了只请求文件的一部分内容(如某个字节范围)时,服务器可以返回 206 状态码,并返回请求的部分数据
301Move Permanently永久性重定向响应头中包含 Location 字段,并指定新的 URL 地址,客户端在接收到响应后会自动重新发送 GET 请求到新地址(即使原来是 POST);如资源重定向、域名重定向、http 自动跳转 https
302Found临时重定向与 301 基本相同(Location 包含临时 URL,自动重新请求临时地址),不同是客户端会记住原始 URL,并在后续请求中继续原始 URL;302 在 AB 测试,网站维护临时页等场景下非常常见
304Not Modify资源未改变,可以直接使用缓存常用于缓存验证,协商缓存,CDN 缓存等;客户端在发起请求时,会发送一个包含 If-None-Match 或 If-Modified-Since 等条件的头字段;服务器根据这些条件判断资源是否发生了变化,然后在响应头中指定 ETag、Last-Modified 等字段来帮助客户端进行缓存验证和更新操作
307Temporary Redirect临时重定向该状态码与 302 Found 有着相同的含义,唯一的区别就是重新发送的请求不会将 POST 改为 GET
400Bad Request请求存在语法错误常见的有参数错误、格式不匹配、请求大小超出限制、访问未被授权的内容等
401Unauthorized未经授权,需要身份验证才能访问资源如登录失效发起请求,服务端可以返回 401 要求客户端重新登录
403Forbidden禁止访问因权限、访问控制和安全等原因被禁止访问站点;在某些情况下,服务器可能会根据IP地址、地理位置对资源进行访问控制,这也就是有时候我们 CDN 地址报错 403 的原因
404Not Found服务器上无法找到对应的资源访问的路径不存在、资源被移动或删除、未发布等原因都将导致 404
405Method Not Allowed使用的 http 方法不支持如限定了资源只能通过 GET 请求,使用 DELETE 则服务器将报错 405 并在 Allowed 字段中告知客户端支持的请求方式
406Not Accepted请求资源的响应格式不可被客户端所接受如客户端在请求头中指定了 Accept 字段,但服务端没有对应格式的内容返回给它
500Internal Server Error服务器内部错误代码问题、资源问题、网络问题导致服务端无法正常响应请求
502Bad Geteway错误的网关表示作为代理或网关的服务器从上游服务器接收到无效的响应
503Service 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();

七、http 全面攻略

但是,如果我们设置 content-Type 为 text/html 时, 它将正确解析标题元素

res.writeHead(200, { 
  'Content-Type': 'text/html;charset=UTF-8',
})
res.write(
  `<h1>我是测试标题</h1>`
)
res.end();

七、http 全面攻略

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头,允许所有的域通过控制

七、http 全面攻略

数据库模拟

使用一个 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>

七、http 全面攻略

没错,咱们已经成功的解决页面与接口的跨域问题;并通过页面请求拿到用户列表数据,只是这个用户列表是存储在本地 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 参数后,那么就是根据逻辑触发修改数据,然后覆盖写入了~

七、http 全面攻略

没错,你的 post 请求处理成功了,本地的 user.js 文件,对应 id 为 001 的用户描述已经修改为了 iKUN

5.3、聚合第三方 API

之前就说过 node 中不存在跨域一说;也就是说,一些没有权限限制的接口,node 可以随意访问,这就允许我们可以通过 node 服务聚合其他第三方的 API 来进行开发

七、http 全面攻略

5.4、配合爬虫记录数据

有了 node 之后,我们收集到的数据,就可以通过接口的形式保存到本地文件;

6、结束语

这里是匆匆结尾,诸如压缩、加密、响应头等内容并没有展开;

要问为什么?周五周五,快乐不添堵~

周五了,该摆一摆了。偶尔的摆烂,是为了以后能摆的更加熟练。干杯~