HTTP 协议、AJAX - 异步网络哀求及跨域、同源计谋

农民  金牌会员 | 2024-12-26 07:54:13 | 显示全部楼层 | 阅读模式
打印 上一主题 下一主题

主题 820|帖子 820|积分 2460

一、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 等,URLURL 的语法定义为 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 还是其他方法,都可以有哀求体
  1. #HTTP 请求报文示例
  2. POST /cgi-bin/process.cgi HTTP/1.1
  3. User-Agent: Mozilla/4.0 (compatible; MSIE5.01; Windows NT)
  4. Host: www.tutorialspoint.com
  5. Content-Type: application/x-www-form-urlencoded
  6. Content-Length: 1000
  7. Accept-Language: en-us
  8. Accept-Encoding: gzip, deflate
  9. Connection: Keep-Alive
  10. licenseID=string&content=string&/paramsXML=string
复制代码

HTTP 协议的响应报文

(1) 报文的第一部门是响应行,内容由协议版本、响应状态码、响应状态码描述构成,回车换行子女表响应行内容结束
内容取值协议版本HTTP/1.0、HTTP/1.1 等状态码2XX(服务器处理正常)、3XX(重定向)、4XX(客户端哀求错误)、5XX(服务器处理错误),具体请参照上面提到的快速指南状态码描述状态码的文字描述 (2) 报文的第二部门是响应头,格式与哀求头雷同,第三部是响应体,内容是服务器返回的数据
  1. #HTTP 响应报文示例
  2. HTTP/1.1 200 OK
  3. Date: Mon, 27 Jul 2009 12:28:53 GMT
  4. Server: Apache/2.2.14 (Win32)
  5. Last-Modified: Wed, 22 Jul 2009 19:15:56 GMT
  6. Content-Length: 88
  7. Content-Type: text/html
  8. Connection: Keep-Alive
  9. <html>
  10. <body>
  11. <h1>Hello, World!</h1>
  12. </body>
  13. </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:
  1. // 导入 Express 模块
  2. const express = require('express')
  3. // 创建 Express 实例
  4. const app = express()
  5. // 指定本程序监听的端口
  6. const port = 8080
  7. // 启动服务器,第一个参数为服务器监听的端口,第二个参数为服务启动成功后的回调函数
  8. app.listen(port, () => {
  9.   console.log(`Express 服务器启动成功,监听的端口为: ${port}`)
  10. })
复制代码
② 下令行工具内运行 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 中追加如下代码即可:
  1. // 监听请求报文中 URL 为 /testGetReq 的 Get 请求
  2. app.get('/testGetReq', (req, res) => {
  3.   // 浏览器的 Get 请求,提交的参数都会拼接在 URL 中,
  4.   // 获取 URL 中参数的方式为 req.query.参数名
  5.   const name = req.query.name
  6.   // 发送响应数据
  7.   res.send('get test success,参数是:' + name)
  8. })
复制代码
使用 Postman 测试 Get 哀求:

(2) 监听 Post 哀求
监听 Post 哀求相对麻烦一点,须要借助 body-parser 模块,运行 npm i body-parser --save 安装 body-parser 模块

在 server.js 中追加如下代码使用 body-parser 解析 Post 哀求中的参数:
  1. // 导入 body-parser 模块
  2. const bodyParser = require('body-parser');
  3. // 解析 Content-Type=application/json 的请求体中的请求信息
  4. app.use(bodyParser.json());
  5. // 解析 Content-Type=text/plain 的请求体中的请求信息
  6. app.use(bodyParser.text());
  7. // 解析 Content-Type=application/x-www-form-urlencoded 的请求体中的请求信息
  8. app.use(bodyParser.urlencoded({
  9.   extended: false
  10. }));
  11. // 监听请求报文中 URL 为 /testPostReq 的 Post 请求
  12. app.post('/testPostReq', (req, res) => {
  13.   // 获取 Post 请求体中的参数
  14.   const name = req.body.name
  15.   // 发送响应数据
  16.   res.send('post test success,参数是:' + name)
  17. })
复制代码
使用 Postman 测试 Post 哀求,Content-type=application/json :

使用 Postman 测试 Post 哀求,Content-type=application/x-www-form-urlencoded :



三、AJAX 异步网络哀求


