1. 什么是跨域?
想象一下,你住在一个小区里,小区有门禁系统,只允许本小区的住户自由进出。如果外来的访客想进入,需要登记或获得许可。浏览器的跨域问题,就是这个“门禁系统”在起作用!
浏览器的安全策略规定:不同源的网站之间,默认不能互相访问数据(比如读取页面内容、发送请求等)。这是为了防止恶意网站窃取用户隐私(比如你的登录信息)。
2. 什么是同源策略?
“同源”是指两个网页的以下三个部分完全一致:
- 协议(如
httpvshttps) - 域名(如
example.comvsapi.example.com) - 端口(如
80vs8080)
例子:
https://www.example.com和http://www.example.com→ 不同源(协议不同)https://example.com和https://www.example.com→ 不同源(域名不同)https://example.com:80和https://example.com:8080→ 不同源(端口不同)
同源策略
1. 浏览器出于安全考虑(数据安全,服务器安全,减少XSS,CSRF攻击),
2. http:// www. abc.com : 8000 / a.html
协议 子域名 主域名 端口 路径
3. 非同源请求发送后,浏览器会拦截响应
3. 跨域的方案
3.1 jsonp:
- 借助script 标签src属性不受同源策略的限制,来发送请求
- 携带一个参数callback给到后端
- 后端将数据作为callback函数的实参,返回给前端一个callback的调用形式
- 浏览器接收到callback的调用会自动执行全局的callback函数
代码演示:
前端代码:
<body>
<button onclick="handle()">请求</button> <!-- 点击按钮触发请求 -->
<img src="" alt=""> <!-- 示例图片,与 JSONP 无关 -->
<script>
// JSONP 封装函数
function jsonp(url, cb) {
return new Promise((resolve, reject) => {
// 1. 创建一个 <script> 标签
const script = document.createElement('script');
// 2. 在 window 对象上挂载一个以 cb 命名的回调函数
// 后端返回的数据会触发此函数
window[cb] = function (data) {
resolve(data); // 将后端返回的数据传递给 Promise 的 resolve
// 可选:请求完成后移除 script 标签和 window 上的回调函数
script.remove();
delete window[cb];
}
// 3. 设置 script 的 src,拼接回调函数名到 URL 参数中
// 例如:http://localhost:3000?cb=callback
script.src = `${url}?cb=${cb}`;
// 4. 将 script 标签插入页面,触发请求
document.body.appendChild(script);
})
}
// 按钮点击事件处理函数
function handle() {
// 调用 jsonp 函数,传入后端地址和回调函数名
jsonp('http://localhost:3000', 'callback').then(res => {
console.log(res); // 输出后端返回的数据:hello world
})
}
</script>
</body>
后端代码
const http = require('http');
// 创建 HTTP 服务器
http.createServer((req, res) => {
// 解析请求的 URL,获取查询参数
const url = new URL(req.url, `http://${req.headers.host}`);
const query = url.searchParams;
// 检查是否存在 'cb' 参数(前端传递的回调函数名)
if (query.get('cb')) {
const cb = query.get('cb'); // 获取回调函数名,这里是 'callback'
const data = 'hello world'; // 要返回的数据
// 拼接一个函数调用的字符串,例如:callback("hello world")
const result = `${cb}("${data}")`;
// 设置响应头(可选,但建议设置 Content-Type)
res.setHeader('Content-Type', 'application/javascript');
// 返回结果,前端会将其作为 JS 代码执行
res.end(result);
}
}).listen(3000); // 监听 3000 端口
代码执行流程
-
前端点击按钮,调用
handle()函数。 -
前端发起 JSONP 请求:
- 创建一个
<script>标签,设置src为http://localhost:3000?cb=callback。 - 定义全局函数
window.callback,用于接收数据。
- 创建一个
-
后端收到请求:
- 解析 URL,获取
cb=callback。 - 返回字符串
callback("hello world")。
- 解析 URL,获取
-
前端执行返回的 JS 代码:
- 自动调用
window.callback("hello world")。 Promise的resolve被触发,数据传递给then中的回调函数。
- 自动调用
-
控制台输出:
hello world。
缺点:
- 必须要前后端配合
- 只能发送get请求
- 不安全,容易受到xss攻击
3.2 CORS(跨域资源共享) (主流方案)
- 原理:服务器明确告诉浏览器“允许哪些来源访问我”。
- 实现:后端在响应头中添加字段,例如:
Access-Control-Allow-Origin: http://my-site.com // 允许特定来源
Access-Control-Allow-Origin: * // 允许所有来源(慎用!)
- 适用场景:前后端分离开发、调用第三方 API。
代码演示
const http = require('http');
const server = http.createServer((req, res) => {
res.writeHead(200, {
'access-control-allow-origin': 'http://127.0.0.1:5500', // 允许所有域名跨域
})
res.end('hello world');
})
server.listen(3000)
3.3 nginx 反向代理
-
原理:让同源的服务器替你转发请求。
- 你的前端请求
http://my-site.com/proxy-api。 - 你的服务器(
my-site.com)转发请求到http://api.com,返回结果给前端。
- 你的前端请求
-
适用场景:前端开发时可用 webpack-dev-server 的代理功能;生产环境用 Nginx 反向代理。
3.4 websocket
- 传统的前后端通讯是基于http协议的,是单向的,只能从一端发到另一端,无法双向通信
- websocket 是基于tcp协议,是双向的,可以从一端发送到另一端,也可以从另一端发送到一端
- socket协议一旦建立连接,就可以一直保持通信状态,不需要每次都建立连接
- 天生就可以跨域
前端代码
<body>
<script>
// WebSocket 测试函数
function webSocketTest(url, params = {}) {
return new Promise((resolve, reject) => {
// 1. 创建一个 WebSocket 实例,连接到指定的 URL
const socket = new WebSocket(url);
// 2. 监听 WebSocket 连接成功事件
socket.onopen = () => {
// 连接成功后,发送数据到服务器
socket.send(JSON.stringify(params)); // 将参数对象转为 JSON 字符串发送
};
// 3. 监听 WebSocket 接收到消息的事件
socket.onmessage = (event) => {
console.log(event.data); // 打印服务器返回的数据
// 将接收到的数据传递给 Promise 的 resolve
resolve(event.data);
};
// 4. 监听 WebSocket 错误事件
socket.onerror = (error) => {
reject(error); // 将错误传递给 Promise 的 reject
};
// 5. 监听 WebSocket 关闭事件
socket.onclose = () => {
console.log('WebSocket 连接关闭');
};
});
}
// 调用 WebSocket 测试函数
webSocketTest('ws://localhost:3000', { age: 18 }).then(res => {
console.log(res); // 打印服务器返回的数据
});
</script>
</body>
后端代码:
const WebSocket = require('ws'); // 引入 WebSocket 库
// 在 3000 端口上创建 WebSocket 服务器
const ws = new WebSocket.Server({ port: 3000 });
let count = 0; // 定义一个计数器变量
// 监听客户端连接事件
ws.on('connection', (obj) => {
// obj 是当前连接的客户端对象
// 监听客户端发送的消息
obj.on('message', (msg) => {
// msg 是客户端发送的消息(Buffer 类型)
console.log(msg.toString()); // 将消息转为字符串并打印
// 向客户端发送一条消息
obj.send('收到了');
// 设置一个定时器,每隔 2 秒向客户端发送一次计数器的值
setInterval(() => {
count++; // 计数器自增
obj.send(count.toString()); // 将计数器的值转为字符串并发送
}, 2000);
});
// 监听客户端断开连接事件
obj.on('close', () => {
console.log('客户端断开连接');
});
});
代码执行流程
-
前端调用
webSocketTest函数:- 创建一个 WebSocket 实例,连接到
ws://localhost:3000。 - 连接成功后,发送数据
{ age: 18 }到服务器。
- 创建一个 WebSocket 实例,连接到
-
后端接收到连接:
- 监听
connection事件,获取客户端对象obj。 - 监听
message事件,接收客户端发送的消息,并打印{ age: 18 }。 - 向客户端发送一条消息
'收到了'。 - 启动定时器,每隔 2 秒向客户端发送一次计数器的值。
- 监听
-
前端接收到消息:
- 打印服务器返回的消息(
'收到了'和计数器的值)。 - 将消息传递给
Promise的resolve,触发then中的回调函数。
- 打印服务器返回的消息(
关键点总结
-
WebSocket 特点:
- 全双工通信:客户端和服务器可以同时发送和接收数据。
- 长连接:连接建立后,客户端和服务器可以持续通信。
- 适合实时应用:如聊天室、实时数据推送等。
-
前端核心:
new WebSocket(url):创建 WebSocket 实例。socket.send(data):发送数据。socket.onmessage:接收数据。
-
后端核心:
new WebSocket.Server({ port }):创建 WebSocket 服务器。ws.on('connection'):监听客户端连接。obj.on('message'):接收客户端消息。obj.send(data):向客户端发送数据。
3.5 postMessage
当父级页面和iframe页面不在同一个域名下,它们之间的数据传输也存在跨域问题,父级页面和iframe页面之间通过postMessage来通信
3.6 document.domain
同上(谷歌禁止)
4. 总结
- 跨域是浏览器的安全机制,防止恶意网站窃取数据。
- 同源要求协议、域名、端口一致。
- 常用解决方案:CORS(后端设置响应头)、代理服务器、JSONP(已过时)。