AJAX & 跨域

185 阅读4分钟

一、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 的描述:

状态描述
0UNSENT代理被创建,但尚未调用 open() 方法。
1OPENEDopen() 方法已经被调用。
2HEADERS_RECEIVEDsend() 方法已经被调用,并且头部和状态已经可获得。
3LOADING下载中; responseText 属性已经包含部分数据。
4DONE下载操作已完成。

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 字符串转成正常对象:

  1. 使用 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)
        }
    }
    
  2. 设置响应体数据类型: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()

注意:跨域的请求,浏览器发出的请求会成功,服务器接收请求也会成功,服务器也会成功返回结果给浏览器,只不过浏览器发现请求跨域了,所以不把数据暴露出来。

image.png

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();
});