在 express-server 目录下创建 startup.js,目的是使用 express 搭建一个专门启动 HTML 页面的服务器:
  1. // 导入 Express 模块
  2. const express = require('express')
  3. // 创建 Express 实例
  4. const app = express()
  5. // 指定本程序监听的端口
  6. const port = 8081
  7. // 启动服务器,第一个参数为服务器监听的端口,第二个参数为服务启动成功后的回调函数
  8. app.listen(port, () => {
  9.   console.log(`Express 服务器启动成功,监听的端口为: ${port}`)
  10. })
复制代码
运行 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->4open方法open(Mehod, URL),初始化链接,设置 URL 和 HTTP 方法send方法send(),正式发送 HTTP 哀求getAllResponseHeaders方法getAllResponseHeaders() 获取全部响应头setRequestHeader方法setRequestHeader(key, value),设置响应头, 只能在调用 open() 后使用
XMLHttpRequest 的根本使用
(1) 修改 server.js ,在服务器追加新的监听函数,第一部门代码先不用管,和跨域有关,重点看 app.get 部门:
  1. // 拦截所有请求 判断跨域
  2. app.all("*", function (req, res, next) {
  3.   // 设置允许跨域的域名,*代表允许任意域名跨域
  4.   res.header("Access-Control-Allow-Origin", "*")
  5.   // 允许的header类型
  6.   res.header("Access-Control-Allow-Headers", "*")
  7.   // 跨域允许的请求方式
  8.   res.header("Access-Control-Allow-Methods", "*")
  9.   if (req.method.toLowerCase() == 'options')
  10.     res.send(200) // 让 options 尝试请求快速结束
  11.   else
  12.     next() // 继续路由
  13. })
  14. // 监听请求报文中 URL 为 /getXMLHttpRequest 的 Get 请求
  15. app.get('/getXMLHttpRequest', (req, res) => {
  16.   // AJAX 涉及到跨域问题,这里用来开放同源限制
  17.   res.setHeader('Access-Control-Allow-Origin', '*')
  18.   // 响应格式为文本
  19.   res.send(req.query.name + ' 你好,这是服务器返回的消息')
  20. })
复制代码

(2) 在 express-server 目录创建用来发送哀求的 HTML 页面 request.html:
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4.   <meta charset="UTF-8">
  5.   <meta http-equiv="X-UA-Compatible" content="IE=edge">
  6.   <meta name="viewport" content="width=device-width, initial-scale=1.0">
  7.   <title>Document</title>
  8.   <style>
  9.     div {
  10.       width: 200px;
  11.       height: 200px;
  12.       border: 1px solid black;
  13.     }
  14.   </style>
  15. </head>
  16. <body>
  17.   <form>
  18.     <input type="button" value="点击展示内容">
  19.   </form>
  20.   <div></div>
  21.   <script>
  22.     // 添加按钮点击事件
  23.     document.querySelector('input').addEventListener('click', () => {
  24.       // 第一步:创建 XMLHttpRequest 对象
  25.       const request = new XMLHttpRequest()
  26.       // 第二步:设置请求方式和URL地址
  27.       request.open('GET', 'http://127.0.0.1:8080/getXMLHttpRequest?name=ares5k')
  28.       // 第三步:设置请求状态(readyState)发生变化时的回调
  29.       request.onreadystatechange = () => {
  30.         // 请求状态为已经接收到响应体
  31.         if (request.readyState === 4) {
  32.           // 响应状态码在 200 ~ 300 之间代表服务器响应成功
  33.           if (request.status >= 200 && request.status < 300) {
  34.             // 将服务器返回数据渲染到DIV中
  35.             document.querySelector('div').innerText = request.response
  36.           }
  37.         }
  38.       }
  39.       // 第四步:发送请求
  40.       request.send()
  41.     })
  42.   </script>
  43. </body>
  44. </html>
复制代码

(3) 修改 startup.js 追加返回 request.html 的处理,留意响应平凡数据使用的是 res.send,响应文件使用 res.sendFile:
  1. // 响应页面
  2. app.get('/startupRequest', (req, res)=>{
  3.   res.sendFile(__dirname + '/request.html')
  4. })
复制代码

(4) 欣赏器访问 http://127.0.0.1:8081/startupRequest,以服务器的方式运行页面测试:



XMLHttpRequest 发送 json 数据


(1) 修改 server.js ,在服务器追加新的监听函数:
  1. // 监听请求报文中 URL 为 /postXMLHttpRequest 的 Post 请求
  2. app.post('/postXMLHttpRequest', (req, res) => {
  3.   // 因为请求头中设置了 Content-type,所以能自动将Json字符串转换成对象
  4.   // Json 对象:    {name: 'ares5k'}
  5.   // Json 字符串: "{name: 'ares5k'}"
  6.   let obj = req.body
  7.   // 响应格式为文本
  8.   res.send(JSON.stringify({
  9.     name: obj.name,
  10.     content: '你好,这是服务器返回的消息'
  11.   }))
  12. })
