前言
跨域问题是我们在面试过程中经常容易被询问到的,今天我们来聊聊什么是跨域以及如何解决跨域。
我们先来聊聊跨域是什么?
比如说我们去访问百度的首页https://www.baidu.com/
,那么浏览器就会朝这个地址去发送一次网络请求。我们来看看这个地址是由哪几个部分组成的:
首先百度是对这段地址的域名去做了一个域名优化的,原来的地址是有端口号的,这才是一个正常的http地址,这里我们就假设为:
https://192.168.31.45:8080/user
这段地址分为四个部分:
协议号:域名:端口号 / 路径
现在我们来思考一个问题,如果该地址是百度的后端服务器的ip地址,只要我们朝该地址发请求,我们是否可以拿到数据?
如果真的可以随便拿到的话,那是不科学的。那么这些大公司的数据将毫无秘密可言。那么为了防止这个问题,所有浏览器都打造了一个同源策略。
同源策略
协议号-域名-端口号 都相同的地址,浏览器才认为是同源
我们来看看这两个地址,它们是同源地址吗?
是的,它们是同源地址,它们协议号-域名-端口号都相同,只是路径不一样。
公司的ip
都是公网ip
,所以公司之间的ip
是绝对不一样的,192.168.31.45这一段数字不可能相同。所以说,百度的程序员不可能去请求到腾讯的后端数据。
如果它们的协议号-域名-端口号有任何一个不相同,那么浏览器就会将返回的数据拦截下来,这就是跨域。
跨域
后端返回给浏览器的数据会被浏览器的同源策略给拦截下来,这就是跨域
假设百度的前端和字节的前端都去访问字节的后端,首先后端只要有请求都会响应,所以会出现很尴尬的情况。字节的后端同样会返还东西给百度的前端,浏览器就做了同源策略。如图所示,假设它们的地址为这样,由于百度的前端和字节的后端的协议-域名-端口号
三者不是完全相同,那么浏览器则将后端返回回来的响应拦截下来,并不会发给前端,这就是跨域。
这里需要注意一下,大公司的ip
地址是不会一样的,这是它们在万维网申请的,是全球唯一的。
同源策略的目的是数据安全,被浏览认为不是同一个源的请求,就拿不到响应。
解决跨域
为什么要解决跨域呢?
假设我在我们公司是干前端的,我们公司有个哥们是干后端的。我们两个负责搭配完成一个全栈项目。假设我把我的前端项目跑在http://192.168.31.1:8000
,后端那个哥们把后端跑在http://192.168.31.2:8000
。
虽然我们都连的是公司的局域网,但是域名最后一个数字还是会不一样的。
前端需要朝后端发送接口请求,那这样能请求到数据吗?答案是不能的,因为发生了跨域!所以就需要我们解决跨域,让我们在开发阶段好调试。
常用的解决跨域的办法有四种,我们需要掌握。
我们用代码来给大家演示一下:
我们来看看后端代码:
const Koa = require('koa'); const app = new Koa() const main = (ctx, next) => { ctx.body = { data: 'hello world' } } app.use(main) app.listen(3000, () => { console.log('listening on port 3000'); })
后端跑在localhost:3000上面。返回数据hello world
。
我们再来看看前端代码:
首先我们来看看前端的代码:
<html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <button id="btn">获取数据</button> <!-- <script src="http://localhost:3000"></script> --> <script> let btn = document.getElementById('btn'); btn.addEventListener('click', () => { // 发请求 fetch('http://localhost:3000') .then(res => res.json()) .then(res => { console.log(res); }) }); </script> </body> </html>
我们想要实现当我们点击按钮时,前端朝后端发请求,拿到数据并打印。
我们点击一下按钮试一试:
瞧,报错了,因为受到了浏览器的同源政策保护,跨域了,后端返回的响应被浏览器劫持了。
1. JSONP
首先我们要明白一点, ajax请求受同源策略的影响,但是script
上的src属性不受同源策略的影响,且该属性也会导致浏览器发送一个请求。
<html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <button id="btn">获取数据</button> <script src="http://localhost:3000"></script> <script> let btn = document.getElementById('btn'); btn.addEventListener('click', () => { }); </script> </body> </html>
我们通过在script
中添加src
属性也会发送一段请求,它是不受同源政策影响的:
这样,是不会发生跨域的。可能有些小伙伴们就想到了,我们有时候会通过CDN引入第三方源码,我们这样引入也没有报错, 比如直接引入vue:<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
如果通过script
去访问资源时也受同源策略影响,那么我们就没有办法引入第三方的库了。
现在我们就来通过这个script
去拿到数据:
<html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <button id="btn">获取数据</button> <!-- <script src="http://localhost:3000"></script> --> <script> function jsonp(url, cb){ return new Promise((resolve, reject) => { const script = document.createElement('script'); script.src = `${url}?cb=${cb}` // http://localhost:3000?cb=`callback` document.body.appendChild(script); }) } let btn = document.getElementById('btn'); btn.addEventListener('click', () => { // 发请求 jsonp('http://localhost:3000', 'callback') .then(res => { console.log('后端返回的结果' + res); }) }); </script> </body> </html>
我们自己封装一个jsonp
函数来用于发接口请求,这个jsonp
函数可以接受url
和cb
作为参数。首先生成一个<script>
标签,并且给script
添加一个src
属性,值为拼接后的url
和cb
,然后将该script
标签放到body
当中去,这样我们就确保了该jsonp
函数可以利用script
标签去发生请求。
前端代码就先写到这,我们点击按钮来测试一下:
我们成功的发送了一个请求。既然前端发送了一个请求给后端,那么后端就一定去接收到了请求。现在我们来到后端,后端应该成功接收到前端传过来的cb
字符串。我们到后端中打印这个参数来看一下:
// const Koa = require('koa'); // const app = new Koa() // const main = (ctx, next) => { // console.log(ctx.query); // { cb: 'callback' } // const cb = ctx.query.cb // const data = '给前端的数据' // const str = `${cb}('${data}')`; // 'callback("给前端的数据")' // ctx.body = str // } // app.use(main) // app.listen(3000, () => { // console.log('listening on port 3000'); // }) const Koa = require('koa'); const app = new Koa() const main = (ctx, next) => { console.log(ctx.query); ctx.body = { data: 'hello world' } } app.use(main) app.listen(3000, () => { console.log('listening on port 3000'); })
const Koa = require('koa'); const app = new Koa() const main = (ctx, next) => { console.log(ctx.query); // { cb: 'callback' } const cb = ctx.query.cb const data = '给前端的数据' const str = `${cb}('${data}')`; // 'callback("给前端的数据")' ctx.body = str } app.use(main) app.listen(3000, () => { console.log('listening on port 3000'); })
前端传了一个单词给我们后端,然后我们后端将给我的这个单词拼接为一个字符串再返回给前端。这里使用的是es6的模板字符串。大家可以看看代码上的注释,更好理解。我们返回给前端的str
像不像一个函数的调用呢?
我们再来看前端:
我们在全局的window
对象上挂上这个参数cb
,属性为该参数值,值为一个函数体。注意,我们这里只是声明,并没有调用。
<html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <button id="btn">获取数据</button> <!-- <script src="http://localhost:3000"></script> --> <script> function jsonp(url, cb){ return new Promise((resolve, reject) => { const script = document.createElement('script'); script.src = `${url}?cb=${cb}` // http://localhost:3000?cb=`callback` document.body.appendChild(script); window[cb] = (data) => { console.log(data) } // callback() // { // "callback": () => {} // } }) } let btn = document.getElementById('btn'); btn.addEventListener('click', () => { // 发请求 jsonp('http://localhost:3000', 'callback') .then(res => { console.log('后端返回的结果' + res); }) }); </script> </body> </html>
到这里,我们就已经可以看到效果了,我们点击按钮来看看打印:
我们的前端成功的拿到了后端的数据,这里有打印是因为我们挂载在window
上的cb
函数触发了。但是我们前端并没有去触发它,那么就是后端来触发的,并且将我们想要的数据作为参数来传给函数,这样才能打印出来数据。
我们来看看这个函数是怎么触发的:
const main = (ctx, next) => { console.log(ctx.query); // { cb: 'callback' } const cb = ctx.query.cb const data = '给前端的数据' const str = `${cb}('${data}')`; // 'callback("给前端的数据")' ctx.body = str }
前端将cb
挂在window
上,并且将cb
传给后端,后端就收到cb
,然后使用字符串模板进行拼接,使其变成一个函数的调用,将给前端的数据作为此函数的参数:'callback("给前端的数据")'
。然后将这个字符串传给前端,浏览器会将字符串执行成callback
的调用。
我们来看看后端的响应:
我们将console.log
换成resolve
,这样后面的.then
即可拿到后端的数据:
<html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <button id="btn">获取数据</button> <!-- <script src="http://localhost:3000"></script> --> <script> function jsonp(url, cb){ return new Promise((resolve, reject) => { const script = document.createElement('script'); script.src = `${url}?cb=${cb}` // http://localhost:3000?cb=`callback` document.body.appendChild(script); window[cb] = (data) => { resolve(data) } // callback() // { // "callback": () => {} // } }) } let btn = document.getElementById('btn'); btn.addEventListener('click', () => { // 发请求 jsonp('http://localhost:3000', 'callback') .then(res => { console.log('后端返回的结果:' + res); }) }); </script> </body> </html>
我给大家画张图来看一下:
- 借助script的src属性给后端发送一个请求,且携带一个
参数('callback')
- 前端在window对象上添加了一个
callback 函数
- 后端接收到了这个参数'callback'后,将要返回前端的数据data和这个参数 'callback' 进行拼接成
'callback(data)'
,并返回 - 因为window上已经有一个callback 函数,后端又返回了一个形如
'callback(data)'
,浏览器会将该字符串执行成``callback 的调用`
总结
- ajax请求受同源策略的影响,但是
<script>
上的src属性不受同源策略的影响,且该属性也会导致 浏览器发送一个请求
缺点:
- 必须要后端配合
- 只能用于get请求
jsonp
是第一种解决跨域的常见手段,接下来我们来看看第二种:
Cors (Cross-Origin Resource Sharing)
我们上面提到,跨域是因为浏览器的同源政策,导致浏览器不接受(或者说拦截)后端的响应,那么Cors
就是让浏览器不得不接受响应。
在http
协议中,任何一个http
请求都由两部分组成,一个是请求头,一个是请求体。请求头放着关于此次http请求的描述信息,请求体中装着传递的参数及数据包。
后端返回的是响应头和响应体,我们在响应头中添加一个字段'Access-Control-Allow-Origin':'*'
。
这相当于设置一个白名单,告诉浏览器不要拒绝接受后端的响应,让浏览器认为是一个同源。
const http = require('http'); const server = http.createServer((req, res) => { // 跨域是浏览器不接受后端的响应 // 想个办法,让浏览器不得接受 res.writeHead(200, { 'Access-Control-Allow-Origin': '*' // 白名单 }) let data = { msg: "hello cors" } res.end(JSON.stringify(data)) // 向前端返回数据 }) server.listen(3000, () => { console.log('listening on port 3000'); })
再来看看前端的代码,前端的代码还是没有变化:
<html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <button id="btn">获取数据</button> <script> let btn = document.getElementById('btn') btn.addEventListener('click', () => { fetch('http://localhost:3000', 'GET') .then(res => json()) .then(res => { console.log(res) }) }) </script> </body> </html>
当我们点击按钮:
成功的拿到后端返回的数据。
那小伙伴们可能又会有疑问了,这样设置白名单的话,是不是所有的前端都可以访问我们的后端了?
这是因为我们实际在开发自己项目中,我们会偷懒,'Access-Control-Allow-Origin': '*'
,设置为 *
号,相当于全部地址设置为白名单。
但是我们应该写成自己前端的ip
地址,例如我们这里写成'Access-Control-Allow-Origin': 'http://127.0.0.1:5500'
。这样的话,我们的前端就能正常的访问后端,同样可以限制别人的前端来访问我们的后端。
总结
Cors (Cross-Origin Resource Sharing) --- 后端通过设置响应头来告诉浏览器不要拒绝接受后端的响应
node代理
node代理也是我们常用的一种解决跨域的手段。
我们就拿vue
来举例一下,在我们写vue
的项目时,可以使用一个node代理
来解决跨域问题。我们用vite
来创建一个后端项目
<template> </template> <script setup> import axios from 'axios' import { onMounted } from 'vue' onMounted(() => { axios.get('http://localhost:3000') .then((res) => { console.log(res); }) }) </script> <style scoped> </style>
我们实现一个当一进去页面时,就朝后端发接口请求拿到数据。这里请求我们写在挂载阶段onMounted
当中,当组件挂载完毕后就会触发回调函数,发送请求。
这里我们使用的是axios
,我们需要安装一下依赖npm i axios
。
后端代码:
const http = require('http'); const server = http.createServer((req, res) => { let data = { msg: "hello cors" } res.end(JSON.stringify(data)) // 向前端返回数据 }) server.listen(3000, () => { console.log('listening on port 3000'); })
现在还是跨域的,因为我们前端和后端的地址不是同源的,它们的端口号不一样。
接下来我们就来到vite的配置项来解决跨域问题
vite.config.js
使用vite
创建的项目是有一个vite.config.js
的文件的:
import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' // https://vitejs.dev/config/ export default defineConfig({ plugins: [vue()], server: { proxy: { '/api': { target: 'http://localhost:3000', changeOrigin: true, rewrite: (path) => path.replace(/^\/api/, '') } } } })
来给大家解释一下这个vite
的配置项是什么样的:
首选vite
的源码是用node来写的,server
是和网络请求相关的配置,然后我们进行一个代理proxy
,只要我们朝/api
这个路径发送请求时,例如:axios.get('/api')
,那么就会将它转发到target
这个路径下来。如果后端本就有/api的路径的话,那么就帮我们去掉。
我们改动一下前端代码:
<template> </template> <script setup> import axios from 'axios' import { onMounted } from 'vue' onMounted(() => { axios.get('/api') .then((res) => { console.log(res); }) }) </script> <style scoped> </style>
再来页面输出看看:
看,我们成功拿到了数据。
我来给大家解释一下,同源策略只在浏览器上有,后端上是没有的。假设我们想要拿到气象局的天气预报数据,那么我们前端朝气象局的后端发送请求,那么一定是会跨域的。如果我们用node
写一个后端,在后端中朝着气象局的后端去发送请求,这个过程中不会经过浏览器,所以不会发生跨域。那么我只需要在后端中再使用一下Cors
,前端就能到自己的后端中拿到气象局的数据,这就是node代理
。
我们上面的vite
就是这么干的,vite
帮我们启动了一个node服务,且帮我们朝着localhost:3000
发起请求,因为后端没有同源策略,所以,vite中的node服务能直接请求到数据,再提供给前端使用。
注意,此时的vite
还帮创建出来的后端Cors
了一下的。
但是,vite只适合我们在开发阶段使用,因为vite
只是一个在开发阶段使用的构建工具,我们所写的项目最后是要打包上线的,最后vite
的整个源码都会被清除掉。
总结
因为后端没有同源政策,vite
创建一个node服务,所以node可以直接请求到后端的数据,再拿给前端。
但是vite
只能在开发环境生效。
nginx代理
nginx代理
跟Cors
的机制差不多,都是通过配置请求头中的字段来实现的,这需要在后端服务器上安装ngnix
,然后进行一个配置,只有源和配置的相等后端才进行响应。ngnix
不是js语法,而是Linux语法,属于操作系统的。
绝大多公司都是通过nginx代理
去解决跨域,我现在没有很好的办法来给大家演示,它主要用于项目上线时区去解决跨域,如果以后我写有关于项目部署的文章,再来跟大家好好聊聊。大家只要了解nginx
就行了,它的机制跟Cors
差不多。
以上四种就是我们常见的解决跨域的方法,它足够我们进行任何开发。不过面试官可能问你你还知道别的手段吗?那就是一些不常用的跨域方法,我们来简单介绍一下:
不常用的解决跨域的手段
domain
首先我们要知道iframe
的作用,它允许我们在一个html
中可以去嵌套另一个html
:
<html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <h2>父级页面</h2> <iframe src="./child.html" frameborder="0"></iframe> </body> </html>
childr.html
为我们的子级页面,它嵌套在当前html
当中。
在iframe中,当父级页面和子级页面的子域不同时,通过设置document.domain='xx'
来将xx定为基础域,从而实现跨域。
postMessage
当两个页面不再同一个域时,我们可以通过postMessage
来实现数据传输。(在iframe中
)
a.html代码:
<html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <h2>a.html</h2> <iframe src="http://127.0.0.1:5500/postMessage/b.html" frameborder="0" id="iframe"></iframe> <script> // 给b发送数据 let iframe = document.getElementById('iframe') iframe.onload = function(){ let data = { name: 'Tom' } iframe.contentWindow.postMessage(JSON.stringify(data), 'http://127.0.0.1:5500') } // 监听b传来的数据 window.addEventListener('message', function(e){ console.log(e.data); }) </script> </body> </html>
b.html代码:
<html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <h4>b.html</h2> <script> window.addEventListener('message', function(e){ console.log(JSON.parse(e.data)); if(e.data){ window.parent.postMessage('我接受到', 'http://127.0.0.1:5500') } }) </script> </body> </html>
大家可以试着打印一下,看看是否拿到了数据。
总结
JSONP、Cors、node代理、nginx代理这四种方法是我们常见的去解决跨域的手段,而这四种方法已经可以满足我们大部分的开发需求了。在面试过程中,大家主要去跟面试官说这四种方法,剩下的方法小伙伴们如果想到了也可以跟面试官讲。
写文章不易,如果帮助到了小伙伴们,可以给本文点赞收藏评论三连呀。有不懂的地方欢迎到评论区留言,我会及时回复。