Ajax 跨域

我们先从这么一个问题来引入我们本章节的学习 —— 什么是跨域请求?

1.跨域请求

简单来说,跨域请求就是一个域下的资源请求另外一个域下的资源。

同一个域,指的是,协议名、域名、端口号都一致。 举个例子来说,假如 “http://www.a.com” 下的 JavaScript 脚本发起 Ajax 请求 “http://www.a.com/ajax” ,由于 协议名 http 、域名 www.a.com 和 端口号(默认都是 80)三者都是一致的,因此都属于同一个域,不造成跨域请求。而假如其中任一元素不相同,则造成跨域请求。与此同时,浏览器出于安全考虑,基于同源策略则会做一定的限制:比方说:

  • 无法获取不同域的 Cookie、LocalStorage 等等。
  • 无法获取不同域的 DOM 对象。
  • 无法向不同域发送 Ajax 请求。

2.正文须知

本章节不考虑不同域文档之间的跨域交互,主要讲 Ajax 造成的跨域的解决方法。

开始讲解 Ajax 造成的跨域问题如何解决之前,我们思考一下:

假如我们要从山的一边 A 到山的另一边 B,这座山无疑就是个障碍,那么我们有几种解决办法?

我想,要么我们就直接穿过去,要么我们就“曲线救国”,绕个道也未尝不可。没错,接下来我们要讲的 Ajax 跨域也是从这两方面来讲,既然跨域有这样那样的一些限制,那我们要么就直面去解决,要么就耍个机灵,同样能够解决。

3.曲线救国法

3.1 JSONP

JSONP 是一个非常经典的解决跨域的方法。我们知道,在 HTML 中,一些资源的引用事实上是不会受到跨域限制的,比如 script 标签。浏览器在解析 HTML 的时候,解析到了 script 标签,会把相应的资源下载下来。我们可以利用这一点,来实现前后端信息的交互。

3.1.1 JSONP 原理

  1. 定义好回调函数,比方说命名为 callback ,并将函数名作为 url 的参数;
  2. 添加 script 标签,指定的资源为目标域的方法,也就是上面的 url ;
  3. 后端接收 GET 请求,返回 callback(responseData) 格式数据,把要返回的数据 responseData 传到 callback() 中;
  4. 前端接收 javaScript 内容,执行了后端返回的 callback(responseData) ,这样就完成了一次前后端交互了。

3.1.2 具体例子

假如 HTML 有一个容器为 container,我们要通过 JSONP 的方式来为 container 插入一条内容,那么,我们可以这么做:

3.1.2.1 HTML 关键代码
<div id="container">

</div>
3.1.2.2 javaScript 关键代码
// jsonp

// 定义一个添加内容的回调函数
window.addContent = function (content) {
    document.getElementById('container').innerHTML = content;
}

/**
* 发送 JSONP 请求的函数
* cb 为回调函数的函数名
*/
function sendJsonPRequest (cb) {
    // 创建 script 标签
    const body = document.getElementsByTagName('body')[0];
    const script = document.createElement('script');
    script.type = 'text/javascript';
    
    // 指定标签的 url ,callback 参数为回调函数的函数名
    script.src = `http://localhost:8082/jsonp/get?callback=${cb}`;
    body.appendChild(script); // 添加到 body 最后面
}

sendJsonPRequest('addContent') // 执行发送 JSONP 请求

显而易见,前端我们会创建一个 script 标签,并且附带定义好的回调函数的函数名传给服务端。与此同时,我们需要在服务端进行 JSONP 请求的响应。

3.1.2.3 服务端关键代码
router.get("/jsonp/get", function(req, res) {
    const cb = req.query.callback; // 读取请求附带的参数 callback
    const resData = '这是一条服务端返回的内容';
    res.send(`${cb}(${JSON.stringify(resData)})`); // 返回 callback(resData) 格式的数据
});
3.1.2.4 效果

图片描述

从右边控制台可以看出来,我们成功创建了 JSONP 的请求,并且结果正如我们预期的执行了 addContent('这是一条服务端返回的内容'),界面上展示出插入的内容。

3.1.3 JSONP 小结

使用 JSONP 的方式,我们可以通过 script 标签绕过浏览器的跨域限制,进行前后端数据交互。不过另一方面,这种方法也很有局限性,我们只能够发送 GET 请求,无法满足更加复杂业务的需求。一般我们也不会推荐直接使用 JSONP 的方式来解决跨域问题。

3.2 服务端代理

接下来讲到的一种是服务端代理的方式。要问为什么采取服务端代理的方式呢?很简单,因为浏览器端 Ajax 请求有跨域的限制,那我们就把请求不同域的操作放在服务端好了,毕竟服务端是没有跨域限制这一说的。

3.2.1 服务端代理原理

  1. 浏览器端发送请求到同域的服务端;
  2. 服务端接收到请求之后,进行转发,请求不同域的另外一个服务端;
  3. 服务端间进行交互数据后,同域服务端返回数据给浏览器端。

3.2.2 具体例子

举一个服务端代理的例子,这里我使用了一个 Express 的中间件,叫做 express-http-proxy 。当然同学们也可以在同域服务端接收到请求的时候,发起 http 请求访问不同域的服务端来模拟这一代理行为。前端方面我使用了 jQuery 的 Ajax 方法。

3.2.2.1 javaScript 关键代码
$.ajax({
    url: '/proxy/proxy_get',
    method: 'GET',
    data: {
        a: '123',
        b: '234'
    }
}).done(data => {
    console.log(data)
})

很简单,我们就是向同域的服务器发送了一个请求。

3.2.2.2 同域服务器关键代码
const proxy = require('express-http-proxy');  // 引入代理中间件

// ... 一些代码