复制代码

(2) 修改 request.html:
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4.   <meta charset="UTF-8">
  5.   <meta http-equiv="X-UA-Compatible" content="IE=edge">
  6.   <meta name="viewport" content="width=device-width, initial-scale=1.0">
  7.   <meta http-equiv="Access-Control-Allow-Origin" content="*">
  8.   <title>Document</title>
  9.   <style>
  10.     div {
  11.       width: 200px;
  12.       height: 200px;
  13.       border: 1px solid black;
  14.     }
  15.   </style>
  16. </head>
  17. <body>
  18.   <form>
  19.     <label for="name">请输入姓名</label><input name="name" type="text" value="">
  20.     <input type="button" value="点击展示内容">
  21.   </form>
  22.   <div></div>
  23.   <script>
  24.     // 添加按钮点击事件
  25.     document.querySelector('input[type=button]').addEventListener('click', () => {
  26.       // 第一步:创建 XMLHttpRequest 对象
  27.       const request = new XMLHttpRequest()
  28.       // 第二步:设置请求方式和URL地址
  29.       request.open('POST', 'http://127.0.0.1:8080/postXMLHttpRequest')
  30.       // 设置头信息为Json格式后,服务端就会自动把Json字符串变为Json对象
  31.       // Json 对象:    {name: 'ares5k'}
  32.       // Json 字符串: "{name: 'ares5k'}"
  33.       request.setRequestHeader('Content-Type', 'application/json')
  34.       // 设置响应格式为Json后,会自动把服务端返回的Json字符串变为Json对象
  35.       // Json 对象:    {name: 'ares5k'}
  36.       // Json 字符串: "{name: 'ares5k'}"
  37.       request.responseType = 'json'
  38.       // 第三步:设置请求状态(readyState)发生变化时的回调
  39.       request.onreadystatechange = () => {
  40.         // 请求状态为已经接收到响应体
  41.         if (request.readyState === 4) {
  42.           // 响应状态码在 200 ~ 300 之间代表服务器响应成功
  43.           if (request.status >= 200 && request.status < 300) {
  44.             // 自动将Json字符串转为对象
  45.             let obj = request.response
  46.             // 将服务器返回数据渲染到DIV中
  47.             document.querySelector('div').innerText = obj.name + ' ' + obj.content
  48.           }
  49.         }
  50.       }
  51.       // 第四步:发送请求
  52.       request.send(JSON.stringify({
  53.         name: document.querySelector('input[type=text]').value
  54.       }))
  55.     })
  56.   </script>
  57. </body>
  58. </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&param2=value2 的格式,然后拼接到 XMLHttpRequest.open
的 URL 后就可以

发送 POST 哀求时,有三种常见的情况:
(1) 以 content-type: application/x-www-form-urlencoded 格式发送数据时,可以将 form 中的数据拼成 param1=value1&param2=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


[size=”3“](1) jQuery 发送 GET 或 POST 哀求
使用 jQuery 核心对象的 get 或 post 函数即可,其常见的参数列表如下:
参数名描述url服务器 URLdata可选,哀求参数列表, 两种格式 param1:value1&param2:value2 或 {param1:value1, param2:value2} 都可以callback可选,乐成时的回调函数,有3个参数,data: 响应体,status: 哀求状态(success 等),xhr: 哀求使用的 XMLHttpRequest 对象responseType可选,预期的返回类型,jQuery 会根据我们的预期类型对响应体举行转换,如 json
(1) 在 express-server 下创建 jQueryRequest.html 文件,使用 jQuery 向我们之前的服务 getXMLHttpRequest 发送哀求:
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4.   <meta charset="UTF-8">
  5.   <meta http-equiv="X-UA-Compatible" content="IE=edge">
  6.   <meta name="viewport" content="width=device-width, initial-scale=1.0">
  7.   <meta http-equiv="Access-Control-Allow-Origin" content="*">
  8.   <title>Document</title>
  9.   <style>
  10.     div {
  11.       width: 200px;
  12.       height: 200px;
  13.       border: 1px solid black;
  14.     }
  15.   </style>
  16. </head>
  17. <body>
  18.   <form>
  19.     <input type="button" value="点击展示内容">
  20.   </form>
  21.   <div></div>
  22.   <!-- 引入 jQuery CDN -->
  23.   <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.js"></script>
  24.   <script>
  25.     // 为按钮绑定点击事件
  26.     $('input[type=button]').on('click', () => {
  27.       // 发送请求
  28.       $.get('http://127.0.0.1:8080/getXMLHttpRequest', // 服务器 URL
  29.         'name=ares5k', // 请求参数, 用这种格式也可以 {name: 'ares5k'}
  30.         (data, status, xhr) => {$('div').text(data)}) // 回调函数
  31.     })
  32.   </script>
  33. </body>
  34. </html>
