一、HTTP 协议简述
1. 协议的基本理解
协议
协议这个词很好理解,既约定的规则,举个简单例子,公司要求员工上班打卡才能计算工时,这就是一种协议,约束了员工的行为,如果员工不遵守该协议,就不能计算工时,只能算作缺勤
在计算机网络中协议也是同样的作用,只不过更多的是用来定义数据传输时的编解码的,数据只能以二进制的形式进行传输,所以数据发送方需要将原本的数据格式,以一套规则编码成二进制,然后数据接收方再用相同的规则将二进制解码成原有的数据格式,这个双方约定好的规则就是协议
OSI 网络模型
在一个完整的网络传输中,数据在不同阶段使用的协议也是不同的,国际标准化组织 ISO 对不同阶段进行了分层,既 OSI 7 层网络模型。但是一般学习使用的都是其简化来的五层网络模型
OSI 5 层网络模型对应的作用和协议
分层 | 作用 | 代表性协议 |
---|---|---|
应用层 | 编解码传输层的二进制数据,协议规定了二进制与原数据结构互相转换的规则 | HTTP、FTP |
传输层 | 生成或解析网络层数据包. 协议规定数据包中需要指明端口号 | TCP、UDP |
网络层 | 生成或解析链路层数据包, 这层用来逻辑寻址, 协议规定数据包中应指明 IP 地址 | IP |
数据链路层 | 生成或解析物理层数据包, 这层用来物理寻址, 协议规定数据包中应指明 MAC 地址, 网卡就遵循该协议 | MAC |
物理层 | 真正的发送或接收数据,这层的协议用来定义传输介质的规格、传输频率等 | IEEE802.5 |
应用层和传输层是程序员应该关注的,Socket 编程就是针对传输层的数据进行的,Socket 可以在发送流程中,把按照规则构建的原始数据结构编码为二进制格式给传输层,接收流程中,把传输层的二进制数据按规则解码回原始数据结构,这里说的规则就是应用层协议,掌握 Socket 编程就可以自定义应用层协议。
2. HTTP 协议的数据格式
HTTP 是由万维网定义的一套超文本传输协议,其属于应用层协议,常用在浏览器与服务器的通信中,服务与服务间的通信也可以使用。现在简单过一下 HTTP 协议格式,更详细的得查看 W3C 官网 或 HTTP 快速指南
HTTP 协议的请求报文
(1) 报文的第一部分是请求行,主要内容由请求方法、URL、协议版本构成,回车换行后代表请求行内容结束
内容 | 取值参考 |
---|---|
请求方法 | GET、POST、PUT、HEAD、DELETE 等, |
URL | URL 的语法定义为 scheme://host:port/path?query#fragment,其中 /path?query#fragment 部分就是该字段内容 |
协议版本 | HTTP/1.0、HTTP/1.1 等 |
(2) 报文的第二部分是请求头,由 key:value
格式的键值对组成,可以设置多个键值对,回车换行代表一个键值对结束,后面是新的键值对,连续两个回车换行时,代表请求头内容结束
请求头中的键值对可自定义,也有一些预定义键值对,比如 Content-Type、Accept、Cookie、Authorization 等
(3) 报文的第三部分是请求体,不管是 GET、POST 还是其他方法,都可以有请求体
#HTTP 请求报文示例
POST /cgi-bin/process.cgi HTTP/1.1
User-Agent: Mozilla/4.0 (compatible; MSIE5.01; Windows NT)
Host: www.tutorialspoint.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 1000
Accept-Language: en-us
Accept-Encoding: gzip, deflate
Connection: Keep-Alive
licenseID=string&content=string&/paramsXML=string
HTTP 协议的响应报文
(1) 报文的第一部分是响应行,内容由协议版本、响应状态码、响应状态码描述构成,回车换行后代表响应行内容结束
内容 | 取值 |
---|---|
协议版本 | HTTP/1.0、HTTP/1.1 等 |
状态码 | 2XX(服务器处理正常)、3XX(重定向)、4XX(客户端请求错误)、5XX(服务器处理错误),具体请参照上面提到的快速指南 |
状态码描述 | 状态码的文字描述 |
(2) 报文的第二部分是响应头,格式与请求头相同,第三部是响应体,内容是服务器返回的数据
#HTTP 响应报文示例
HTTP/1.1 200 OK
Date: Mon, 27 Jul 2009 12:28:53 GMT
Server: Apache/2.2.14 (Win32)
Last-Modified: Wed, 22 Jul 2009 19:15:56 GMT
Content-Length: 88
Content-Type: text/html
Connection: Keep-Alive
<html>
<body>
<h1>Hello, World!</h1>
</body>
</html>
二、Express 服务器
通过浏览器发送 HTTP 协议的消息时,浏览器会帮我们自动构建好满足协议规则的请求报文,然后编码为二进制后在传输层、网络层等 OSI 网络模型中依次向下传输,当数据到达服务器后也会按照 OSI 网络模型定义的依次向上传递
当数据到达传输层后,需要使用 Socket API 根据 HTTP 协议对二进制数据进行解析还原,如果不想自己写 Socket API 代码,可以使用第三方支持 HTTP 协议的服务器,这些服务器会自动帮我们完成 Socket API 的操作以及提供更多功能, 如: Nignx、Express 等
Express 服务器就是一个基于 NodeJS 环境的 HTTP 服务器(说白了就是用 JS 语法实现的 Socket 通信服务器),它不仅能编解码 HTTP 报文,还可以根据请求路径,将请求信息封装成 JS 对象,然后路由到我们对应的 JS 处理函数中
本文只记录 Express 的简单使用,详细学习请参照 Express 官方地址
1. Express 服务器的安装和启动
(1) 创建目录
Express 依赖 NodeJS 环境,所以先保证电脑中安装了 NodeJS,然后创建一个目录,此处我将目录命名为 express-server
,该目录用来安装 Express 服务器,以及编写各个请求对应的业务处理代码
(2) 通过 npm 安装 express 模块
① 进入 express-server
目录内,使用命令行工具运行 npm init
让 npm 来管理项目,运行命令过程中会让填各种项目信息,不用管一路回车就可以
② 命令行工具内继续运行命令 npm install express --save
安装 Express 服务器
(3) 测试 Express 服务器
① 在 express-server
目录内创建 JS 文件,用来配置服务器,我此处将文件命名为 server.js
:
// 导入 Express 模块
const express = require('express')
// 创建 Express 实例
const app = express()
// 指定本程序监听的端口
const port = 8080
// 启动服务器,第一个参数为服务器监听的端口,第二个参数为服务启动成功后的回调函数
app.listen(port, () => {
console.log(`Express 服务器启动成功,监听的端口为: ${port}`)
})
② 命令行工具内运行 node server.js
启动 Express 服务器程序
(3) 安装热部署插件 nodemon
每次修改 server.js
文件 或 其引用的文件后,都必须重新运行 node server.js
命令,才能将修改后的内容更新到 Express 服务器中,非常麻烦。我们可以通过安装 nodemon 模块,来解决这个问题, 当我们修改 server.js
或 其引用的文件后,nodemon 会自动重启 Express 服务器并加载我们最新的代码
安装 nodemon 模块包,命令行工具中运行 npm install nodemon -g
命令:
将 node server.js
启动命令改为 nodemon server.js
命令来启动项目,之后每次修改保存都会重启 Express 服务器:
2. Express 服务器的基本使用
(1) 监听 Get 请求
监听 Get 请求很简单,在 server.js
中追加如下代码即可:
// 监听请求报文中 URL 为 /testGetReq 的 Get 请求
app.get('/testGetReq', (req, res) => {
// 浏览器的 Get 请求,提交的参数都会拼接在 URL 中,
// 获取 URL 中参数的方式为 req.query.参数名
const name = req.query.name
// 发送响应数据
res.send('get test success,参数是:' + name)
})
使用 Postman 测试 Get 请求:
(2) 监听 Post 请求
监听 Post 请求相对麻烦一点,需要借助 body-parser
模块,运行 npm i body-parser --save
安装 body-parser
模块
在 server.js
中追加如下代码使用 body-parser
解析 Post 请求中的参数:
// 导入 body-parser 模块
const bodyParser = require('body-parser');
// 解析 Content-Type=application/json 的请求体中的请求信息
app.use(bodyParser.json());
// 解析 Content-Type=text/plain 的请求体中的请求信息
app.use(bodyParser.text());
// 解析 Content-Type=application/x-www-form-urlencoded 的请求体中的请求信息
app.use(bodyParser.urlencoded({
extended: false
}));
// 监听请求报文中 URL 为 /testPostReq 的 Post 请求
app.post('/testPostReq', (req, res) => {
// 获取 Post 请求体中的参数
const name = req.body.name
// 发送响应数据
res.send('post test success,参数是:' + name)
})
使用 Postman 测试 Post 请求,Content-type=application/json
:
使用 Postman 测试 Post 请求,Content-type=application/x-www-form-urlencoded
:
三、AJAX 异步网络请求
在 express-server
目录下创建 startup.js
,目的是利用 express
搭建一个专门启动 HTML 页面的服务器:
// 导入 Express 模块
const express = require('express')
// 创建 Express 实例
const app = express()
// 指定本程序监听的端口
const port = 8081
// 启动服务器,第一个参数为服务器监听的端口,第二个参数为服务启动成功后的回调函数
app.listen(port, () => {
console.log(`Express 服务器启动成功,监听的端口为: ${port}`)
})
运行 nodemon startup.js
,启动服务器,监听 8081 端口
1. ES5 的 XMLHttpRequest 对象实现 AJAX 异步网络请求
ES5 中使用原生的 AJAX 时,需要用到 XMLHttpRequest
对象,以下是其常用的属性和方法总结,更多使用方式请参照文档 MDN,注意:XMLHttpRequest
对象不兼容早期 IE,需要使用 ActiveXObject
对象
名称 | 类型 | 描述 |
---|---|---|
readyState | 属性 | 请求状态(0: 未初始化、1: 已调用 open 方法、2: 已调用 send 方法、3: 接收到响应头、4: 接收到响应体) |
status | 属性 | 响应状态码 |
statusText | 属性 | 响应状态码描述 |
responseType | 属性 | 服务器返回数据的格式化方式, 设置为 json 时,会将服务器返回结果按照 json 格式转换成对象,否则是文本 |
responseText | 属性 | 响应体,当 responseType 为 text 时可用 |
response | 属性 | 完整的响应对象 |
timeout | 属性 | 超时时间 |
ontimeout | 属性 | 接收一个函数,当超时发生后的回调 |
onerror | 属性 | 接收一个函数,当发生网络错误时的回调 |
onreadystatechange | 属性 | 接收一个函数,请求状态变更时的回调,最多能触发 4 次,0->1、1->2、2->3、3->4 |
open | 方法 | open(Mehod, URL),初始化链接,设置 URL 和 HTTP 方法 |
send | 方法 | send(),正式发送 HTTP 请求 |
getAllResponseHeaders | 方法 | getAllResponseHeaders() 获取全部响应头 |
setRequestHeader | 方法 | setRequestHeader(key, value),设置响应头, 只能在调用 open() 后使用 |
XMLHttpRequest 的基本使用
(1) 修改 server.js
,在服务器追加新的监听函数,第一部分代码先不用管,和跨域有关,重点看 app.get 部分:
// 拦截所有请求 判断跨域
app.all("*", function (req, res, next) {
// 设置允许跨域的域名,*代表允许任意域名跨域
res.header("Access-Control-Allow-Origin", "*")
// 允许的header类型
res.header("Access-Control-Allow-Headers", "*")
// 跨域允许的请求方式
res.header("Access-Control-Allow-Methods", "*")
if (req.method.toLowerCase() == 'options')
res.send(200) // 让 options 尝试请求快速结束
else
next() // 继续路由
})
// 监听请求报文中 URL 为 /getXMLHttpRequest 的 Get 请求
app.get('/getXMLHttpRequest', (req, res) => {
// AJAX 涉及到跨域问题,这里用来开放同源限制
res.setHeader('Access-Control-Allow-Origin', '*')
// 响应格式为文本
res.send(req.query.name + ' 你好,这是服务器返回的消息')
})
(2) 在 express-server
目录创建用来发送请求的 HTML 页面 request.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>Document</title>
<style>
div {
width: 200px;
height: 200px;
border: 1px solid black;
}
</style>
</head>
<body>
<form>
<input type="button" value="点击展示内容">
</form>
<div></div>
<script>
// 添加按钮点击事件
document.querySelector('input').addEventListener('click', () => {
// 第一步:创建 XMLHttpRequest 对象
const request = new XMLHttpRequest()
// 第二步:设置请求方式和URL地址
request.open('GET', 'http://127.0.0.1:8080/getXMLHttpRequest?name=ares5k')
// 第三步:设置请求状态(readyState)发生变化时的回调
request.onreadystatechange = () => {
// 请求状态为已经接收到响应体
if (request.readyState === 4) {
// 响应状态码在 200 ~ 300 之间代表服务器响应成功
if (request.status >= 200 && request.status < 300) {
// 将服务器返回数据渲染到DIV中
document.querySelector('div').innerText = request.response
}
}
}
// 第四步:发送请求
request.send()
})
</script>
</body>
</html>
(3) 修改 startup.js
追加返回 request.html
的处理,注意响应普通数据使用的是 res.send
,响应文件使用 res.sendFile
:
// 响应页面
app.get('/startupRequest', (req, res)=>{
res.sendFile(__dirname + '/request.html')
})
(4) 浏览器访问 http://127.0.0.1:8081/startupRequest
,以服务器的方式运行页面测试:
XMLHttpRequest 发送 json 数据
(1) 修改 server.js
,在服务器追加新的监听函数:
// 监听请求报文中 URL 为 /postXMLHttpRequest 的 Post 请求
app.post('/postXMLHttpRequest', (req, res) => {
// 因为请求头中设置了 Content-type,所以能自动将Json字符串转换成对象
// Json 对象: {name: 'ares5k'}
// Json 字符串: "{name: 'ares5k'}"
let obj = req.body
// 响应格式为文本
res.send(JSON.stringify({
name: obj.name,
content: '你好,这是服务器返回的消息'
}))
})
(2) 修改 request.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">
<meta http-equiv="Access-Control-Allow-Origin" content="*">
<title>Document</title>
<style>
div {
width: 200px;
height: 200px;
border: 1px solid black;
}
</style>
</head>
<body>
<form>
<label for="name">请输入姓名</label><input name="name" type="text" value="">
<input type="button" value="点击展示内容">
</form>
<div></div>
<script>
// 添加按钮点击事件
document.querySelector('input[type=button]').addEventListener('click', () => {
// 第一步:创建 XMLHttpRequest 对象
const request = new XMLHttpRequest()
// 第二步:设置请求方式和URL地址
request.open('POST', 'http://127.0.0.1:8080/postXMLHttpRequest')
// 设置头信息为Json格式后,服务端就会自动把Json字符串变为Json对象
// Json 对象: {name: 'ares5k'}
// Json 字符串: "{name: 'ares5k'}"
request.setRequestHeader('Content-Type', 'application/json')
// 设置响应格式为Json后,会自动把服务端返回的Json字符串变为Json对象
// Json 对象: {name: 'ares5k'}
// Json 字符串: "{name: 'ares5k'}"
request.responseType = 'json'
// 第三步:设置请求状态(readyState)发生变化时的回调
request.onreadystatechange = () => {
// 请求状态为已经接收到响应体
if (request.readyState === 4) {
// 响应状态码在 200 ~ 300 之间代表服务器响应成功
if (request.status >= 200 && request.status < 300) {
// 自动将Json字符串转为对象
let obj = request.response
// 将服务器返回数据渲染到DIV中
document.querySelector('div').innerText = obj.name + ' ' + obj.content
}
}
}
// 第四步:发送请求
request.send(JSON.stringify({
name: document.querySelector('input[type=text]').value
}))
})
</script>
</body>
</html>
(3) 浏览器访问 http://127.0.0.1:8081/startupRequest
,以服务器的方式运行页面测试:
Json 和 对象自动转换
发送请求时:在请求头中设置了请求的数据格式为 content-type: application/json
,如果服务端支持该格式的解析,则能根据 content-type
自动将 Json 字符串转换成对象类型
接收响应时:因为设置了 XMLHttpRequest
对象的 responseType = 'json'
, 所以 XMLHttpRequest
对象会将服务器的响
应结果,以Json格式解析,并转成对象类型, 如果服务器返回的不是Json字符串则无法成功转换
Json 和 对象手动转换
将对象转换成Json格式字符串:JSON.stringify(对象)
将Json格式字符串转换成对象:JSON.parse(Json格式字符串)
XMLHttpRequest 发送 form 参数时的问题
发送 GET 请求时,将 form 中的数据拼成 param1=value1¶m2=value2
的格式,然后拼接到 XMLHttpRequest.open
的 URL 后就可以
发送 POST 请求时,有三种常见的情况:
(1) 以 content-type: application/x-www-form-urlencoded
格式发送数据时,可以将 form 中的数据拼成 param1=value1¶m2=value2
的样子,然后传入到 XMLHttpRequest.send
函数中就可以,也可以借助 FormData
对象
(2) 以 content-type: application/json
格式发送数据时,可以将 form 中的数据拼接成 Json 格式的字符串,如 '{"name":"ares5k"}'
然后传入到 XMLHttpRequest.send
方法中
(3) 以 content-type: multipart/form-data
格式发送数据时,需要借助 JS 的 FormData
对象,常用的场景是发送文件
2. 利用 jQuery 库实现 AJAX 异步网络请求
在项目中想封装一个相对完美的 XMLHttpRequest
,是比较复杂的,所以一般都会使用 jQuery 库来发送异步请求,jQuery 内部也是对 XMLHttpRequest
对象进行的封装,下面简单记录 jQuery 操作 AJAX 的方式,更多、更详细的 AJAX 使用方式,请参考 jQueryApi
(1) jQuery 发送 GET 或 POST 请求
利用 jQuery 核心对象的 get
或 post
函数即可,其常见的参数列表如下:
参数名 | 描述 |
---|---|
url | 服务器 URL |
data | 可选,请求参数列表, 两种格式 param1:value1¶m2:value2 或 {param1:value1, param2:value2} 都可以 |
callback | 可选,成功时的回调函数,有3个参数,data: 响应体,status: 请求状态(success 等),xhr: 请求使用的 XMLHttpRequest 对象 |
responseType | 可选,预期的返回类型,jQuery 会根据我们的预期类型对响应体进行转换,如 json |
(1) 在 express-server
下创建 jQueryRequest.html
文件,利用 jQuery 向我们之前的服务 getXMLHttpRequest
发送请求:
<!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">
<meta http-equiv="Access-Control-Allow-Origin" content="*">
<title>Document</title>
<style>
div {
width: 200px;
height: 200px;
border: 1px solid black;
}
</style>
</head>
<body>
<form>
<input type="button" value="点击展示内容">
</form>
<div></div>
<!-- 引入 jQuery CDN -->
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.js"></script>
<script>
// 为按钮绑定点击事件
$('input[type=button]').on('click', () => {
// 发送请求
$.get('http://127.0.0.1:8080/getXMLHttpRequest', // 服务器 URL
'name=ares5k', // 请求参数, 用这种格式也可以 {name: 'ares5k'}
(data, status, xhr) => {$('div').text(data)}) // 回调函数
})
</script>
</body>
</html>
(2) 修改 startup.js
追加返回 jQueryRequest.html
的处理:
// 响应页面
app.get('/startupRequestWithJQuery', (req, res)=>{
res.sendFile(__dirname + '/jQueryRequest.html')
})
(3) 浏览器访问 http://127.0.0.1:8081/startupRequestWithJQuery
,以服务器的方式运行页面测试:
(2) jQuery 发送自定义请求
想对请求更详细的设置,我们可以使用 jQuery 核心对象的 ajax
函数,其参数为对象结构,该对象常见的属性如下:
属性名 | 描述 |
---|---|
url | 服务器 URL |
type | 请求方法, GET、POST、PUT |
timeout | 可选,请求延迟时间,毫秒单位 |
headers | 可选,请求头,格式为 {key1:value1, key2:value2} |
data | 可选,请求参数列表, 两种格式 param1:value1¶m2:value2 或 {param1:value1, param2:value2} 都可以 |
dataType | 可选,预期的返回类型,jQuery 会根据我们的预期类型对响应体进行转换,如 application/json |
success | 可选,成功时的回调函数,有3个参数,data: 响应体,status: success 字符串,xhr: 请求使用的 XMLHttpRequest 对象 |
error | 可选, 超时或发生错误时的回调函数, 有3个参数, xhr: 请求使用的 XMLHttpRequest 对象, status: error 字符串, statusText: 错误信息 |
(1) 修改 jQueryRequest.html
文件,向我们之前的服务 postXMLHttpRequest
发送请求:
<!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">
<meta http-equiv="Access-Control-Allow-Origin" content="*">
<title>Document</title>
<style>
div {
width: 200px;
height: 200px;
border: 1px solid black;
}
</style>
</head>
<body>
<form>
<input type="button" value="点击展示内容">
</form>
<div></div>
<!-- 引入 jQuery CDN -->
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.js"></script>
<script>
// 为按钮绑定点击事件
$('input[type=button]').on('click', () => {
// 发送请求
$.ajax({
url: 'http://127.0.0.1:8080/postXMLHttpRequest', // 服务器 URL
type: 'POST', // 请求方法
data: JSON.stringify({
name: 'ares5k'
}), // 请求参数, 用这种格式也可以 'name=ares5k'
timeout: 2000, // 超时时间
dataType: 'json', // 预计响应体格式
contentType: 'application/json', // 请求参数格式
success: (data) => {
// 请求成功的回调
$('div').text(data.name + ' ' + data.content)
}
})
})
</script>
</body>
</html>
(2) 浏览器访问 http://127.0.0.1:8081/startupRequestWithJQuery
,以服务器的方式运行页面测试:
(3) jQuery 快速格式化 Form 数据的方法
将 Form 内容拼装成 param1:value1¶m2:value2
格式很麻烦, jQuery 提供了函数简化这类操作 :
const data = $('form').serialize() // 将 form 表单中的内容格式化成 param1:value1¶m2:value2 的格式
const data = $('form').serializeArray() // 将 form 表单中的内容格式化成 [{param1:value1}, {param2:value2}] 的数组格式
$.get('http://127.0.0.1:8080/getXMLHttpRequest', data})
3. ES6 的 Fetch 函数实现 AJAX 异步网络请求
fetch
是 ES6 新增的发送异步请求的全局函数,它跟 XMLHttpRequest
相比,能更简洁的操作异步请求,也能更细粒度的控制请求信息,fetch
函数会返回一个 Promise
对象, 用来避免回调地狱问题
在实际开发中,直接使用 fetch
来发送异步请求的项目不是很多,所以暂时也没必要深究,下面用代码简单记录下如何用 fetch
发送异步请求,更多使用方式请参照文档 MDN
演示
(1) 在 express-server
下创建 fetchRequest.html
文件,利用 fetch 向我们之前的服务发送请求,注意 fetch
发送的
GET 请求,不能传 body
参数:
<!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">
<meta http-equiv="Access-Control-Allow-Origin" content="*">
<title>Document</title>
<style>
div {
width: 200px;
height: 200px;
border: 1px solid black;
}
</style>
</head>
<body>
<!-- GET 请求部分 -->
<form>
Get请求:
<input type="button" value="点击展示内容">
</form>
<div></div>
<br/>
<!-- POST 请求部分 -->
<form>
Post请求:
<input type="button" value="点击展示内容">
</form>
<div></div>
<script>
// GET 请求部分
// 添加按钮单击事件
document.querySelector('form:first-of-type input').addEventListener('click', () => {
// 利用 fetch 发送 ajax 请求
fetch(
// URL
'http://127.0.0.1:8080/getXMLHttpRequest?name=ares5k', {
// 请求方法
method: 'GET'
}).then((response) => {
// 从响应信息中读取响应体信息,response.text() 会返回 Promise 对象
return response.text()
}).then(data => {
// 填充元素内容
document.querySelector('div:first-of-type').innerText = data
})
})
// POST 请求部分
// 添加按钮单击事件
document.querySelector('form:last-of-type input').addEventListener('click', () => {
// 利用 fetch 发送 ajax 请求
fetch(
// URL
'http://127.0.0.1:8080/postXMLHttpRequest', {
// 请求方法
method: 'POST',
// 请求头
headers: {
'content-type': 'application/json'
},
// 请求体
body: JSON.stringify({
name: 'ares5k'
})
}).then((response) => {
// 从响应信息中以 JSON 格式读取响应体并转为对象,response.json() 会返回 Promise 对象
return response.json()
}).then(data => {
// 填充元素内容
document.querySelector('div:last-of-type').innerText = data.name + ' ' + data.content
})
})
</script>
</body>
</html>
(2) 修改 startup.js
追加返回 fetchRequest.html
的处理:
// 响应页面
app.get('/startupRequestWithFetch', (req, res)=>{
res.sendFile(__dirname + '/fetchRequest.html')
})
(3) 浏览器访问 http://127.0.0.1:8081/startupRequestWithFetch
,以服务器的方式运行页面测试:
4. 利用 axios 库实现 AJAX 异步网络请求
axios 库,应该是当前最流行的异步请求库,它可以在 nodejs 中和浏览器中自动使用不同的方式发送请求,nodejs 中会使用 http
模块, 在浏览器中则会使用 XMLHttpRequest
对象,axios 发送请求后也会返回一个 Promise 对象来解决回调地狱问题,它还可以对请求和响应进行拦截,比如添加公共的请求信息,本文简单记录其使用方式,想学习更多使用方式请参照 axios 中文文档,想学习源码请参考 axios github
演示
(1) 在 express-server
下创建 axiosRequest.html
文件,利用 axios 向我们之前的服务发送请求:
<!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">
<meta http-equiv="Access-Control-Allow-Origin" content="*">
<title>Document</title>
<style>
div {
width: 200px;
height: 200px;
border: 1px solid black;
}
</style>
</head>
<body>
<!-- Get 请求 -->
<form>
Get请求:
<input type="button" value="点击展示内容">
</form>
<div></div>
<br />
<!-- Post 请求 -->
<form>
Post请求:
<input type="button" value="点击展示内容">
</form>
<div></div>
<br />
<!-- 自定义请求 -->
<form>
自定义发送方式:
<input type="button" value="点击展示内容">
</form>
<div></div>
<!-- 导入 axios 库 -->
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
// 添加按钮单击事件
document.querySelector('form:first-of-type input').addEventListener('click', () => {
// 用 axios 发送 get 请求
axios.get(
'http://127.0.0.1:8080/getXMLHttpRequest', // 服务器 URL
{
// URL 参数
params: {
name: 'ares5k'
}
})
.then(value => {
// 填充元素
document.querySelector('div:first-of-type').innerText = value.data
})
})
// 添加按钮单击事件
document.querySelector('form:nth-of-type(2) input').addEventListener('click', () => {
// 用 axios 发送 post 请求
axios.post(
'http://127.0.0.1:8080/postXMLHttpRequest', // 服务器 URL
// 请求体参数
{
name: 'ares5k'
})
.then(value => {
// 填充元素
document.querySelector('div:nth-of-type(2)').innerText = value.data.name + ' ' + value.data.content
})
})
// 添加按钮单击事件
document.querySelector('form:nth-of-type(3) input').addEventListener('click', () => {
// 用 axios 自定义发送请求
axios({
url: 'http://127.0.0.1:8080/postXMLHttpRequest', // 服务器 URL
method: 'POST', // 请求方式
// 请求体参数
data: {
name: 'ares5k'
}
}).then(value => {
// 填充元素
document.querySelector('div:nth-of-type(3)').innerText = value.data.name + ' ' + value.data.content
})
})
</script>
</body>
</html>
(2) 修改 startup.js
追加返回 axiosRequest.html
的处理:
// 响应页面
app.get('/startupRequestWithAxios', (req, res)=>{
res.sendFile(__dirname + '/axiosRequest.html')
})
(3) 浏览器访问 http://127.0.0.1:8081/startupRequestWithAxios
,以服务器的方式运行页面测试:
5. 异步网络请求与传统网络请求的区别
1. 传统网络请求演示
(1) 在 express-server
目录内创建请求用的 HTML 文件,命名为 oldReq.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>Document</title>
<style>
div {
width: 200px;
height: 200px;
border: 1px solid black;
}
</style>
</head>
<body>
<form action="http://127.0.0.1:8080/getContent">
<input type="submit" value="点击展示内容">
</form>
<div></div>
</body>
</html>
(2) 在 express-server
目录内创建响应用的 HTML 文件,命名为 oldRes.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>Document</title>
<style>
div {
width: 200px;
height: 200px;
border: 1px solid black;
}
</style>
</head>
<body>
<form action="http://127.0.0.1:8080/getContent">
<input type="submit" value="点击展示内容">
</form>
<div>这是服务器返回的数据</div>
</body>
</html>
(3) 修改 server.js
文件,追加请求处理逻辑:
// 响应页面
app.get('/getContent', (req, res) => {
res.sendFile(__dirname + '/oldRes.html')
})
(4) 修改 startup.js
文件,追加初始页面:
// 响应页面
app.get('/startupRequestOldReq', (req, res) => {
res.sendFile(__dirname + '/oldReq.html')
})
(5) 运行 http://127.0.0.1:8081/startupRequestOldReq
,以服务器的方式启动页面,然后点击按钮展示响应后的页面,
状态码返回 304 是因为之前执行过该请求, 而本次用相同的 GET 请求访问,其结果和上次相比没有变化,服务器就会返回 304
2. AJAX 与 传统请求对比
(1) 传统请求方式:
流程:通过上面对传统请求的演示,我们可以发现,页面请求后,会发生页面跳转,等待服务器返回新的完整的 HTML,尽管返回的 HTML 中,大部分内容与原页面是相同的,但浏览器仍然要重新渲染
缺点:传统请求方式的缺点非常明显,当返回页面过大,就会影响网络传输,也会影响浏览器的渲染时间,在这期间,因为浏览器发生了跳转,所以会一直处于白屏状态,影响用户使用体验
优点:因为返回的是带数据的完整的 HTML,所以更容易做 SEO 优化
(2) AJAX 请求方式:
优点:一般使用 AJAX 发送请求时,只是为了向服务器请求部分的数据,不需要整个页面返回,这样的可以减少网络开销,因为返回的只是小部分数据,所以页面渲染也会很快,AJAX 请求时也不会发生页面跳转,不会出现白屏的情况
缺点:使用 AJAX 的 HTML 中,很多内容都是等待 AJAX 动态获取的,不像传统请求的方式,返回的都是带数据的静态页面,所以影响 SEO 优化 ,浏览器的返回按钮也会受影响,还有就是 AJAX 有同源策略限制
6. AJAX 浏览器缓存问题
有些浏览器会对 AJAX 发送的 GET 请求结果进行强制缓存,比如 IE,当再次发送相同路径,相同参数的请求时,请求不会真正的发送到服务器,而是直接从缓存中获取上次的结果,这就可能导致结果不是最新的,为了避免这种问题,我们可以在 URL 路径后加上当前时间的时间戳,以保证每次访问时 URL 都有变化
如: XMLHttpRequest.open('GET','http://127.0.0.1:8000?t=' + Date.now)
四、同源策略及跨域
1. 同源策略
同源策略是浏览器的一种安全机制,同源策略限制:不能向非同源的服务器地址发起 AJAX 请求,也不能对非同源页面进行 DOM 操作 ( 主要是 iframe 的场景 )。同源是指,AJAX 请求的服务器的域名、协议、端口与本站完全相同,或 iframe 场景下,要操作的窗口与本窗口的域名、协议、端口完全相同,反之就是非同源。
(1) 假设没有同源策略的 iframe 场景
iframe 可以在同一浏览器窗口内嵌套多个页面,那么黑客就可以这样做:
- 黑客创建一个假的网站(
fake.com
),并在主页面内利用 iframe 将银行网站(bank.com
) 原比例嵌套进来, 包括地址栏信息,此时是分辨不出真假的 - 黑客诱骗用户访问假的网站 (
fake.com
),当用户在嵌套的银行页面 (bank.com
)输入完用户名和密码后,黑客就可以在主页面(fake.com
) 中操作bank.com
的 DOM,从而盗取用户的用户名和密码
(2) 假设没有同源策略的 ajax 场景
这种场景,很容易发生经典的 CSRF ( 跨站请求伪造 ) 攻击:
- 用户通过浏览器访问银行网站
bank.com
,这时银行网站会将登录标识记录到域名为bank.com
的 cookie 中 - 之后用户不小心访问了黑客网站 (
black.com
),黑客网站 (black.com
) 内部利用 AJAX 向银行 (bank.com
) 发送了请求,发送请求时默认会携带请求域名下的 cookie 信息 - 银行
bank.com
验证 cookie 中的信息,发现已登录,随后会将 AJAX 访问的数据返回,从而造成信息泄露
上述案例很好的解释了同源策略的重要性,但有时我们需要达到跨域访问的效果,这时就需要一些手段,常见的有 JSONP、CORS、反向代理服务器(本文不讨论)
2. JSONP
这是一种早期的跨域访问手段,虽然可以跨域,但并不是真正解决同源策略限制的官方手段,其原理只是利用了标签的 src 属性不受同源策略约束的特点来访问服务器,再通过执行服务器返回的 JS 代码来解决跨域问题
标签的 src 属性不受同源策略限制,换句话说 JSONP 的方式跟同源策略这个概念不发生关系,这只是一种避开同源策略来实现跨域的手段,src 请求服务器时不会携带 cookie 信息,而且只能是 GET 请求
JSONP 流程图示
上图中 JSONP 的重要流程分析:
- index.html 中定义了单击事件函数 onClick 和 处理服务器响应结果的函数 callback
- onClick 函数触发后,会创建 script 标签,并将 src 属性赋值为服务器地址,浏览器解析新标签时会通过 src 地址到服务器去拉取资源
- 服务器处理完对应的业务后,将待返回的 JSON 拼接在前端定义好的函数 ( callback ) 的形参中,最后以字符串的形式在响应体中返回,其实拼接好的字符串 callback({name:'ares5k}), 就是一段 JS 函数调用的语句
- 前端收到响应后,发现 conent-type 是 javascript, 则会立即执行响应体的内容,既 callback({name:'ares5k}) 语句
这个流程中的第三步,可以很好的解释 JSONP (JSON-Padding) 这个名字,服务器用待返回的 JSON 当作参数,填充到前端已定义好的形参中
JSONP 演示
(1) 用代码演练一下上边的流程,先创建 HTML 页面 jsonpRequest.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">
<meta http-equiv="Access-Control-Allow-Origin" content="*">
<title>Document</title>
<style>
div {
width: 200px;
height: 200px;
border: 1px solid black;
}
</style>
</head>
<body>
<form>
<input type="button" value="点击展示内容">
</form>
<div></div>
<script>
// 添加按钮点击事件
document.querySelector('input').addEventListener('click', () => {
// 创建 script 标签
let script = document.createElement('script')
// 让 script 标签的 src 属性指向服务器,并将服务器响应信息的处理函数名告知服务器
script.src = 'http://127.0.0.1:8080/jsonp?name=ares5k&callback=callbackMethod'
// 将 script 标签渲染到画面,同时触发 src 下载
document.querySelector('body').appendChild(script)
})
// 服务器响应信息的处理函数
function callbackMethod(json){
document.querySelector('div').innerText = json.name + ' ' +json.content
}
</script>
</body>
</html>
(2) 修改 server.js
,追加请求处理函数,并把 server.js
中,以前添加的跨域部分代码删除:
// 拦截所有请求 判断跨域
// app.all("*", function (req, res, next) {
// // 设置允许跨域的域名,*代表允许任意域名跨域
// res.header("Access-Control-Allow-Origin", "*")
// // 允许的header类型
// res.header("Access-Control-Allow-Headers", "*")
// // 跨域允许的请求方式
// res.header("Access-Control-Allow-Methods", "*")
// if (req.method.toLowerCase() == 'options')
// res.send(200) // 让 options 尝试请求快速结束
// else
// next() // 继续路由
// })
// 监听请求报文中 URL 为 /postXMLHttpRequest 的 Post 请求
app.get('/jsonp', (req, res) => {
// 参数
let name = req.query.name
// 服务器响应后要调用的方法名
let callbackMethod = req.query.callback
// 服务器真正要响应的数据
const json = {
name: name,
content: '你好,这是服务器返回的消息'
}
// 将真正要响应的数据填充到方法名中
res.send(`${callbackMethod}(${JSON.stringify(json)})`)
})
(3) 修改 startup.js
追加返回 jsonpRequest.html
的处理:
// 响应页面
app.get('/startupRequestJsonp', (req, res) => {
res.sendFile(__dirname + '/jsonpRequest.html')
})
(4) 浏览器访问 http://127.0.0.1:8081/startupRequestJsonp
,以服务器的方式运行页面测试:
利用 jQuery 库实现 JSONP
如果手动写 JSONP 嫌麻烦,也可以利用 jQuery 库实现,将 jsonpRequest.html
的 <script>
做如下修改即可:
<script>
// 添加按钮点击事件
document.querySelector('input').addEventListener('click', () => {
$.getScript('http://127.0.0.1:8080/jsonp?name=ares5k&=callbackMethod')
})
// 服务器响应信息的处理函数
function callbackMethod(json){
document.querySelector('div').innerText = json.name + ' ' +json.content
}
</script>
3. CORS
CORS ( Cross-origin resource sharing,跨域资源共享 ),和 JSONP 那种耍小聪明式的跨域方式不同,CORS 是 W3C 官方推荐的正规跨域方式,其执行流程又根据简单请求和复杂请求而不同
在弄懂 CORS 执行流程前,先看看实现 CORS 跨域所依赖的头信息,请求类头信息均为浏览器自动添加,响应类头信息需要由服务器端设置:
请求/响应 | 头名称 | 描述 |
---|---|---|
请求 | Origin | 请求页面的源信息(协议、域名和端口) |
请求 | Access-Control-Request-Method | 请求方法:GET、POST、PUT 等 |
请求 | Access-Control-Request-Headers | 请求头:简单请求规定的请求头之外的请求头 |
响应 | Access-Control-Allow-Origin | 服务器允许跨域的源(协议、域名和端口),* 代表允许任何源访问 |
响应 | Access-Control-Allow-Methods | 服务器允许的请求方法 |
响应 | Access-Control-Allow-Headers | 服务器允许的请求头 |
响应 | Access-Control-Max-Age | 预检有效期,有效期内不会再发送预检请求,单位是秒 |
1. 简单请求
本文记录的简单请求的原理,与全网大部分文章都不同,后面会详细说,孰对孰错还得自行判断
(1) 简单请求的定义
同时满足下面两个条件的 AJAX 请求,即为简单请求:
① 请求方法范围:HEAD、GET、POST
② 请求头范围:Accept、Accept-Language、Content-Language、Last-Event-ID、
Content-Type (application/x-www-form-urlencoded、multipart/form-data、text/plain)
(2) 简单请求的流程
我总结的请求流程如下:
- 服务器预先定义好响应时要添加的 CORS 响应头信息
Access-Control-Allow-Origin
(按需添加,不许跨域就不用添加) - 浏览器发送 AJAX 请求,并自动添加请求头
Origin
- 服务器正常执行业务逻辑, 响应返回前,将预先定义好的 CORS 响应头添加到响应报文中
- 浏览器判断响应头中的
Access-Control-Allow-Origin
是否包函请求头中的Origin
,包函的时候
代表服务器允许该域访问,则将响应体赋值给XMLHttpRequest
对象,否则将响应体丢弃报跨域错误
和网上其他文章有歧义的地方,在第三,第四步,网上第三,第四步流程如下:
- 服务器收到请求后,直接返回,在响应返回前,将预先定义好的 CORS 响应头添加到响应报文中
- 浏览器判断响应头中的
Access-Control-Allow-Origin
是否包函请求头中的Origin
,包函的时候代表服务器允许该域访问,则正常发送请求,否则不发送请求报跨域错误
我个人理解的流程和网上其他文章的流程,有本质的区别,网上文章的流程是浏览器发了两次请求,第一次请求时
服务器不会执行业务代码,而是直接返回包函响应头 Access-Control-Allow-Origin
的响应报文,浏览器认证通过后,
发起第二次请求真正执行业务逻辑,我当时看了就感觉这和复杂请求的预检过程没区别,所以自己调查了一番总结出
了我的流程,既不管 CORS 能否通过,简单请求的场景下,业务逻辑都会执行,CORS 通过与否,只是浏览器是否将
响应体丢弃的区别,而且浏览器只发送一次请求
为了验证我的结论,我们可以在服务器业务代码中打印 log 来证实一下
(1) 修改 request.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>Document</title>
<style>
div {
width: 200px;
height: 200px;
border: 1px solid black;
}
</style>
</head>
<body>
<form>
<input type="button" value="点击展示内容">
</form>
<div></div>
<script>
// 添加按钮点击事件
document.querySelector('input').addEventListener('click', () => {
// 第一步:创建 XMLHttpRequest 对象
const request = new XMLHttpRequest()
// 第二步:设置请求方式和URL地址
request.open('GET', 'http://127.0.0.1:8080/getXMLHttpRequest?name=ares5k')
// 第三步:设置请求状态(readyState)发生变化时的回调
request.onreadystatechange = () => {
// 请求状态为已经接收到响应体
if (request.readyState === 4) {
// 响应状态码在 200 ~ 300 之间代表服务器响应成功
if (request.status >= 200 && request.status < 300) {
// 将服务器返回数据渲染到DIV中
document.querySelector('div').innerText = request.response
}
}
}
// 第四步:发送请求
request.send()
})
</script>
</body>
</html>
(2) 修改 server.js
中 /getXMLHttpRequest
对应的业务代码,添加 log 打印,注意把以前跨域相关代码删除:
// 监听请求报文中 URL 为 /getXMLHttpRequest 的 Get 请求
app.get('/getXMLHttpRequest', (req, res) => {
console.log('简单请求时,虽然 CORS 不能通过,但是业务也执行了')
// 响应格式为文本
res.send(req.query.name + ' 你好,这是服务器返回的消息')
})
(3) 浏览器访问 http://127.0.0.1:8081/startupRequest
,以服务器的方式运行页面测试:
上面动图可以看到,请求后确实没有得到响应体信息,而且浏览器控制台也报出了 CORS 不通过的错误信息,那么我们再看看服务器的控制台有没有打印我们加在业务逻辑中的 log:
log 信息已经被打印出来,说明业务逻辑确实执行了,不受 CORS 影响
简单请求的场景下,预防恶意数据攻击
CORS 在简单请求场景下的安全问题, 完全是由浏览器来决定,其根据响应头来决定是否保留响应体,但这并不能保证我们的系统就一定是安全的,因为在简单请求的场景下,我们的业务代码一定会被执行,这就可能导致被恶意数据攻击
为了避免这种情况,建议服务器端除了添加 CORS 响应头,让浏览器保证安全的基础上,服务器端还应该自己写一个拦截器,当请求到达时,我们先判断是否可以跨域,如果不允许跨域,我们就提前返回,而不是让程序继续执行我们的业务代码,Spring MVC
框架的 CorsFilter
的实现原理就是如此
2. 复杂请求
复杂请求的流程就是多了一步预检 ( Preflight ) :
- 服务器预先定义好响应时要添加的 CORS 响应头信息
Access-Control-Allow-Origin
、Access-Control-Allow-Methods
、Access-Control-Allow-Headers
、Access-Control-Max-Age
(这些响应头都是按需添加) - 浏览器先发送一个请求方法为
OPTIONS
的请求,并自动添加请求头Origin
、Access-Control-Request-Method
、Access-Control-Request-Headers
(后两个请求头浏览器会按需添加) - 服务器的接口一般不会设置请求方法为
OPTIONS
对应的业务逻辑,一般来说服务器与OPTIONS
对应的处理就是直接返回,响应返回前,将预先定义好的 CORS 响应头添加到响应报文中 - 浏览器判断响应头中的
Access-Control-Allow-Origin
、Access-Control-Allow-Methods
、Access-Control-Allow-Headers
是否能与请求头中的Origin
、Access-Control-Request-Method
、Access-Control-Request-Headers
匹配,匹配成功后,代表允许跨域,会再次发送请求(我们真正的请求),并执行后面步骤,无法匹配时不会再发送请求,而会报 CORS 不通过的错误信息 - 浏览器认证通过后,发送真正的 AJAX 请求,并自动添加请求头
Origin
、Access-Control-Request-Method
、Access-Control-Request-Headers
(后两个请求头浏览器会按需添加) - 服务器正常执行业务逻辑
一起来验证复杂请求时,是否会发送两次网络请求,不要看前面的动图,前面的动图中我将预检请求给隐藏了
(1) 修改 request.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">
<meta http-equiv="Access-Control-Allow-Origin" content="*">
<title>Document</title>
<style>
div {
width: 200px;
height: 200px;
border: 1px solid black;
}
</style>
</head>
<body>
<form>
<label for="name">请输入姓名</label><input name="name" type="text" value="">
<input type="button" value="点击展示内容">
</form>
<div></div>
<script>
// 添加按钮点击事件
document.querySelector('input[type=button]').addEventListener('click', () => {
// 第一步:创建 XMLHttpRequest 对象
const request = new XMLHttpRequest()
// 第二步:设置请求方式和URL地址
request.open('POST', 'http://127.0.0.1:8080/postXMLHttpRequest')
// 设置头信息为Json格式后,服务端就会自动把Json字符串变为Json对象
// Json 对象: {name: 'ares5k'}
// Json 字符串: "{name: 'ares5k'}"
request.setRequestHeader('Content-Type', 'application/json')
// 设置响应格式为Json后,会自动把服务端返回的Json字符串变为Json对象
// Json 对象: {name: 'ares5k'}
// Json 字符串: "{name: 'ares5k'}"
request.responseType = 'json'
// 第三步:设置请求状态(readyState)发生变化时的回调
request.onreadystatechange = () => {
// 请求状态为已经接收到响应体
if (request.readyState === 4) {
// 响应状态码在 200 ~ 300 之间代表服务器响应成功
if (request.status >= 200 && request.status < 300) {
// 自动将Json字符串转为对象
let obj = request.response
// 将服务器返回数据渲染到DIV中
document.querySelector('div').innerText = obj.name + ' ' + obj.content
}
}
}
// 第四步:发送请求
request.send(JSON.stringify({
name: document.querySelector('input[type=text]').value
}))
})
</script>
</body>
</html>
(2) 修改 server.js
,将之前注释掉的跨域代码恢复:
// 拦截所有请求 判断跨域
app.all("*", function (req, res, next) {
// 设置允许跨域的域名,*代表允许任意域名跨域
res.header("Access-Control-Allow-Origin", "*")
// 允许的header类型
res.header("Access-Control-Allow-Headers", "*")
// 跨域允许的请求方式
res.header("Access-Control-Allow-Methods", "*")
if (req.method.toLowerCase() == 'options')
res.send(200) // 让 options 尝试请求快速结束
else
next() // 继续路由
})
(3) 浏览器访问 http://127.0.0.1:8081/startupRequest
,以服务器的方式运行页面测试:
上面动图中可以清晰的看到,先以 OPTIONS
发送了一次预检请求( Preflight ) ,之后又发送了我们真正的 POST
请求