app.use('/proxy', proxy('http://localhost:8082/')); // 注册,之后 /proxy 都会代理到 http://localhost:8082/ 上
3.2.2.3 不同域的服务器关键代码
router.get("/proxy_get", function(req, res) {
    const {a, b} = req.query
    res.send(`参数是:${a}${b}`)
});

这是目标服务器的响应方法,返回一个 处理后的字符串。

3.2.2.4 效果

图片描述

3.2.3 服务端代理小结

服务端代理通过服务端和服务端之间的交互来避免浏览器和不同域的服务端之间直接进行交互,从而避免了跨域的问题。当然这种方法要求我们有一个中间服务器的存在。

4.面对疾风法

举了两个绕过跨域限制的方法,接下来我们要谈谈常规解决的情况。既然有跨域限制了,我们就来老老实实解决这个问题。

接下来我要讲的,是 CORS

4.1 CORS

首先展开一下 CORS 的全称:

Cross-origin resource sharing

意思是跨域资源共享,这是一个 W3C 标准,从字面意思来看不难理解,它允许浏览器向跨域的资源发送请求,并且获得结果数据。

4.1.1 CORS 原理

跨域资源共享标准新增了一组 HTTP 首部的字段,使得我们能够通过这些字段来跨域获取到我们所需要的资源。而要实现这一功能,我们需要前后端的配合,只有当后端实现了 CORS 功能,我们才能够通过浏览器直接访问资源。为此,我们先来看看接下来的几个首部字段:

  • Access-Control-Allow-Origin :表示服务端允许的请求源的域,如果是 * 表示允许所有域访问,一般我们不建议使用 *;
  • Access-Control-Allow-Headers: 表示预检测中,列出了将会在正式请求的 Access-Control-Request-Headers 字段中出现的首部信息
  • Access-Control-Allow-Methods: 表示服务端允许的请求方法
  • Access-Control-Allow-Credentials: 表示服务端是否允许发送cookie。当然前端也需要设置对应的 xhr.withCredentials 来进行配合;
  • Access-Control-Expose-Headers: 列出了可以作为响应的一部分暴露在外的头部信息。

其中,我们更为重要的当属 Access-Control-Allow-Origin 字段,因为这个字段直接关系到你是否能够跨域访问资源的权限了。通常情况下,为了解决跨域问题,后端同学会设置 Access-Control-Allow-Origin 指定为我们的请求源的域,而前端代码基本无感。

4.1.2 简单请求和非简单请求

关于 CORS ,HTTP 请求上会有一些小小区别,最直观的区别就是会不会触发多一次 OPTIONS 预检测请求。我们把一些不会触发预检测请求的请求,称为简单请求,而相反,会触发预检测的请求则是非简单请求

而关于如何区分简单请求和非简单请求,这里我就不再累赘,有兴趣的同学可以读一下 HTTP 控制访问 。在实际的工作过程中,使用到 CORS 来解决跨域限制是非常常见的,这里我们注意一下简单请求和非简单请求的直观区别即可,并在以后的工作中留意一下,而不至于懵逼于为什么多了一次 OPTIONS 请求。

4.1.3 具体例子

4.1.3.1 服务端核心代码
// 全局设置请求过滤
app.all('*',function (req, res, next) {
    res.header('Access-Control-Allow-Origin', 'http://localhost:8080'); // 设置 Access-Control-Allow-Origin
    res.header('Access-Control-Allow-Headers', 'Content-Type, Content-Length, Authorization, Accept, X-Requested-With');  // 设置 Access-Control-Allow-Headers
    res.header('Access-Control-Allow-Methods', 'PUT, POST, GET, DELETE, OPTIONS'); // 设置 Access-Control-Allow-Methods
    next()
});


// 注册一个简单的路由
router.get("/simple/get", function(req, res) {
    const {a} = req.query
    res.send(`参数值是${a}`)
});

后端要做的工作就是实现 CORS 功能。正如上方代码,我们规定了一系列 HTTP 请求头首部字段,使得 http://localhost:8080 这个域的前端脚本拥有向服务端发起请求并取得资源的权限。

4.1.3.2 前端核心代码
$.ajax({
    url: 'http://localhost:8083/simple/get',
    method: 'GET',
    data : {
        a: 1
    }
}).done(data => {
    console.log(data)
})
4.1.3.3 效果

图片描述

可见,通过 CORS ,前端成功拿到了不同域的服务端的返回内容。

4.1.4 CORS 小结

CORS 是一个 W3C 的标准。使用 CORS ,我们可以使用使用常规的方式来解决前后端跨域访问的问题。并且,大多数的工作其实也是放在了服务端上,对于前端而言,基本上可以说是无感的。

当然, CORS 也是存在着一些弊端。正因为它是 W3C 中一个比较新的方案,导致了各大浏览器引擎没有对其做严格规格的实现,由此可能产生一些不一致的情况。

5.本章最后

跨域远不止 Ajax 跨域,而解决 Ajax 跨域的方法也不只有本章中提到的这三种。

说跨域远不止 Ajax 跨域,打个比方,不同域的网页之间的通信也是属于跨域范畴。但由于本章的主题是 Ajax 跨域,因此我们不做过多的讨论。有兴趣的同学,可以深入去探究一下。

而解决 Ajax 跨域的方法,本章提及 3 种方法,从两个方面来阐述。对于遇见的问题,解决的方法要么就是绕个道走,要么就是穿过去走。无论你使用哪一种方法,肯定也都有利有弊。而实际的应用中,我们到底要采用何种方法来解决 Ajax 跨域问题呢?我的建议是关注业务和场景,这就需要同学们在另外一个层面去进行深入的思考了。

本着鼓励深入学习深入思考的原则,我希望同学们能够在跨域的问题上,进行深入的研究,总结起来。