持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第1天,点击查看活动详情
一、什么是同域?
协议(http/https,tcp,udp,ftp等),域名(www.baidu.com,moblie.baidu.com), 端口(8080,8081,443)等全相同才是同域,一个不同即跨域。
二、浏览器哪些不支持跨域
- cookie、localStorage、sessionStorage
- Dom元素也有同源策略 iframe引用其他页面的行为
- ajax也默认不支持跨域(代理,加Auth的header头等方法解决)
三、实现跨域的方式
- Jsonp
- cors(后端提供)
- postMessage(两个页面,或原生App内嵌H5)
- window.name
- location.hash
- http-proxy
- nginx
- websocket
四、栗子🌰
4.1 JSONP
Jsonp就是通过script标签去引入的js,平常打开百度,加载了很多js,这就是jsonp实现跨域的手段:
如上图所示,www.baidu.com和js地址并不在同一个域, 通过script标签可以引入资源。
通过百度搜索,抓包到一个接口 www.baidu.com/sugrec?pre=…
这是搜索一个“1”,百度返回的数据
注意看wd(搜索关键词word)和cb(callback回调方法)是关键,我们把cb改成show,返回的就是show方法了
基于百度的接口我们自己封装一个jsonp方法
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body></body>
<script>
function jsonp({ url, params, cb }) {
return new Promise((resolve, reject) => {
const script = document.createElement('script')
window[cb] = function (data) {
resolve(data)
document.body.removeChild(script)
}
const totalParams = { ...params, cb }
const paramsArr = []
for (const key in totalParams) {
paramsArr.push(`${key}=${totalParams[key]}`)
}
script.src = `${url}&${paramsArr.join('&')}`
document.body.appendChild(script)
})
}
jsonp({
url: 'https://www.baidu.com/sugrec?pre=1&p=3&ie=utf-8&json=1&prod=pc&from=pc_web&sugsid=36425,36455,36367,34812,36422,36167,35979,36055,26350,36468,36314,36447&req=2&csor=1',
params: {
wd: '1',
},
cb: 'show',
}).then(res => {
console.log(res, '得到的结果')
})
</script>
</html>
通过jsonp的方式,在本地域,拿到了百度返回的数据
注意:1.script写在body之后。 2. createElement等代码都写在peomised的箭头函数中
缺点: 1.只能发送get请求 不支持 post delete put请求 2.只能发送get请求 不支持 post delete put请求
4.1.1 jsonp通过后端去拿数据
- 起一个express的服务
let express = require('express')
let app = express()
app.get('/say', function (req, res) {
let { wd, cb } = req.query
console.log(wd, 'wd')
res.end(`${cb}('我不爱你')`)
})
app.listen(3000)
- 把前端jsonp的代码稍微改造一下
script.src =
url.indexOf('localhost') > -1
? `${url}?${paramsArr.join('&')}`
: `${url}&${paramsArr.join('&')}`
document.body.appendChild(script)
jsonp({
url: 'http://localhost:3000/say',
params: {
wd: '我爱你',
},
cb: 'show',
}).then(res => {
console.log(res, '得到的结果')
})
4.2 cors的方式
我们通过express起一个3000端口的服务,和4000端口的服务,3000端口有个静态页发送Ajax请求到4000端口
server1.js
let express = require('express')
let app = express()
app.use(express.static(__dirname))
app.listen(3000, () => {
'app is listen to 3000'
})
serve2.js
let express = require('express')
let app = express()
app.get('/getData', function (req, res) {
console.log(req.headers)
res.end('我不爱你')
})
app.listen(4000)
ajax请求
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script>
let xhr = new XMLHttpRequest()
xhr.open('GET', 'http://localhost:4000/getData', true)
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
console.log(xhr.response)
}
}
}
xhr.send()
</script>
</body>
</html>
注意: 请求时发送到了服务端,是浏览器判断跨域屏蔽掉了响应结果
4.2.1 Access-Control-Allow-Origin
在localhost:4000 的node服务,添加请求头的中间件方法
let whiteList = ['http://localhost:3000']
app.use(function (req, res, next) {
let origin = req.headers.origin
if (whiteList.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin)
}
next()
})
localhost:3000可以访问localhost:4000的服务了
4.2.2 Access-Control-Allow-Headers
- 通过设置Access-Control-Allow-Headers, 来设置允许哪些头
res.setHeader('Access-Control-Allow-Origin', origin)
res.setHeader('Access-Control-Allow-Headers', 'x-token,x-name')
4.2.3 Access-Control-Allow-Methods
- 换成put方法又报跨域错误了
- 老样子,继续设置headers
res.setHeader('Access-Control-Allow-Methods', 'PUT,POST,GET,DELETE')
- ok
4.2.4 Access-Control-Max-Age
PUT请求实际上是发送了2次,第一次是options,预处理判断是否可以发put,第一次无意义,在后端提前end, 6s内不需要重复预检测
res.setHeader('Access-Control-Max-Age', 6000)
if (req.method === 'OPTIONS') {
res.end()
}
现在只发送一次了
4.2.5 Access-Control-Allow-Credentials
xhr如果带cookie,也需要后端设置header去放行
res.setHeader('Access-Control-Allow-Credentials', true)
4.2.6 Access-Control-Expose-Headers
如果想获取服务端的响应头,直接获取会拒绝
console.log(xhr.getResponseHeader('name'))
接下来继续在服务端设置头
4.3 postMessage
起两个serve,两个html,html1,iframe去引用index2,资源加载好后,通过onload,然后postMessage的方式去通信
index1.html发送
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<h1>index1.html</h1>
<iframe src="http://localhost:4000/index2.html" onload="load()" id="frame">
</iframe>
<script>
function load() {
let frame = document.getElementById('frame')
frame.contentWindow.postMessage('i love u', 'http://localhost:4000')
}
window.onmessage = function (e) {
console.log(e.data, 'index1.html')
}
</script>
</body>
</html>
index2.html接受
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<h3>index2.html</h3>
<script>
window.onmessage = function (e) {
console.log(e.data, 'index2.html')
e.source.postMessage('i love u too', e.origin)
}
</script>
</body>
</html>
效果
4.4 window.name
起两个服务,3个页面,有两个页面同域,一个不同域。同域的一个页面iframe去引用不同域的,不同域的把一个属性挂载到window.name上。然后通过改变iframe的src到同域的页面,由于name不会消失,实现了跨域。 a.html, b.html同域。 c不同
a.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<h1>a.html</h1>
<iframe
src="http://localhost:4000/c.html"
onload="load()"
id="frame"
frameborder="0"
>
</iframe>
<script>
let first = true
function load() {
if (first) {
let frame = document.getElementById('frame')
frame.src = 'http://localhost:3000/b.html'
first = false
} else {
console.log(frame.contentWindow.name)
}
}
</script>
</body>
</html>
b.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<h3>b.html</h3>
<script></script>
</body>
</html>
c.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<h3>c.html</h3>
<script>
window.name = 'i love u'
</script>
</body>
</html>
4.5 window.hash
起2个服务,3个页面,2个同域,1个不同。a.html跨域请求c.html。c把hash带到b,b 通过parent.location.hash获得hash, 由于a中监听了hashchange事件。所以实现了跨域 a.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<h1>a.html</h1>
<iframe
src="http://localhost:4000/c.html#iloveyou"
id="frame"
frameborder="0"
>
</iframe>
<script>
window.onhashchange = function () {
console.log(location.hash)
}
</script>
</body>
</html>
b.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<h3>b.html</h3>
<script>
window.parent.parent.location.hash = location.hash
</script>
</body>
</html>
c.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<h3>c.html</h3>
<script>
console.log(location.hash)
let iframe = document.createElement('iframe')
iframe.src = 'http://localhost:3000/b.html#iloveutoo'
document.body.appendChild(iframe)
</script>
</body>
</html>
4.6 window.domain
和window.name实现方式类似
4.7 Websocket
a.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<h1>a.html</h1>
<script>
let socket = new WebSocket('ws://localhost:3000')
socket.onopen = function (e) {
socket.send('i love u')
}
socket.onmessage = function (data) {
console.log(data, 'a.html')
}
</script>
</body>
</html>
server
let express = require('express')
let app = express()
let WebSocket = require('ws')
let wss = new WebSocket.Server({ port: 3000 })
wss.on('connection', function (ws) {
ws.on('message', function (data) {
console.log(JSON.stringify(data), 'DATA')
ws.send('i love u too')
})
})
4.8 nginx
mac安装nginx, windows直接下载包就好了
# 安装
brew install nginx
# 启动
brew services start nginx
# 打开配置目录
open /usr/local/etc/nginx
通过加配置,实现跨域,修改nginx.conf,然后restart
location ~.*\.json {
root json;
add_header "Access-Control-Allow-Origin" "*";
}
4.9 常用的webpack中的代理
module.exports = {
proxy: {
'/apis': { //将www.exaple.com印射为/apis
target: 'https://www.exaple.com', // 接口域名
secure: false, // 如果是https接口,需要配置这个参数
changeOrigin: true, //是否跨域
pathRewrite: {
'^/apis': '' //需要rewrite的,
}
}
}
}