复制代码

(2) 修改 startup.js 追加返回 jQueryRequest.html 的处理:
  1. // 响应页面
  2. app.get('/startupRequestWithJQuery', (req, res)=>{
  3.   res.sendFile(__dirname + '/jQueryRequest.html')
  4. })
复制代码

(3) 欣赏器访问 http://127.0.0.1:8081/startupRequestWithJQuery,以服务器的方式运行页面测试:



[size=”3“](2) jQuery 发送自定义哀求
想对哀求更详细的设置,我们可以使用 jQuery 核心对象的 ajax 函数,其参数为对象布局,该对象常见的属性如下:

属性名描述url服务器 URLtype哀求方法, GET、POST、PUTtimeout可选,哀求延伸时间,毫秒单位headers可选,哀求头,格式为 {key1:value1, key2:value2}data可选,哀求参数列表, 两种格式 param1:value1&param2:value2 或 {param1:value1, param2:value2} 都可以dataType可选,预期的返回类型,jQuery 会根据我们的预期类型对响应体举行转换,如 application/jsonsuccess可选,乐成时的回调函数,有3个参数,data: 响应体,status: success 字符串,xhr: 哀求使用的 XMLHttpRequest 对象error可选, 超时或发生错误时的回调函数, 有3个参数, xhr: 哀求使用的 XMLHttpRequest 对象, status: error 字符串, statusText: 错误信息
(1) 修改 jQueryRequest.html 文件,向我们之前的服务 postXMLHttpRequest 发送哀求:
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4.   <meta charset="UTF-8">
  5.   <meta http-equiv="X-UA-Compatible" content="IE=edge">
  6.   <meta name="viewport" content="width=device-width, initial-scale=1.0">
  7.   <meta http-equiv="Access-Control-Allow-Origin" content="*">
  8.   <title>Document</title>
  9.   <style>
  10.     div {
  11.       width: 200px;
  12.       height: 200px;
  13.       border: 1px solid black;
  14.     }
  15.   </style>
  16. </head>
  17. <body>
  18.   <form>
  19.     <input type="button" value="点击展示内容">
  20.   </form>
  21.   <div></div>
  22.   <!-- 引入 jQuery CDN -->
  23.   <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.js"></script>
  24.   <script>
  25.     // 为按钮绑定点击事件
  26.     $('input[type=button]').on('click', () => {
  27.       // 发送请求
  28.       $.ajax({
  29.         url: 'http://127.0.0.1:8080/postXMLHttpRequest', // 服务器 URL
  30.         type: 'POST', // 请求方法
  31.         data: JSON.stringify({
  32.           name: 'ares5k'
  33.         }), // 请求参数, 用这种格式也可以 'name=ares5k'
  34.         timeout: 2000, // 超时时间
  35.         dataType: 'json', // 预计响应体格式
  36.         contentType: 'application/json', // 请求参数格式
  37.         success: (data) => {
  38.           // 请求成功的回调
  39.           $('div').text(data.name + ' ' + data.content)
  40.         }
  41.       })
  42.     })
  43.   </script>
  44. </body>
  45. </html>
复制代码

(2) 欣赏器访问 http://127.0.0.1:8081/startupRequestWithJQuery,以服务器的方式运行页面测试:


[size=”3“](3) jQuery 快速格式化 Form 数据的方法
将 Form 内容拼装成 param1:value1&param2:value2 格式很麻烦, jQuery 提供了函数简化这类利用 :
  1.   const data = $('form').serialize() // 将 form 表单中的内容格式化成 param1:value1&param2:value2 的格式
  2.   const data = $('form').serializeArray() // 将 form 表单中的内容格式化成 [{param1:value1}, {param2:value2}] 的数组格式
  3.   $.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 参数:

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4.   <meta charset="UTF-8">
  5.   <meta http-equiv="X-UA-Compatible" content="IE=edge">
  6.   <meta name="viewport" content="width=device-width, initial-scale=1.0">
  7.   <meta http-equiv="Access-Control-Allow-Origin" content="*">
  8.   <title>Document</title>
  9.   <style>
  10.     div {
  11.       width: 200px;
  12.       height: 200px;
  13.       border: 1px solid black;
  14.     }
  15.   </style>
  16. </head>
  17. <body>
  18.   <!-- GET 请求部分 -->
  19.   <form>
  20.     Get请求:
  21.     <input type="button" value="点击展示内容">
  22.   </form>
  23.   <div></div>
  24.   <br/>
  25.   <!-- POST 请求部分 -->
  26.   <form>
  27.     Post请求:
  28.     <input type="button" value="点击展示内容">
  29.   </form>
  30.   <div></div>
  31.   <script>
  32.     // GET 请求部分
  33.     // 添加按钮单击事件
  34.     document.querySelector('form:first-of-type input').addEventListener('click', () => {
  35.       // 利用 fetch 发送 ajax 请求
  36.       fetch(
  37.         // URL   
  38.         'http://127.0.0.1:8080/getXMLHttpRequest?name=ares5k', {
  39.           // 请求方法
  40.           method: 'GET'
  41.         }).then((response) => {
  42.         // 从响应信息中读取响应体信息,response.text() 会返回 Promise 对象
  43.         return response.text()
  44.       }).then(data => {
  45.         // 填充元素内容
  46.         document.querySelector('div:first-of-type').innerText = data
  47.       })
  48.     })
  49.    
  50.     // POST 请求部分
  51.     // 添加按钮单击事件
  52.     document.querySelector('form:last-of-type input').addEventListener('click', () => {
  53.       // 利用 fetch 发送 ajax 请求
  54.       fetch(
  55.         // URL  
  56.         'http://127.0.0.1:8080/postXMLHttpRequest', {
  57.           // 请求方法
  58.           method: 'POST',
  59.           // 请求头
  60.           headers: {
  61.             'content-type': 'application/json'
  62.           },
  63.           // 请求体
  64.           body: JSON.stringify({
  65.             name: 'ares5k'
  66.           })
  67.         }).then((response) => {
  68.         // 从响应信息中以 JSON 格式读取响应体并转为对象,response.json() 会返回 Promise 对象
  69.         return response.json()
  70.       }).then(data => {
  71.         // 填充元素内容
  72.         document.querySelector('div:last-of-type').innerText = data.name + ' ' + data.content
  73.       })
  74.     })
  75.   </script>
  76. </body>
  77. </html>
复制代码

(2) 修改 startup.js 追加返回 fetchRequest.html 的处理:
  1. // 响应页面
  2. app.get('/startupRequestWithFetch', (req, res)=>{
  3.   res.sendFile(__dirname + '/fetchRequest.html')
  4. })
复制代码

