笔者常常在前端开源群答疑,加上以前的招聘面试经历。发现许多新手前端在问起跨域问题的解决方案,一套一套的,但是实际遇到跨域问题了就不知道怎么解决了。此次写这篇文章从实践角度聊一聊跨域问题。javascript
出于浏览器的同源策略限制,浏览器会拒绝跨域请求。 这就是跨域问题的产生缘由,同源策略是用于隔离潜在恶意文件的重要安全机制。css
这句话的三个关键字:html
那么第一个问题来了,什么算是同源。解决这个问题须要先了解一下URL的完整结构: 前端
API 接口地址 | 是否跨域 | 缘由 |
---|---|---|
www.domain.com/api/users/1 | 否 | 协议、主机、端口所有都相同 |
www.domain.com:80/api/users/1 | 是 | 端口不一样 |
www.baidu.com/api/users/1 | 是 | 协议不一样 |
api.domain.com/v1/users/1 | 是 | 主机不一样 |
script
、img
、link
、video
等标签加载资源的请求(HTTP GET请求)不作限制。具体的限制规则还有不少,这里只说常见和本文用得上的。vue
那么那些环境算是浏览器?java
那么若是用户发出的AJAX GET请求是一个跨域请求,那么会在上图中哪个阶段被阻止? 可是第3阶段,也就是说用户发送的信息能够到达服务端,服务器是可以接受处理并返回了。返回的浏览器发现这是一个跨域请求。就直接拒绝,同时把返回的信息替换为报错信息,返回给JavaScript程序。 对于更复杂的POST/PUT等请求, MDN CORS文档里面有更详细的处理方法。这里就不细说。react
这一点很重要,可是老是被新人忽略。因此重要的事情说三遍,webpack
反过来讲,Nginx、Java/Nodejs等编程语言的HTTPClient以及手机App,他们发出的HTTP请求就彻底没有跨域问题,由于他们不是浏览器,没有实现W3C规范。ios
在浏览器中假设有如下一段代码会执行结果会是什么样?nginx
<script> window.callback = function (data) { console.log(data); delete window.callback; } </script>
<script> callback({ "code": 1, "data": [1,2,3] }); </script>
复制代码
毫无疑问,确定是在控制台输出了一个对象信息。 记得刚才在介绍跨域基本概念的时候说个浏览器不限制script
标签加载js文件。那么把这两者的特性相结合。第二个script
标签改成从网络加载. 就能够实现跨域. 例如 一个跨域APIhttp://api.domain.com/v1/users/1
<script src="http://api.domain.com/v1/users/1?callback=callbackFun"></script>
GET http://api.domain.com/v1/users/1?callback=callbackFun
的请求application/javascript
callbackFun({/*须要的数据*/});
复制代码
以上步骤就是JSONP的思想。实现一个完善的JSNOP请求库还有细节要处理,好比超时取消、回调函数防重名等。不少开源库(jQuery, axios)都实现了JSNOP请求。想要代码的去Github阅读源码,这里就不给出代码。
JSONP虽然是一种实现跨域访问的方法,前端想要使用JSONP进行跨域访问却不容易。
GET http://api.domain.com/v1/users/1
返回ContentType: application/json
复制代码
{
"code": 1,
"data" : {"userid": 1}
}
复制代码
而GET http://api.domain.com/v1/users/1?callback=callbackFun
返回
ContentType: application/javascript
复制代码
callbackFun({
"code": 1,
"data" : {"userid": 1}
});
复制代码
既然能够和后端商量配合你改造接口,那还有更好的方案能够解决。何须用这种方案。
JSONP有一个有点就是兼容性好,IE678统统兼容,因此通常JSNOP是后端同窗若是主动须要开放API给他人使用,同时有须要极高的兼容的一个妥协方案。通常状况下不推荐这个方案。
真实经历。以前开发项目须要调用另外一个项目组的接口。 跨域形成接口掉不通,而后找Z君沟通, Z君说:"你用JSONP来掉接口就行了。这都不知道...." 而后我还在想大神这么NB的么,JSONP兼容都提早作完了。我试了JSONP。坑爹呀,你后端根本就没兼容JSONP,我怎么调用,呵呵... 呵呵呵呵....
JSONP方案不推荐,那么又须要访问跨域接口,怎么办呢? 重要事情不在意再多说一遍拒绝跨域请求是浏览器。 那么若是有一个非W3C标准的HttpClient帮助咱们转发请求,不就能够了实现跨域访问了。
一般App对于webview都有很强的控制权,能够在Webview的JS环境中注入一些方法。 那么移动端程序员能够在Webview中注入一个接口,运行在里面的js代码能够经过这个方法把本身的请求地址、请求参数、请求体等数据交给App Native端,让App Native代为收发请求。App Native不是浏览器,不受跨域限制。
Web端必然运行在浏览器环境中,那么没有App Native。还有服务器上能够作反向代理。 所谓的反向代理,原理和App Native请求代理的原理差很少,就是咱们请求非跨域下的反向代理服务,反向代理服务会把你的请求转发给目标服务器。 反向代理服务能够是Nginx也能够是java/Nodejs程序等等。这些程序也不受跨域限制,能够接受目标服务器的请求,并返回给咱们。
React/Vue 这种SPA开发施行的彻底的先后端分离的模式,开发阶段必然是须要跨域访问接口的。 Vue开发能够这样配置:
// vue.config.js
module.exports = {
devServer: {
proxy: {
'/api': {
target: '<url>',
ws: true,
changeOrigin: true
},
'/foo': {
target: '<other_url>'
}
}
}
}
复制代码
详情见Vue CLI文档。React也有相似的配置,详情见 create-react-app文档
那么他们具体是怎么实现的呢? 本地起一个服务端程序提供反向代理的能力。而React/Vue本地启动的这个服务端程序就是Webpack-dev-server。来探索Webpack-dev-server源码,源码中启动server的关键代码在lib/Server.js中,挑重点
/* 此处省略许多行代码 */
// 27行 引入express 做为服务端框架
const express = require('express');
/* 此处省略许多行代码 */
// 31行 引入 http-proxy-middleware 提供反向代理的能力
const httpProxyMiddleware = require('http-proxy-middleware');
/* 此处省略许多行代码 */
// 328行 获取 proxyMiddleware 并加载到为express的中间件Middleware
app.use((req, res, next) = > {
if (typeof proxyConfigOrCallback === 'function') {
const newProxyConfig = proxyConfigOrCallback();
if (newProxyConfig !== proxyConfig) {
proxyConfig = newProxyConfig;
// 334行 根据 proxyConfig 获取 处理proxy请求的中间件proxyMiddleware
proxyMiddleware = getProxyMiddleware(proxyConfig);
}
}
const bypass = typeof proxyConfig.bypass === 'function';
const bypassUrl = (bypass && proxyConfig.bypass(req, res, proxyConfig)) || false;
if (bypassUrl) {
req.url = bypassUrl;
next();
} else if (proxyMiddleware) {
// 347行 最最关键一行 通过屡次断定某个请求是须要代理转发的请求,那么把它交给proxyMiddleware进行处理, proxyMiddleware
return proxyMiddleware(req, res, next);
} else {
next();
}
});
复制代码
以上代码有点NodeJS服务端开发的同窗基本能看明白,看不明白也不要紧。你知道React/Vue能够经过相应的配置项得到接口跨域访问的能力就能够了。其中最核心的就是依靠Express的网络请求能力充当反向代理服务器。
开发阶段还能够经过本地启动一个Express服务器做为代理,帮助咱们处理跨域问题,问题是生产环境是不推荐这么作的。React/Vue 项目一般在build之后会生成如下文件:
对于index.html的部署,Vue-Router文档写的很清楚。推荐经过nginx try-file命令来进行部署。同时nginx又是一个反向代理服务器。假设 网页须要在host http://www.domain.com/
下, 真实API服务部署在http://api.domain.com/api
。那么经过反向代理把接口代理到 http://www.domain.com/api
下。那么跨域访问就变成了同域名访问。 那么nginx的配置文件能够这样写
server {
listen 80;
server_name www.domain.com ;
root www; # 存放html文件的文件夹
location ^/api { # 接口代理到 8080
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_pass http://api.domain.com/api;
}
location / { # 其余请求返回index.html
try_files $uri $uri/ /index.html;
}
}
复制代码
这样作完, 访问API就会被代理转发,访问其余路径就返回html。以下图所示:
线上部署的方式可能根据系统架构选型而多种多样。这只是其中一种比较通用且为官方推荐的方式。仅作参考。相似ngixn的服务端软件仍是Caddy、Envoy
这种方案的优势是不须要后端同窗改动接口,只须要运维小哥帮助配置一下nginx便可完成兼容。缺点是多一次转发可能带来性能损失。
实际状况多种多样,有些时候没办法使用JSONP,也经过nginx转发又会产生性能损失。那么还有一个终极大招———— CORS.
W3C的同源策略出来之后形成了不少不便,没法应对某些跨域访问的强需求。为此W3C增长了CORS相关的规范, 文档以前也说起过:MDN CORS文档。
重要的事情再重复一遍:拒绝跨域请求是浏览器,那么CORS的原理就是CORS相关的规范中制定了一些响应头(Response Header),这些响应头以Access-Control-Request-
开头。简单枚举几个,具体这些头的含义和用法见MDN CORS文档.
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER, Content-Type
Access-Control-Allow-Origin: http://foo.example
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
Access-Control-Max-Age: 86400
复制代码
浏览器在接收到设置过CORS响应头的返回之后,会根据CORS规范检查合法性,检查经过则再也不阻止,放行经过。
简而言之就是CORS响应头就是用来告诉浏览器:"我是虽然是跨域请求,可是我是合法的,请不要拒绝我"。
CORS方案的优势是支持各类方法 GET、POST、PUT、DELTE等等。并且改动量比较小。能够在服务端程序好比Java或者NodeJS上作,也可经过前置代理服务器nginx完成。
缺点就是
其余还有用与父页面与子页面(iframe)之间的通讯的跨域问题,window.name、postMessage等方法。这里就不详细说了。平常用的确实很少,有须要再查把。
生产环境中建议选择顺序是 反向代理 > CORS >> JSONP。 由于反向代理兼容性最好,程序改动少。 CORS适用于没法容忍反向代理的性能损失和第三方OpenAPI访问。 JSONP 只有当后端须要兼容性高,没办法部署反向代理服务器的状况以及前端访问第三方提供的JSONP接口。其余任何状况下不推荐。