前端跨域的各类文章其实已经不少了,但大部分仍是不太符合我胃口的介绍跨域。看来看去,若是要让本身理解印象深入,果真仍是得本身敲一敲,并总结概括整理一篇博客出来,以此记录。javascript
跨域是为了阻止用户读取到另外一个域名下的内容,Ajax 能够获取响应,浏览器认为这不安全,因此拦截了响应。html
除非特别说明,不然下方标记的 html 文件默认都运行在 http://127.0.0.1:5500 服务下前端
CORS 便是指跨域资源共享。它容许浏览器向非同源服务器,发出 Ajax 请求,从而克服了 Ajax 只能同源使用的限制。这种方式的跨域主要是在后端进行设置。vue
这种方式的关键是后端进行设置,便是后端开启 Access-Control-Allow-Origin 为*
或对应的 origin
就能够实现跨域。java
浏览器将 CORS 请求分红两类:简单请求和非简单请求。node
只要同时知足如下两大条件,就属于简单请求。react
凡是不一样时知足上面两个条件,就属于非简单请求。webpack
简单请求nginx
cors.htmlgit
let xhr = new XMLHttpRequest() xhr.open('GET', 'http://localhost:8002/request') xhr.send(null)
server.js
const express = require('express') const app = express() app.use((req, res, next) => { res.header('Access-Control-Allow-Origin', 'http://127.0.0.1:5500') // 设置容许哪一个域访问 next() }) app.get('/request', (req, res) => { res.end('server ok') }) app.listen(8002)
非简单请求
上面的是简单请求,若是咱们用非简单请求的方式,好比请求方法是 PUT,也能够经过设置实现跨域。
非简单请求的 CORS 请求,会在正式通讯以前,增长一次 HTTP 查询请求,称为"预检"请求。
浏览器先询问服务器,服务器收到"预检"请求之后,检查了 Origin、Access-Control-Request-Method 和 Access-Control-Request-Headers 字段之后,确认容许跨源请求,浏览器才会发出正式的 XMLHttpRequest 请求,不然就报错。
let xhr = new XMLHttpRequest() xhr.open('PUT', 'http://localhost:8002/request') xhr.send(null)
server.js
const express = require('express') const app = express() let whileList = ['http://127.0.0.1:5500'] // 设置白名单 app.use((req, res, next) => { let origin = req.headers.origin console.log(whitList.includes(origin)) if (whitList.includes(origin)) { res.setHeader('Access-Control-Allow-Origin', origin) // 设置容许哪一个域访问 res.setHeader('Access-Control-Allow-Methods', 'PUT') // 设置容许哪一种请求方法访问 } next() }) app.put('/request', (req, res) => { res.end('server ok') }) app.listen(8002)
整个过程发送了两次请求,跨域成功。
固然,还能够设置其余参数:
该字段是一个逗号分隔的字符串,指定浏览器 CORS 请求会额外发送的头信息字段
表示是否容许发送 Cookie。默认状况下,Cookie 不包括在 CORS 请求之中。
CORS 请求时,XMLHttpRequest 对象的 getResponseHeader()方法只能拿到 6 个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。若是想拿到其余字段,就必须在 Access-Control-Expose-Headers 里面指定。
用来指定本次预检请求的有效期,单位为秒。有效期是 20 天(1728000 秒),即容许缓存该条回应 1728000 秒(即 20 天),在此期间,不用发出另外一条预检请求。
实现原理:同源策略是浏览器须要遵循的标准,而若是是服务器向服务器请求就没有跨域一说。
代理服务器,须要作如下几个步骤:
此次咱们使用 express 中间件 http-proxy-middleware 来代理跨域, 转发请求和响应
案例三个文件都在同一级目录下:
index.html
let xhr = new XMLHttpRequest() xhr.open('GET', '/api/request') xhr.onreadystatechange = () => { if (xhr.readyState === 4 && xhr.status === 200) { console.log('请求成功,结果是:', xhr.responseText) // request success } } xhr.send(null)
nodeMdServer.js
const express = require('express') const { createProxyMiddleware } = require('http-proxy-middleware') const app = express() // 设置静态资源 app.use(express.static(__dirname)) // 使用代理 app.use( '/api', createProxyMiddleware({ target: 'http://localhost:8002', pathRewrite: { '^/api': '', // 重写路径 }, changeOrigin: true, }) ) app.listen(8001)
nodeServer.js
const express = require('express') const app = express() app.get('/request', (req, res) => { res.end('request success') }) app.listen(8002)
运行http://localhost:8001/index.html
,跨域成功
日常 vue/react 项目配置 webpack-dev-server 的时候也是经过 Node proxy 代理的方式来解决的。
实现原理相似于 Node 中间件代理,须要你搭建一个中转 nginx 服务器,用于转发请求。
这种方式只需修改 Nginx 的配置便可解决跨域问题,前端除了接口换成对应形式,而后先后端不须要修改做其余修改。
实现思路:经过 nginx 配置一个代理服务器(同域不一样端口)作跳板机,反向代理要跨域的域名,这样能够修改 cookie 中 domain 信息,方便当前域 cookie 写入,实现跨域登陆。
nginx 目录下的 nginx.conf 修改以下:
// proxy服务器 server { listen 80; server_name www.domain1.com; location / { proxy_pass http://www.domain2.com:8080; # 反向代理 proxy_cookie_domain www.domain2.com www.domain1.com; # 修改cookie里域名 index index.html index.htm; # 当用webpack-dev-server等中间件代理接口访问nignx时,此时无浏览器参与,故没有同源限制,下面的跨域配置可不启用 add_header Access-Control-Allow-Origin http://www.domain1.com; # 当前端只跨域不带cookie时,可为* add_header Access-Control-Allow-Credentials true; } }
启动 Nginx
index.html
var xhr = new XMLHttpRequest() // 前端开关:浏览器是否读写cookie xhr.withCredentials = true // 访问nginx中的代理服务器 xhr.open('get', 'http://www.domain1.com:81/?user=admin', true) xhr.send()
server.js
var http = require('http') var server = http.createServer() var qs = require('querystring') server.on('request', function (req, res) { var params = qs.parse(req.url.substring(2)) // 向前台写cookie res.writeHead(200, { 'Set-Cookie': 'l=123456;Path=/;Domain=www.domain2.com;HttpOnly', // HttpOnly:脚本没法读取 }) res.write(JSON.stringify(params)) res.end() }) server.listen(8080)
原理:利用了 script 标签可跨域的特性,在客户端定义一个回调函数(全局函数),请求服务端返回该回调函数的调用,并将服务端的数据以该回调函数参数的形式传递过来,而后该函数就被执行了。该方法须要服务端配合完成。
实现步骤:
jsonp.html
function getInfo(data) { console.log(data) // 告诉你一声, jsonp跨域成功 } let script = document.createElement('script') script.src = 'http://localhost:3000?callback=getInfo' // document.body.appendChild(script)
server.js
const express = require('express') const app = express() app.get('/', (req, res) => { let { callback } = req.query res.end(`${callback}('告诉你一声, jsonp跨域成功')`) }) app.listen(3000)
jQuery 的 $.ajax() 方法当中集成了 JSONP 的实现,在此就不写出来了。
在开发中可能会遇到多个 JSONP 请求的回调函数名是相同的,并且这种方式用起来也麻烦,故咱们本身封装一个 jsonp 函数
function jsonp({ url, params, callback }) { return new Promise((resolve, reject) => { let script = document.createElement('script') // 定义全局回调函数 window[callback] = function (data) { resolve(data) document.body.removeChild(script) // 调用完毕即删除 } params = { callback, ...params } // {callback: "getInfo", name: "jacky"} let paramsArr = [] for (const key in params) { paramsArr.push(`${key}=${params[key]}`) } script.src = `${url}?${paramsArr.join('&')}` // http://localhost:3000/?callback=getInfo&name=jacky document.body.appendChild(script) }) } jsonp({ url: 'http://localhost:3000', params: { name: 'jacky', }, callback: 'getInfo', }).then(res => { console.log(res) // 告诉你一声, jsonp跨域成功 })
服务端解构的时候能够取出参数
app.get('/', (req, res) => { let { callback, name } = req.query res.end(`${callback}('告诉你一声, jsonp跨域成功')`) })
优势:兼容性好
缺点:因为 script 自己的限制,该跨域方式仅支持 get 请求,且不安全可能遭受 XSS 攻击
postMessage 是 HTML5 XMLHttpRequest Level 2 中的 API,且是为数很少能够跨域操做的 window 属性之一,它可用于解决如下方面的问题:
总之,它能够容许来自不一样源的脚本采用异步方式进行有限的通讯,能够实现跨文本档、多窗口、跨域消息传递。
otherWindow.postMessage(message, targetOrigin, [transfer]);
此次咱们把两个 html 文件挂到两个 server 下,采起 fs 读取的方式引入,运行两个 js 文件
postMessage1.html
<body> <iframe src="http://localhost:8002" frameborder="0" id="frame" onLoad="load()"></iframe> <script> function load() { let frame = document.getElementById('frame') frame.contentWindow.postMessage('你好,我是postMessage1', 'http://localhost:8002') //发送数据 window.onmessage = function (e) { //接受返回数据 console.log(e.data) // 你好,我是postMessage2 } } </script> </body>
postMsgServer1.js
const express = require('express') const fs = require('fs') const app = express() app.get('/', (req, res) => { const html = fs.readFileSync('./postMessage1.html', 'utf8') res.end(html) }) app.listen(8001, (req, res) => { console.log('server listening on 8001') })
postMessage2.html
<body> <script> window.onmessage = function (e) { console.log(e.data) // 你好,我是postMessage1 e.source.postMessage('你好,我是postMessage2', e.origin) } </script> </body>
postMsgServer2.js
const express = require('express') const fs = require('fs') const app = express() app.get('/', (req, res) => { const html = fs.readFileSync('./postMessage2.html', 'utf8') res.end(html) }) app.listen(8002, (req, res) => { console.log('server listening on 8002') })
WebSocket 是一种网络通讯协议。它实现了浏览器与服务器全双工通讯,同时容许跨域通信,长链接方式不受跨域影响。因为原生 WebSocket API 使用起来不太方便,咱们通常都会使用第三方库如 ws。
Web 浏览器和服务器都必须实现 WebSockets 协议来创建和维护链接。因为 WebSockets 链接长期存在,与典型的 HTTP 链接不一样,对服务器有重要的影响。
socket.html(http://127.0.0.1:5500/socket.html
)
let socket = new WebSocket('ws://localhost:8001') socket.onopen = function () { socket.send('向服务端发送数据') } socket.onmessage = function (e) { console.log(e.data) // 服务端传给你的数据 }
运行nodeServer.js
const express = require('express') const WebSocket = require('ws') const app = express() let wsServer = new WebSocket.Server({ port: 8001 }) wsServer.on('connection', function (ws) { ws.on('message', function (data) { console.log(data) // 向服务端发送数据 ws.send('服务端传给你的数据') }) })
这种方式只能用于二级域名相同的状况下。
好比 a.test.com 和 b.test.com 就属于二级域名,它们都是 test.com 的子域
只须要给页面添加 document.domain ='test.com' 表示二级域名都相同就能够实现跨域。
好比:页面 a.test.com:3000/test1.html 获取页面 b.test.com:3000/test2.html 中 a 的值
test1.html
<body> <iframe src="http://b.test.com:3000/test2.html" frameborder="0" onload="load()" id="iframe" ></iframe> <script> document.domain = 'test.com' function load() { console.log(iframe.contentWindow.a) } </script> </body>
test2.html
document.domain = 'test.com' var a = 10
浏览器具备这样一个特性:同一个标签页或者同一个 iframe 框架加载过的页面共享相同的 window.name 属性值。在同个标签页里,name 值在不一样的页面加载后也依旧存在,这些页面上 window.name 属性值都是相同的。利用这些特性,就能够将这个属性做为在不一样页面之间传递数据的介质。
因为安全缘由,浏览器始终会保持 window.name 是 string 类型。
打开http://localhost:8001/a.html
<body> <iframe src="http://localhost:8002/c.html" frameborder="0" onload="load()" id="iframe" ></iframe> <script> let first = true function load() { if (first) { // 第1次onload(跨域页)成功后,切换到同域代理页面 let iframe = document.getElementById('iframe') iframe.src = 'http://localhost:8001/b.html' first = false } else { // 第2次onload(同域b.html页)成功后,读取同域window.name中数据 console.log(iframe.contentWindow.name) // 我是c.html里的数据 } } </script> </body>
<body> <script> window.name = '我是c.html里的数据' </script> </body>
c 页面给 window.name 设置了值, 即使 c 页面销毁,但 name 值不会被销毁;a 页面依旧可以获得 window.name。
实现原理: a.html 欲与 c.html 跨域相互通讯,经过中间页 b.html 来实现。 三个页面,不一样域之间利用 iframe 的 location.hash 传值,相同域之间直接 js 访问来通讯。
具体实现步骤:一开始 a.html 给 c.html 传一个 hash 值,而后 c.html 收到 hash 值后,再把 hash 值传递给 b.html,最后 b.html 将结果放到 a.html 的 hash 值中。
一样的,a.html 和 b.html 是同域的,都是http://localhost:8001,也就是说 b 的 hash 值能够直接复制给 a 的 hash。c.html 为http://localhost:8002下的
a.html
<body> <iframe src="http://localhost:8002/c.html#jackylin" style="display: none;"></iframe> <script> window.onhashchange = function () { // 检测hash的变化 console.log(456, location.hash) // #monkey } </script> </body>
b.html
window.parent.parent.location.hash = location.hash // b.html将结果放到a.html的hash值中,b.html可经过parent.parent访问a.html页面
c.html
console.log(location.hash) // #jackylin let iframe = document.createElement('iframe') iframe.src = 'http://localhost:8001/b.html#monkey' document.body.appendChild(iframe)