(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 向我们之前的服务发送哀求:
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4.   <meta charset="UTF-8">
  5.   <meta http-equiv="X-UA-Compatible" content="IE=edge">
  6.   <meta name="viewport" content="width=device-width, initial-scale=1.0">
  7.   <meta http-equiv="Access-Control-Allow-Origin" content="*">
  8.   <title>Document</title>
  9.   <style>
  10.     div {
  11.       width: 200px;
  12.       height: 200px;
  13.       border: 1px solid black;
  14.     }
  15.   </style>
  16. </head>
  17. <body>
  18.   <!-- Get 请求 -->
  19.   <form>
  20.     Get请求:
  21.     <input type="button" value="点击展示内容">
  22.   </form>
  23.   <div></div>
  24.   <br />
  25.   <!-- Post 请求 -->
  26.   <form>
  27.     Post请求:
  28.     <input type="button" value="点击展示内容">
  29.   </form>
  30.   <div></div>
  31.   <br />
  32.   <!-- 自定义请求 -->
  33.   <form>
  34.     自定义发送方式:
  35.     <input type="button" value="点击展示内容">
  36.   </form>
  37.   <div></div>
  38.   <!-- 导入 axios 库 -->
  39.   <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
  40.   <script>
  41.     // 添加按钮单击事件
  42.     document.querySelector('form:first-of-type input').addEventListener('click', () => {
  43.       // 用 axios 发送 get 请求
  44.       axios.get(
  45.           'http://127.0.0.1:8080/getXMLHttpRequest', // 服务器 URL
  46.           {
  47.             // URL 参数
  48.             params: {
  49.               name: 'ares5k'
  50.             }
  51.           })
  52.         .then(value => {
  53.           // 填充元素
  54.           document.querySelector('div:first-of-type').innerText = value.data
  55.         })
  56.     })
  57.     // 添加按钮单击事件
  58.     document.querySelector('form:nth-of-type(2) input').addEventListener('click', () => {
  59.       // 用 axios 发送 post 请求
  60.       axios.post(
  61.           'http://127.0.0.1:8080/postXMLHttpRequest', // 服务器 URL
  62.           // 请求体参数
  63.           {
  64.             name: 'ares5k'
  65.           })
  66.         .then(value => {
  67.           // 填充元素
  68.           document.querySelector('div:nth-of-type(2)').innerText = value.data.name + ' ' + value.data.content
  69.         })
  70.     })
  71.     // 添加按钮单击事件
  72.     document.querySelector('form:nth-of-type(3) input').addEventListener('click', () => {
  73.       // 用 axios 自定义发送请求
  74.       axios({
  75.         url: 'http://127.0.0.1:8080/postXMLHttpRequest', // 服务器 URL
  76.         method: 'POST', // 请求方式
  77.         // 请求体参数
  78.         data: {
  79.           name: 'ares5k'
  80.         }
  81.       }).then(value => {
  82.         // 填充元素
  83.         document.querySelector('div:nth-of-type(3)').innerText = value.data.name + ' ' + value.data.content
  84.       })
  85.     })
  86.   </script>
  87. </body>
  88. </html>
复制代码

(2) 修改 startup.js 追加返回 axiosRequest.html 的处理:
  1. // 响应页面
  2. app.get('/startupRequestWithAxios', (req, res)=>{
  3.   res.sendFile(__dirname + '/axiosRequest.html')
  4. })
复制代码

(3) 欣赏器访问 http://127.0.0.1:8081/startupRequestWithAxios,以服务器的方式运行页面测试:



5. 异步网络哀求与传统网络哀求的区别


1. 传统网络哀求演示
(1) 在 express-server 目录内创建哀求用的 HTML 文件,定名为 oldReq.html:
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4.   <meta charset="UTF-8">
  5.   <meta http-equiv="X-UA-Compatible" content="IE=edge">
  6.   <meta name="viewport" content="width=device-width, initial-scale=1.0">
  7.   <title>Document</title>
  8.   <style>
  9.     div {
  10.       width: 200px;
  11.       height: 200px;
  12.       border: 1px solid black;
  13.     }
  14.   </style>
  15. </head>
  16. <body>
  17.   <form action="http://127.0.0.1:8080/getContent">
  18.     <input type="submit" value="点击展示内容">
  19.   </form>
  20.   <div></div>
  21. </body>
  22. </html>
复制代码

(2) 在 express-server 目录内创建响应用的 HTML 文件,定名为 oldRes.html:
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4.   <meta charset="UTF-8">
  5.   <meta http-equiv="X-UA-Compatible" content="IE=edge">
  6.   <meta name="viewport" content="width=device-width, initial-scale=1.0">
  7.   <title>Document</title>
  8.   <style>
  9.     div {
  10.       width: 200px;
  11.       height: 200px;
  12.       border: 1px solid black;
  13.     }
  14.   </style>
  15. </head>
  16. <body>
  17.   <form action="http://127.0.0.1:8080/getContent">
  18.     <input type="submit" value="点击展示内容">
  19.   </form>
  20.   <div>这是服务器返回的数据</div>
  21. </body>
  22. </html>
复制代码

(3) 修改 server.js 文件,追加哀求处理逻辑:
  1. // 响应页面
  2. app.get('/getContent', (req, res) => {
  3.   res.sendFile(__dirname + '/oldRes.html')
  4. })
复制代码
(4) 修改 startup.js 文件,追加初始页面:
  1. // 响应页面
  2. app.get('/startupRequestOldReq', (req, res) => {
  3.   res.sendFile(__dirname + '/oldReq.html')
  4. })
复制代码

(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:
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4.   <meta charset="UTF-8">
  5.   <meta http-equiv="X-UA-Compatible" content="IE=edge">
  6.   <meta name="viewport" content="width=device-width, initial-scale=1.0">
  7.   <meta http-equiv="Access-Control-Allow-Origin" content="*">
  8.   <title>Document</title>
  9.   <style>
  10.     div {
  11.       width: 200px;
  12.       height: 200px;
  13.       border: 1px solid black;
  14.     }
  15.   </style>
  16. </head>
  17. <body>
  18.   <form>
  19.     <input type="button" value="点击展示内容">
  20.   </form>
  21.   <div></div>
  22.   <script>
  23.     // 添加按钮点击事件
  24.     document.querySelector('input').addEventListener('click', () => {
  25.       // 创建 script 标签
  26.       let script = document.createElement('script')
  27.       // 让 script 标签的 src 属性指向服务器,并将服务器响应信息的处理函数名告知服务器
  28.       script.src = 'http://127.0.0.1:8080/jsonp?name=ares5k&callback=callbackMethod'
  29.       // 将 script 标签渲染到画面,同时触发 src 下载
  30.       document.querySelector('body').appendChild(script)
  31.     })
  32.     // 服务器响应信息的处理函数
  33.     function callbackMethod(json){
  34.       document.querySelector('div').innerText = json.name + ' ' +json.content
  35.     }
  36.   </script>
  37. </body>
  38. </html>
复制代码

(2) 修改 server.js,追加哀求处理函数,并把 server.js 中,以前添加的跨域部门代码删除:
  1. // 拦截所有请求 判断跨域
  2. // app.all("*", function (req, res, next) {
  3. //   // 设置允许跨域的域名,*代表允许任意域名跨域
  4. //   res.header("Access-Control-Allow-Origin", "*")
  5. //   // 允许的header类型
  6. //   res.header("Access-Control-Allow-Headers", "*")
  7. //   // 跨域允许的请求方式
  8. //   res.header("Access-Control-Allow-Methods", "*")
  9. //   if (req.method.toLowerCase() == 'options')
  10. //     res.send(200) // 让 options 尝试请求快速结束
  11. //   else
  12. //     next() // 继续路由
  13. // })
  14. // 监听请求报文中 URL 为 /postXMLHttpRequest 的 Post 请求
  15. app.get('/jsonp', (req, res) => {
  16.   // 参数
  17.   let name = req.query.name
  18.   // 服务器响应后要调用的方法名
  19.   let callbackMethod = req.query.callback
  20.   // 服务器真正要响应的数据
  21.   const json = {
  22.     name: name,
  23.     content: '你好,这是服务器返回的消息'
  24.   }
  25.   // 将真正要响应的数据填充到方法名中
  26.   res.send(`${callbackMethod}(${JSON.stringify(json)})`)
  27. })
复制代码
(3) 修改 startup.js 追加返回 jsonpRequest.html 的处理:
  1. // 响应页面
  2. app.get('/startupRequestJsonp', (req, res) => {
  3.   res.sendFile(__dirname + '/jsonpRequest.html')
  4. })
复制代码

(4) 欣赏器访问 http://127.0.0.1:8081/startupRequestJsonp,以服务器的方式运行页面测试:



使用 jQuery 库实现 JSONP
如果手动写 JSONP 嫌麻烦,也可以使用 jQuery 库实现,将 jsonpRequest.html 的 <script> 做如下修改即可:
  1.   <script>
  2.     // 添加按钮点击事件
  3.     document.querySelector('input').addEventListener('click', () => {
  4.       $.getScript('http://127.0.0.1:8080/jsonp?name=ares5k&=callbackMethod')
  5.     })
  6.     // 服务器响应信息的处理函数
  7.     function callbackMethod(json){
  8.       document.querySelector('div').innerText = json.name + ' ' +json.content
  9.     }
  10.   </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,发送简单哀求,访问我们之前的业务处理函数
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4.   <meta charset="UTF-8">
  5.   <meta http-equiv="X-UA-Compatible" content="IE=edge">
  6.   <meta name="viewport" content="width=device-width, initial-scale=1.0">
  7.   <title>Document</title>
  8.   <style>
  9.     div {
  10.       width: 200px;
  11.       height: 200px;
  12.       border: 1px solid black;
  13.     }
  14.   </style>
  15. </head>
  16. <body>
  17.   <form>
  18.     <input type="button" value="点击展示内容">
  19.   </form>
  20.   <div></div>
  21.   <script>
  22.     // 添加按钮点击事件
  23.     document.querySelector('input').addEventListener('click', () => {
  24.       // 第一步:创建 XMLHttpRequest 对象
  25.       const request = new XMLHttpRequest()
  26.       // 第二步:设置请求方式和URL地址
  27.       request.open('GET', 'http://127.0.0.1:8080/getXMLHttpRequest?name=ares5k')
  28.       // 第三步:设置请求状态(readyState)发生变化时的回调
  29.       request.onreadystatechange = () => {
  30.         // 请求状态为已经接收到响应体
  31.         if (request.readyState === 4) {
  32.           // 响应状态码在 200 ~ 300 之间代表服务器响应成功
  33.           if (request.status >= 200 && request.status < 300) {
  34.             // 将服务器返回数据渲染到DIV中
  35.             document.querySelector('div').innerText = request.response
  36.           }
  37.         }
  38.       }
  39.       // 第四步:发送请求
  40.       request.send()
  41.     })
  42.   </script>
  43. </body>
  44. </html>
复制代码

(2) 修改 server.js 中 /getXMLHttpRequest 对应的业务代码,添加 log 打印,留意把以前跨域相关代码删除:
  1. // 监听请求报文中 URL 为 /getXMLHttpRequest 的 Get 请求
  2. app.get('/getXMLHttpRequest', (req, res) => {
  3.   console.log('简单请求时,虽然 CORS 不能通过,但是业务也执行了')
  4.   // 响应格式为文本
  5.   res.send(req.query.name + ' 你好,这是服务器返回的消息')
  6. })
复制代码

(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,发送复杂哀求,访问我们之前的业务处理函数:
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4.   <meta charset="UTF-8">
  5.   <meta http-equiv="X-UA-Compatible" content="IE=edge">
  6.   <meta name="viewport" content="width=device-width, initial-scale=1.0">
  7.   <meta http-equiv="Access-Control-Allow-Origin" content="*">
  8.   <title>Document</title>
  9.   <style>
  10.     div {
  11.       width: 200px;
  12.       height: 200px;
  13.       border: 1px solid black;
  14.     }
  15.   </style>
  16. </head>
  17. <body>
  18.   <form>
  19.     <label for="name">请输入姓名</label><input name="name" type="text" value="">
  20.     <input type="button" value="点击展示内容">
  21.   </form>
  22.   <div></div>
  23.   <script>
  24.     // 添加按钮点击事件
  25.     document.querySelector('input[type=button]').addEventListener('click', () => {
  26.       // 第一步:创建 XMLHttpRequest 对象
  27.       const request = new XMLHttpRequest()
  28.       // 第二步:设置请求方式和URL地址
  29.       request.open('POST', 'http://127.0.0.1:8080/postXMLHttpRequest')
  30.       // 设置头信息为Json格式后,服务端就会自动把Json字符串变为Json对象
  31.       // Json 对象:    {name: 'ares5k'}
  32.       // Json 字符串: "{name: 'ares5k'}"
  33.       request.setRequestHeader('Content-Type', 'application/json')
  34.       // 设置响应格式为Json后,会自动把服务端返回的Json字符串变为Json对象
  35.       // Json 对象:    {name: 'ares5k'}
  36.       // Json 字符串: "{name: 'ares5k'}"
  37.       request.responseType = 'json'
  38.       // 第三步:设置请求状态(readyState)发生变化时的回调
  39.       request.onreadystatechange = () => {
  40.         // 请求状态为已经接收到响应体
  41.         if (request.readyState === 4) {
  42.           // 响应状态码在 200 ~ 300 之间代表服务器响应成功
  43.           if (request.status >= 200 && request.status < 300) {
  44.             // 自动将Json字符串转为对象
  45.             let obj = request.response
  46.             // 将服务器返回数据渲染到DIV中
  47.             document.querySelector('div').innerText = obj.name + ' ' + obj.content
  48.           }
  49.         }
  50.       }
  51.       // 第四步:发送请求
  52.       request.send(JSON.stringify({
  53.         name: document.querySelector('input[type=text]').value
  54.       }))
  55.     })
  56.   </script>
  57. </body>
  58. </html>
复制代码

(2) 修改 server.js,将之前注释掉的跨域代码规复:
  1. // 拦截所有请求 判断跨域
  2. app.all("*", function (req, res, next) {
  3.   // 设置允许跨域的域名,*代表允许任意域名跨域
  4.   res.header("Access-Control-Allow-Origin", "*")
  5.   // 允许的header类型
  6.   res.header("Access-Control-Allow-Headers", "*")
  7.   // 跨域允许的请求方式
  8.   res.header("Access-Control-Allow-Methods", "*")
  9.   if (req.method.toLowerCase() == 'options')
  10.     res.send(200) // 让 options 尝试请求快速结束
  11.   else
  12.     next() // 继续路由
  13. })
复制代码
(3) 欣赏器访问 http://127.0.0.1:8081/startupRequest,以服务器的方式运行页面测试:

上面动图中可以清晰的看到,先以 OPTIONS 发送了一次预检哀求( Preflight ) ,之后又发送了我们真正的 POST 哀求

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

农民

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表