背景
跨域问题在开发中很常见,目前很多框架都我们封装好了具体解决方案。我们只需要直接配置一个属性、或是调用一个方案等等,即可完成跨域。解决跨域的方案不唯一,下面我们来看一下跨域有几种解决方案。
同源策略
提到跨域就要有同源策略了。同时满足 协议 域名 端口号 一致,则两个页面具有相同的源,才算是同一个域名,否则均认为需要做跨域的处理。
jsonp
提到跨域不得不提 jsonp ,该方法兼容性好,但不支持 POST ,且不安全会造成 XSS 攻击。具体前后端 Demo 完整实现可见 链接
下面只写出核心代码,若后端不自行实现,可拿百度搜索接口来测试;
前端入门助理解版
function searchBaidu (value) {
jsonp(`https://www.baidu.com/sugrec?json=1&prod=pc&wd=${value}&bs=${value}&cb=jsonpCallBack`) // 百度接口测试
}
function jsonp(url) {
var element = document.createElement('script');
element.setAttribute('src', url);
document.getElementsByTagName('head')[0].appendChild(element);
}
function jsonpCallBack (res) {
const { g } = res
const list = Array.from(g, ({ q }) => q)
const domList = list.map(item => {
return `<div>${item}</div>`
})
$('#selectWrap').html(domList.join(''))
}
前端 promise 封装版
function searchBaidu (value) {
jsonp({
params: {
wd: value,
bs: value,
json: 1,
prod: 'pc',
},
url: `https://www.baidu.com/sugrec`,
cb: 'jsonpCallBack',
}).then(res => { // 运用 promise 封装,即可用 then 回调实现
const { g } = res
const list = Array.from(g, ({ q }) => q)
const domList = list.map(item => {
return `<div>${item}</div>`
})
$('#selectWrap').html(domList.join(''))
})
}
// promise 封装
function jsonp({ url, params, cb }) {
params = { ...params, cb }
let paramsStringify = Object.keys(params).map(item => `${item}=${params[item]}`).join('&')
return new Promise((resolve, reject) =>{
var element = document.createElement('script');
element.setAttribute('src', `${url}?${paramsStringify}`);
document.getElementsByTagName('head')[0].appendChild(element);
window[cb] = res => {
resolve(res)
}
})
}
后端 Node.js 实现
const url = require('url');
require('http').createServer((req, res) => {
const data = {"q":"跨域","p":false,"g":[{"type":"sug","sa":"s_1","q":"跨域解决方案"},{"type":"sug","sa":"s_2","q":"跨域物流"},{"type":"sug","sa":"s_3","q":"跨域立案"},{"type":"sug","sa":"s_4","q":"跨域是什么"},{"type":"sug","sa":"s_5","q":"跨域请求"},{"type":"sug","sa":"s_6","q":"跨越速运单号查询"},{"type":"sug","sa":"s_7","q":"跨域问题"},{"type":"sug","sa":"s_8","q":"跨越速运"},{"type":"sug","sa":"s_9","q":"跨域访问"},{"type":"sug","sa":"s_10","q":"跨域立案诉讼服务规定"}],"slid":"10238582904221488926"}
const callback = url.parse(req.url, true).query.cb;
res.writeHead(200);
res.end(`${callback}(${JSON.stringify(data)})`); // 后端实现最核心
}).listen(9000, '127.0.0.1');
console.log('启动服务,监听 127.0.0.1:9000');
实践代码
效果演示
CORS
普通跨域请求:只服务端设置头 Access-Control-Allow-Origin 即可,前端无须设置。
实践代码
效果演示
框架跨域应用
- Egg.js 实现跨域具体方案 egg-cors
http-proxy
实践代码
这里服务没有基于 Koa 、Express 来写,采用无框架 Node.js 写法。
效果演示
- 可以看到在
localhost:8080端口号请求,可以获取到localhost:9000的数据;
框架跨域应用
vue 、 react 的脚手架已经帮我们将跨域配置简化抽离,相信也是基于 http-proxy 原理进行跨域访问的,只是框架都帮我们封装好了,让我们使用起来更简单。
vue.config.js一部分
devServer: {
host: '0.0.0.0',
disableHostCheck: true,
proxy: {
'/mock': {
target: `http://127.0.0.1:3000`,
changeOrigin: true,
pathRewrite: {
'^/mock': ''
}
},
},
},
React中的setupProxy.js很清晰基于http-proxy-middleware
const proxy = require('http-proxy-middleware')
const { REACT_APP_PROXY_URL } = process.env
module.exports = function(app) {
app.use(proxy('/mock', {
target: 'http://127.0.0.1:3001',
secure: false,
changeOrigin: true,
pathRewrite: {
'^/mock': ''
}
}))
app.use(proxy(`${REACT_APP_PROXY_URL}`, {
target: 'http://xxxx.xxx.com',
secure: false,
changeOrigin: true,
pathRewrite: {
[`^${REACT_APP_PROXY_URL}`]: ''
}
}))
}
Umi.js配置的跨域, 在config.ts文件中
const {
REACT_APP_PROXY_URL,
REACT_APP_PROXY_BASE_URL,
} = process.env;
/// ...
export default {
/// ....,
proxy: {
[`${REACT_APP_PROXY_URL}`]: {
target: REACT_APP_PROXY_BASE_URL,
changeOrigin: true,
pathRewrite: {
[`^${REACT_APP_PROXY_URL}`]: '',
},
},
},
//...
}
正反代理
通过上面我们知道通过 CORS 方式的代码,前端 HTML 直接请求 localhost:9000 地址的数据,
而 Proxy 方式,前端 HTML 还是请求 localhost:8080 地址的数据。 CORS 方式成为正向代理, Proxy 方式成为反向代理。
总结一句话到底是正向代理还是反向代理,只需要判断客户端知不知道真正返回数据的服务器在谁,知道就是正向代理,不知道就是反向代理
PostMessage
PostMessage 是 HTML5 中的 API ,且是为数不多可以跨域操作的 Window 属性之一。
当前页面与打开新窗口 进行数据传输
- 实践代码
- 前提:在
from中打开新窗口to,from在8080端口号访问,to在9000端口号访问; from通过找到window.open返回得到to的window,在通过postMessage传递消息;to通过监听message得到数据;
- 前提:在
- 效果图
页面与嵌套 iframe 消息传递
- 实践代码
from嵌套to,from在8080端口号访问,to在9000端口号访问。from通过找到to的iframe得到window通过postMessage传递消息to通过监听message得到数据
- 效果图
Nginx
未配置前
server {
listen 8888;
server_name localhost;
location /mystatus {
stub_status;
}
location / {
root /www/test;
index 1.html;
}
}
配置后
- 重点在:add_header "Access-Control-Allow-Origin" "*";
server {
listen 8888;
server_name localhost;
location /mystatus {
stub_status;
}
location / {
root /www/test;
index 1.html;
add_header "Access-Control-Allow-Origin" "*";
}
}
- 页面请求该网址接口
document.domain
该方式只能用于二级域名相同的情况下,比如 a.test.com 和 b.test.com 适用于该方式。
只需要给页面添加 document.domain = 'test.com' 表示二级域名都相同就可以实现跨域
实践代码
- 首先在本地
host配置 两个域名指向127.0.0.1
127.0.0.1 b.test.com
127.0.0.1 a.test.com
- 接这开启两个服务
8080端口给到a.html页面,9000端口给到b.html页面
效果演示
- 未加 domain 之前
- 添加 domain 之后
window.name
实践代码
a1.htmla2.html部署在同域8080端口下,b.html部署在9000端口上a1.html的iframe嵌套不同域b.html- 将
b.html中数据挂载到window.name上
效果演示
location.hash
代码实践
a1.htmla2.html部署在同域8080端口下,b.html部署在9000端口上a1.html的iframe嵌套不同域b,并将传递数据放置到hash上- 在
b.html将监听到的hash值经过处理,在创建iframe,src指向a2 - 此时
a2可以将自己location.hash给到window.parent.parent.location.hash
效果演示
WebSocket
实践代码
效果演示
总结
由此可看跨域种类多种多样,目前最常用的是 proxy , Webpack 已经帮我们封装好, Vue 、 React 等脚手架配置起来也相当方便, window.name domain hash 都与 iframe 相干,目前不常用。 CORS 方案也有封装好,方便项目中使用。大家一起来手动实践一下吧,相信会有所收获;当然不止这些例子,还有更多等着你去挖掘;