HTTP 协议、AJAX - 异步网络请求及跨域、同源策略


一、HTTP 协议简述


1. 协议的基本理解


协议

协议这个词很好理解,既约定的规则,举个简单例子,公司要求员工上班打卡才能计算工时,这就是一种协议,约束了员工的行为,如果员工不遵守该协议,就不能计算工时,只能算作缺勤

在计算机网络中协议也是同样的作用,只不过更多的是用来定义数据传输时的编解码的,数据只能以二进制的形式进行传输,所以数据发送方需要将原本的数据格式,以一套规则编码成二进制,然后数据接收方再用相同的规则将二进制解码成原有的数据格式,这个双方约定好的规则就是协议

OSI 网络模型

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 还是其他方法,都可以有请求体

#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管理项目

② 命令行工具内继续运行命令 npm install express --save 安装 Express 服务器

安装Experss

(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 服务器程序

启动Express

(3) 安装热部署插件 nodemon

每次修改 server.js文件 或 其引用的文件后,都必须重新运行 node server.js 命令,才能将修改后的内容更新到 Express 服务器中,非常麻烦。我们可以通过安装 nodemon 模块,来解决这个问题, 当我们修改 server.js 或 其引用的文件后,nodemon 会自动重启 Express 服务器并加载我们最新的代码

安装 nodemon 模块包,命令行工具中运行 npm install nodemon -g 命令:

安装nodemon

node server.js 启动命令改为 nodemon server.js 命令来启动项目,之后每次修改保存都会重启 Express 服务器:

nodemon自动更新

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 请求:

测试GET请求

(2) 监听 Post 请求

监听 Post 请求相对麻烦一点,需要借助 body-parser 模块,运行 npm i body-parser --save 安装 body-parser 模块

安装bodyparser模块

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

Post请求Json格式

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

Post请求Form格式

三、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的基本使用

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,以服务器的方式运行页面测试:

XMLHttpRequest发送Json


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

(1) jQuery 发送 GET 或 POST 请求

利用 jQuery 核心对象的 getpost 函数即可,其常见的参数列表如下:

参数名描述
url服务器 URL
data可选,请求参数列表, 两种格式 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 发送请求:

<!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,以服务器的方式运行页面测试:

jQuery发送Get或Post请求

(2) jQuery 发送自定义请求

想对请求更详细的设置,我们可以使用 jQuery 核心对象的 ajax 函数,其参数为对象结构,该对象常见的属性如下:

属性名描述
url服务器 URL
type请求方法, GET、POST、PUT
timeout可选,请求延迟时间,毫秒单位
headers可选,请求头,格式为 {key1:value1, key2:value2}
data可选,请求参数列表, 两种格式 param1:value1&param2: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,以服务器的方式运行页面测试:

jQuery的ajax请求


(3) jQuery 快速格式化 Form 数据的方法

将 Form 内容拼装成 param1:value1&param2:value2 格式很麻烦, jQuery 提供了函数简化这类操作 :

  const data = $('form').serialize() // 将 form 表单中的内容格式化成 param1:value1&param2: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,以服务器的方式运行页面测试:

fetch请求


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,以服务器的方式运行页面测试:

axios发送请求

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流程图

上图中 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,以服务器的方式运行页面测试:

jsonp运行效果

利用 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不通过也能执行业务代码

上面动图可以看到,请求后确实没有得到响应体信息,而且浏览器控制台也报出了 CORS 不通过的错误信息,那么我们再看看服务器的控制台有没有打印我们加在业务逻辑中的 log:

执行业务逻辑了

log 信息已经被打印出来,说明业务逻辑确实执行了,不受 CORS 影响

简单请求的场景下,预防恶意数据攻击

CORS 在简单请求场景下的安全问题, 完全是由浏览器来决定,其根据响应头来决定是否保留响应体,但这并不能保证我们的系统就一定是安全的,因为在简单请求的场景下,我们的业务代码一定会被执行,这就可能导致被恶意数据攻击

为了避免这种情况,建议服务器端除了添加 CORS 响应头,让浏览器保证安全的基础上,服务器端还应该自己写一个拦截器,当请求到达时,我们先判断是否可以跨域,如果不允许跨域,我们就提前返回,而不是让程序继续执行我们的业务代码,Spring MVC 框架的 CorsFilter 的实现原理就是如此

2. 复杂请求


复杂请求的流程就是多了一步预检 ( Preflight ) :

  • 服务器预先定义好响应时要添加的 CORS 响应头信息 Access-Control-Allow-OriginAccess-Control-Allow-MethodsAccess-Control-Allow-HeadersAccess-Control-Max-Age (这些响应头都是按需添加)
  • 浏览器先发送一个请求方法为 OPTIONS 的请求,并自动添加请求头 OriginAccess-Control-Request-Method Access-Control-Request-Headers(后两个请求头浏览器会按需添加)
  • 服务器的接口一般不会设置请求方法为 OPTIONS 对应的业务逻辑,一般来说服务器与 OPTIONS 对应的处理就是直接返回,响应返回前,将预先定义好的 CORS 响应头添加到响应报文中
  • 浏览器判断响应头中的 Access-Control-Allow-OriginAccess-Control-Allow-MethodsAccess-Control-Allow-Headers 是否能与请求头中的 OriginAccess-Control-Request-Method Access-Control-Request-Headers 匹配,匹配成功后,代表允许跨域,会再次发送请求(我们真正的请求),并执行后面步骤,无法匹配时不会再发送请求,而会报 CORS 不通过的错误信息
  • 浏览器认证通过后,发送真正的 AJAX 请求,并自动添加请求头 OriginAccess-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 请求

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值
>