一、AJAX 简介
AJAX(Asynchronous JavaScript And XML),异步的 JavaScript 和 XML。通过 AJAX 可以实现在浏览器中向服务器发送异步请求获取数据,从而不用刷新页面就可以进行页面的局部更新。
尽管 X 在 Ajax 中代表 XML, 但由于 JSON 的许多优势,比如更加轻量以及作为 Javascript 的一部分,目前 JSON 的使用比 XML 更加普遍。
AJAX 的优点:
- 无需刷新页面,就可以与服务器端进行通信
- 可以根据用户行为来更新部分页面内容
AJAX 的缺点:
- 没有浏览历史,不能回退(因为 AJAX 是局部刷新页面)
- 存在跨域问题(因为 AJAX 默认遵循浏览器的同源策略)
- SEO 不友好(因为页面数据都是从服务端获取的,源代码中没有数据信息,爬虫爬不到)
二、HTTP 请求与响应的结构
以下请求报文和响应报文是从百度页面复制来的,为了便于观察和美观,内容做了删除、替换、简化等操作。
1. 请求报文的结构
请求行 GET /xxx?a=1&b=2 HTTP/1.1 --> 请求方法 请求的url 协议/版本
请求头 Host: www.baidu.com
Connection: keep-alive
Accept: application/json, text/javascript, */*; q=0.01
User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64)
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Cookie: xxxxxx
空行
请求体 a=1&b=2
2. 响应报文的结构
响应行 HTTP/1.1 200 OK --> 协议/版本 状态码 状态描述
响应头 Content-Length: 53
Content-Type: text/plain; charset=UTF-8
Date: Sat, 16 Apr 2022 01:38:01 GMT
空行
响应体 {"a": "100", "b": "200"} --> 响应体可以是 html文本、json、js、css、图片等
三、原生 AJAX 请求
1. 基本使用
const xhr = new XMLHttpRequest() // 创建对象
xhr.open('GET', 'http://xxx') // 设置请求方法和 url
xhr.send() // 发送
xhr.onreadystatechange = function () { // 绑定事件,处理服务端返回的结果
if (xhr.readystate === 4) { // 下载完成
if (xhr.status >= 200 && xhr.status < 300 || xhr.status === 304) { // 判断响应状态码
console.log(xhr.response) // 响应体
}
}
}
MDN 关于 XMLHttpRequest.readyState 的描述:
| 值 | 状态 | 描述 |
|---|---|---|
0 | UNSENT | 代理被创建,但尚未调用 open() 方法。 |
1 | OPENED | open() 方法已经被调用。 |
2 | HEADERS_RECEIVED | send() 方法已经被调用,并且头部和状态已经可获得。 |
3 | LOADING | 下载中; responseText 属性已经包含部分数据。 |
4 | DONE | 下载操作已完成。 |
2. 设置请求头
xhr.setRequestHeader('Content-Type': 'application/x-www-form-urlencoded')
// 可以设置自定义的请求头,但如果服务器端没有做相应配置,请求会报错
xhr.setRequestHeader('name': 'dongdong')
3. 设置请求体(参数)
// GET 请求
xhr.open('GET', 'http://xxx?a=100&b=200')
// POST 请求
xhr.open('POST', 'http://xxx')
xhr.send('a=100&b=200')
4. 服务器响应 JSON 数据
当服务器返回 JSON 格式的数据,可以有两种方式将 JSON 字符串转成正常对象:
-
使用
JSON.parse()if (xhr.readystatechange === 4) { if (xhr.status >= 200 && xhr.status < 300 || xhr.status === 304) { const result = JSON.parse(xhr.response) console.log(result) } } -
设置响应体数据类型:
xhr.responseType = 'json'xhr.responseType = 'json' if (xhr.readystatechange === 4) { if (xhr.status >= 200 && xhr.status < 300 || xhr.status === 304) { console.log(xhr.response) } }
5. IE 缓存问题
IE 浏览器会对 AJAX 请求结果做缓存,下次再发送相同请求时,获取的是本地的缓存,而不是服务器返回的最新数据。
解决方案:加时间戳,浏览器会认为这是个新的请求,就不会走缓存。
xhr.open('GET', `http://xxx?t=${Date.now()}`)
6. 请求超时与网络异常
// 如果请求超过 3s 还没有返回结果,请求会自动取消
xhr.timeout = 3000
xhr.ontimeout = function () {
alert('请求超时!')
}
xhr.onerror = function () {
alert('网络异常!')
}
7. 取消请求
const xhr = new XMLHttpRequest()
btn.onclick = function () {
xhr.abort() // 取消请求
}
8. 重复发送请求
场景:当用户点击按钮发送请求,服务器还没有返回响应时,用户再次重复多次点击,可能会导致相同的请求重复发送,给服务器造成不必要的压力。
解决方案:可以用一个变量来标识是否正在发送请求,如果是,就取消该请求,创建一个新的请求。
let xhr = null
let pending = false // 是否正在发送请求
btn.onclick = function () {
if (pending) xhr.abort() // 如果正在发送请求,就取消该请求,创建一个新的请求
xhr = new XMLHttpRequest()
xhr.open('GET', 'http://xxx')
xhr.send()
pending = true
xhr.onreadystatechange = function () {
if (xhr.readystate === 4) {
pending = false
if (xhr.status >= 200 && xhr.status < 300 || xhr.status === 304) {
console.log(xhr.response)
}
}
}
}
四、jQuery、Axios、fetch 发送 AJAX 请求
1. jQuery 发送 AJAX 请求
// 发送 GET 请求
// $.get(url, 参数, 回调, 响应体数据格式)
$.get('http://xxx', {a: 100, b: 200}, function (data) {
console.log(data)
}, 'json')
// 发送 POST 请求
// $.post(url, 参数, 回调, 响应体数据格式)
$.post('http://xxx', {a: 100, b: 200}, function (data) {
console.log(data)
}, 'json')
// 发送通用请求
$.ajax({
type: 'GET',
url: 'http://xxx',
data: {a: 100, b: 200}, // 参数
dataType: 'json', // 响应体数据格式
timeout: 3000,
success: function (result) {
console.log(result)
},
error: function (err) {
console.log(err)
},
})
更多用法请看文档:jQuery.Ajax
2. Axios 发送 AJAX 请求
Axios 是一个基于 promise 的网络请求库,可以用于浏览器和 node.js。
以下代码来自 Axios 文档。
axios.get('/user', {
params: { ID: 12345 }
})
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
})
.then(function () {
// 总是会执行
});
axios.post('/user', {
firstName: 'Fred',
lastName: 'Flintstone'
})
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
});
axios({
method: 'post',
url: '/user/12345',
data: {
firstName: 'Fred',
lastName: 'Flintstone'
}
});
更多用法请看文档:Axios
3. fetch 发送 AJAX 请求
fetch 是 window 上的一个方法,可以直接使用,它返回一个 Promise 对象。
fetch('http://xxx', {
method: 'POST',
body: 'a=100&b=200' // 请求体
})
.then(response => {
return response.json()
})
.then(response => {
console.log(response)
})
更多用法请看文档:使用 Fetch
五、跨域问题及解决方案
1. 同源策略
- 同源策略(Same-Origin Policy),最早由 Netscape 公司提出,是浏览器的一种安全策略。
- 同源:协议、域名、端口号必须完全相同。
- 跨域:违背同源策略(协议、域名、端口号有一个不一样)就是跨域。
AJAX 默认遵循同源策略。遵循同源策略的请求的 url,可以省略协议、域名、端口号,如:
const xhr = new XMLHttpRequest()
xhr.open('GET', '/data')
xhr.send()
注意:跨域的请求,浏览器发出的请求会成功,服务器接收请求也会成功,服务器也会成功返回结果给浏览器,只不过浏览器发现请求跨域了,所以不把数据暴露出来。
2. 使用 JSONP 解决跨域
JSONP(JSON with Padding),是一个非官方的跨域解决方案,纯粹凭借程序员的聪明才智开发出来的,只支持 get 请求。
- 网页中有一些标签天生具有跨域能力,比如:img、link、iframe、script 等。
- 举例:我们使用 script 标签引入其他网站的 js 文件,就是跨域的,并且不影响使用。
- JSONP 就是利用 script 标签的跨域能力来发送请求的。
原理
指定 script 标签的 src 属性,向服务器发出跨域请求。服务器返回一段可执行的 js 代码(如:函数调用),前端 script 标签拿到这段代码,就会解析执行这段代码,从而获取服务器返回的数据。
使用
注意点:需要前后端配合。前端提前定义好回调函数,并把回调函数的名字以参数的形式传给后端;后端获取回调函数的名字,并返回函数调用的 js 代码。
前端:
// 1. 动态创建一个 script 标签
const script = document.createElement("script");
// 2. 设置 script 的 src,设置回调函数
script.src = "http://xxx?callback=fun";
function fun(data) {
console.log(data);
};
// 3. 将 script 添加到 body 中
document.body.appendChild(script);
后端:
// 4. 服务器中路由的处理
router.get("/xxx" , function (req, res) {
const callback = req.query.callback;
let obj = {
name: "dongdong",
age: 18
}
obj = JSON.stringify(obj)
// 返回一段函数调用的 js 代码
res.send(`${callback}(${obj})`);
});
3. 使用 CORS 解决跨域
CORS(Cross-Origin Resource Sharing),跨域资源共享。
CORS 是官方的跨域解决方案,它的特点是不需要在客户端做任何特殊的操作,完全在服务器中进行处理,支持 get 和 post 请求。
原理
CORS 是通过设置一个响应头来告诉浏览器,该请求允许跨域,浏览器收到该响应以后就会对响应放行。
使用
router.get('/xxx' , function (req , res) {
// 通过 res 设置响应头,允许跨域请求
// res.setHeader("Access-Control-Allow-Origin", "http://xxx"); // http://xxx 可以访问
res.setHeader("Access-Control-Allow-Origin", "*"); // 所有 url 都可以访问
res.send();
});