文章目录
本文以使用一个XMLHttpRequest对象发起GET请求开始,探讨同源策略与跨域和跨域的实现。
一、XMLHttpRequest对象
MDN:XMLHttpRequest
XMLHttpRequest
(XHR)对象用于与服务器交互。通过 XMLHttpRequest 可以在不刷新页面的情况下请求特定 URL,获取数据。这允许网页在不影响用户操作的情况下,更新页面的局部内容。XMLHttpRequest
在 AJAX 编程中被大量使用。使用
XMLHttpRequest
来发送 HTTP 请求以实现网站和服务器之间的数据交换。发送一个 HTTP 请求,需要创建一个
XHR
对象,打开一个 URL,最后发送请求。当所有这些事务完成后,该对象将会包含一些诸如响应主体或 HTTP status 的有用信息。
后端准备
我这里使用了express简单搭建了一个服务。
app.js
const express = require('express');
const app = express();
app.all('*', function (req, res, next) {
res.header('Access-Control-Allow-Origin', '*'); // 跨域配置
next();
});
app.get('/api/test', (req, res) => {
res.send('hello express');
});
app.listen(3000, () => {
console.log('server running at port 3000');
});
实现一个简易的ajax请求
const xhr = new XMLHttpRequest();
xhr.open('GET', 'http://127.0.0.1:3000/api/test', true);
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
alert(xhr.responseText);
}
}
}
xhr.send(null);
1.open()
XMLHttpRequest.open() 方法初始化一个请求
/**
* @param method HTTP方法
* @param url 路径
* @param async 是否异步
*/
xhr.open(method, url, async);
2.xhr.readyState
XMLHttpRequest.readyState 属性返回一个 XMLHttpRequest 代理当前所处的状态。一个 XHR 代理总是处于下列状态中的一个:
值 | 状态 | 描述 |
---|---|---|
0 | UNSENT | 代理被创建,但尚未调用 open() 方法。 |
1 | OPENED | open() 方法已经被调用。 |
2 | HEADERS_RECEIVED | send() 方法已经被调用,并且头部和状态已经可获得。 |
3 | LOADING | 下载中; responseText 属性已经包含部分数据。 |
4 | DONE | 下载操作已完成。 |
我们如何查看readyState的变化?这就需要XMLHttpRequest.onreadystatechange
3.xhr.onreadystatechange()
只要 readyState 属性发生变化,就会调用相应的处理函数。这个回调函数会被用户线程所调用。XMLHttpRequest.onreadystatechange 会在 XMLHttpRequest 的readyState 属性发生改变时触发 readystatechange 事件的时候被调用。
当 readyState
的值改变的时候,callback
函数会被调用。
我们需要将回调函数的定义提到 open()
函数之前。因为open调用,请求初始化,readyState会从初始的0变成1,我们就不能打印出完整过程。
const xhr = new XMLHttpRequest();
console.log('UNSENT:', xhr.readyState); // 打印初始readyState
xhr.onreadystatechange = function () {
console.log('onreadystatechange:', xhr.readyState);
};
xhr.open('GET', 'http://127.0.0.1:3000/api/test', true);
xhr.send(null);
在浏览器中查看
4.xhr.status
只读属性
XMLHttpRequest.status
返回了响应中的数字状态码。在请求完成前和XMLHttpRequest 出错,status
的值为0
。 status码是标准的HTTP status codes。
查看完整状态的代码如下
const xhr = new XMLHttpRequest();
console.log(`UNSENT:readyState=${xhr.readyState};status=${xhr.status}`);
xhr.onreadystatechange = function () {
console.log(`onreadystatechange=${xhr.readyState};status=${xhr.status}`);
};
xhr.open('GET', 'http://127.0.0.1:3000/api/test', true);
xhr.send(null);
在浏览器中查看
5.send()
XMLHttpRequest.send()
方法用于发送 HTTP 请求。如果是异步请求(默认为异步请求),则此方法会在请求发送后立即返回;如果是同步请求,则此方法直到响应到达后才会返回。XMLHttpRequest.send() 方法接受一个可选的参数,其作为请求主体;如果请求方法是 GET 或者 HEAD,则应将请求主体设置为 null。
XMLHttpRequest.send(body)
注:在 open()
和 send()
之间,我们可以通过 XMLHttpRequest.setRequestHeader()
设置请求头等参数。
通过以上的步骤,我们就可以发送一个ajax请求。
二、同源策略与跨域
1.什么是跨域?
跨域,是指浏览器不能执行其他网站的脚本。它是由浏览器的同源策略造成的,是浏览器对JavaScript实施的安全限制。
2.同源策略
同源策略是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,浏览器很容易受到XSS、CSFR等攻击。
同源是指"协议+域名+端口"三者相同。
同源策略限制了以下行为:
- Cookie、LocalStorage 和 IndexDB 等存储性内容
- DOM 和 JS 对象无法获取
- Ajax 请求不能发送
三、实现跨域的几种方式
1.JSONP实现跨域
JSON (JSON with Padding ) json填充?
JSONP 是为解决跨域问题搞出来的一种获取数据的方式
JSONP原理
原理:在html页面中通过相应的标签从不同域名下加载静态资源文件是被浏览器允许的
加载图片、css、js可无视同源策略。
用处:img可用于统计打点,可使用第三方统计服务;link、script可使用CDN。利用script可实现JSONP跨域
<img src="跨域的图片地址" />
<link href="跨域的css地址" />
<script src="跨域的js地址"></script>
要点:
- 利用
<script>
标签没有跨域限制的漏洞,网页可以得到从其他来源动态产生的 JSON 数据。 - JSONP请求一定需要服务器做支持。
- 局限是只能get请求。
JSONP实现
我们需要知道,服务端可以任意动态拼接数据返回。如果返回页面,需要符合html格式,本质都是返回一些文本,那么!我们可以通过script可以跨域的获取数据。
先修改服务器端的app.js,不去设置Access-Control-Allow-Origin
此时我们刷新浏览器,会看到如下
现在修改html和后端,实现JSONP跨域
index.html
<script>
window.callback = (data) => {
console.log(data);
}
</script>
<script src="http://127.0.0.1:3000/api/jsonpdata"></script>
app.js中添加路由
app.get('/api/jsonpdata', (req, res) => {
res.send('callback("hello jsonp")');
});
这样从服务器获取字符串,符合js语法,就可以获取data,执行callback函数,然后在控制台中打印出来。
服务端可以通过get url传参,动态拼接出各种数据来返回给客户端。
2.CORS服务端支持
CORS是一个W3C标准,全称是"跨域资源共享"(Cross-origin resource sharing)。 它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。
整个CORS通信过程,都是浏览器自动完成,不需要用户参与。
对于简单请求
对于简单请求,浏览器直接发出CORS请求。具体来说,就是在请求头信息之中,增加一个Origin字段。
Origin字段用来说明,本次请求来自哪个源(协议 + 域名 + 端口)。服务器根据这个值,对比Access-Control-Allow-Origin
,决定是否同意这次请求。
图示:拒绝情况 firefox,第一个请求options 200,第二个请求并没有发出去
如果Origin指定的域名在许可范围内,服务器返回的响应,会多出几个头信息字段 Access-Control-XXXX
图示:成功允许情况 chrome
对于非简单请求
非简单请求是那种对服务器有特殊要求的请求,比如请求方法是PUT或DELETE,或者Content-Type字段的类型是application/json。
非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预请求"——浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错。
后端修改
我们修改后端app.js
app.all('*', function (req, res, next) {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Methods', 'GET, POST, PATCH, PUT, DELETE, OPTIONS')
next();
});
响应头自然也会多出一部分
我们把之前的/api/test,改成put方法请求,前端也随之修改put。
我们刷新页面,打开F12,发现有四个请求,分别是获取html文档,加载js文件和两个test请求
因为这是一个非简单请求,其中第一个为OPTIONS请求。
第二个请求才是正式的PUT请求
一旦服务器通过了"预检"请求,以后每次浏览器正常的CORS请求,就都跟简单请求一样,会有一个Origin头信息字段。服务器的回应,也都会有一个Access-Control-Allow-Origin头信息字段。
注意
需要注意的是,如果要发送Cookie,Access-Control-Allow-Origin就不能设为星号*,必须指定明确的、与请求网页一致的域名。
3.HTTP代理(http proxy)
我们通常使用 webpack 的 devServer.proxy实现跨域,原理就是http proxy,将所有ajax请求发送给devServer服务器,再由devServer服务器做一次转发,发送给数据接口服务器。
参考资料: