跨域是开发中经常会遇到的一个场景,也是面试中经常会讨论的一个问题。掌握常见的跨域解决方案及其背后的原理,不仅可以提高我们的开发效率,还能在面试中表现的更加游刃有余。
因此今天就来和大家从前端的角度来聊聊解决跨域常见的几种方式。
在讲跨域之前,我们先来看看URL的组成内容:
一个URL的组成,通常包含协议、主机名、端口号、路径、查询参数和锚点几个部分。
这里展示了一个URL的示例:
https://www.example.com:8080/path/resource.html?page=1&sort=desc#header
在上述示例中:
● 协议为HTTPS
● 主机名为www.example.com
● 端口号为8080
● 路径为/path/resource.html
● 查询参数为page=1&sort=desc
● 锚点为header
所谓跨域,指的是请求URL中协议、主机名、端口号中任意一个部分不相同。
以上述URL为例,下面几种写法都算是和它跨域:
ttp://www.example.com:8080/ // 协议不同
https://www.example.a.com:8080/ // 主机名不同
https://www.example.com:8081/ // 端口号不同
其实跨域问题的出现是受限于浏览器的同源策略。
所谓同源策略,其实是浏览器的一种安全机制,用于限制一个网页中的网络请求仅能够访问来自同一源(域名、协议和端口号均相同)的资源,主要目的是防止恶意网站通过脚本窃取其他网站的敏感数据,保障用户的隐私和安全。
当浏览器端的脚本(js文件)访问了其他域的网络资源时,就会出现跨域问题。
前文说到,跨域问题的出现是受限于浏览器的同源策略,那么常见的解决跨域问题的方案,其实也是围绕着浏览器展开的:
在我们平常的开发中,解决跨域问题最常使用的方案是使用代理服务器。
代理服务器解决跨域问题其实是抓住了同源策略只受限于浏览器访问服务器,对于服务器访问服务器并没有限制的特点,作为中间服务器做了一个请求转发的功能。
具体来说,就是前端工程师编写的网页运行在由webpack等脚手架搭建的代理服务器上,当前端网页在浏览器中发起网络请求时,其实这个请求是发送到代理服务器上的,然后代理服务器会将请求转发给目标服务器,再将目标服务器返回的响应转发给客户端。
代理服务器在此过程中扮演了一个中转的角色,可以对请求和响应进行一些修改、过滤和拦截,以实现一些特定的功能。因为前端网页运行在代理服务器上,所以不存在跨域问题。
那么在线上环境和开发环境下,代理服务器是如何做请求转发的呢?
在线上环境下,我们一般会采用nginx来做反向代理,从而把前端的请求转发到目标接口上。
nginx是一个轻量级高并发的web服务器,基于事件驱动,而且跨平台,window和Linux都可以进行配置。
它作为代理服务器来解决开发中的跨域问题的主要方法就是监听线上前端网址的运行端口,然后碰到包含特殊标记的请求后就进行请求转发。
在开发环境下,无论是借助于webpack还是使用vite或其他脚手架搭建的前端项目,解决跨域问题的核心是借助http-proxy-middleware中间件实现的。而http-proxy-middleware中间件的核心又是对http-proxy的进一步封装。
这里先展示一下在项目中使用http-proxy-middleware来实现请求转发功能的示例代码:
const { createProxyMiddleware } = require('http-proxy-middleware');
module.exports = {
server: {
proxy: {
// 将 /api/* 的请求代理到 http://localhost:3000/*
'/api': {
target: 'http://localhost:3000',
changeOrigin: true,
pathRewrite: { '^/api': '/' }
}
}
}
};
接着我们可以自己使用原生node,借助http-proxy库来搭建一个具有请求转发功能的代理服务器Demo,感兴趣的朋友可以自己测试玩玩:
1. 首先需要创建一个空文件夹(全英命名)作为项目文件夹,然后使用npm init -y命令将项目升级为node的项目:
npm init -y
2. 接着在项目根目录下创建一个index.html文件用于发起跨域请求:
DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>请求转发测试title>
head>
<body>
<h1>请求转发测试h1>
<p id="message">p>
<script>
fetch('/api/login')
.then(response => response.text())
.then(data => {
document.getElementById('message').textContent = data;
});
script>
body>
html>
3. 接着在项目根目录下新建index.js文件来编写服务端的代码。
index.js文件是实现具有请求转发功能的代理服务器的核心文件。
const http = require('http');
const httpProxy = require('http-proxy');
const fs = require('fs');
const path = require('path');
// 创建代理服务器实例
const proxy = httpProxy.createProxyServer({});
// 创建HTTP服务器
const server = http.createServer((req, res) => {
if (req.url === '/' || req.url.endsWith('.html')) {
// 读取HTML文件
const filename = path.join(__dirname, 'index.html');
fs.readFile(filename, 'utf8', (err, data) => {
if (err) {
res.writeHead(500);
res.end('Error reading HTML file');
} else {
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(data);
}
});
} else if (req.url.startsWith('/api')) {
// 重写路径,替换跨域关键词
req.url = req.url.replace(/^\/api/, '');
// 将请求转发至目标服务器
proxy.web(req, res, {
target: 'http://localhost:3000/',
changeOrigin: true,
});
}
});
// 监听端口
server.listen(8080, () => {
console.log('Server started on port 8080');
});
4. 接着编写目标服务器target.js文件的内容,用于测试跨域访问:
const http = require('http');
const server = http.createServer((req, res) => {
if (req.url.startsWith('/login')) {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('我是localhost主机3000端口下的方法,恭喜你访问成功!');
} else {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Hello, world!');
}
});
server.listen(3000, () => {
console.log('Target server is listening on port:3000');
})
5. 打开终端,输入启动目标服务器的命令:
node ./target.js //项目根目录下执行
6. 再开一个终端启动代理服务器,等待浏览器端发起请求就可以啦:
node ./index.js //项目根目录下执行
7. 最后在浏览器里访问http://localhost:8080, 打开控制台即可查看效果:
可以发现,浏览器network模块的网络请求确实是访问的8080端口的方法,但是我们的服务器默默的做了请求转发的功能,并将请求转发获取到的内容返回到了前端页面上。
其实http-proxy是对node内置库http的进一步封装,网络请求的核心部分还是使用http创建一个服务器对象去访问的。感兴趣的同学可以再读读http-proxy的源码~
除了代理服务器这种绕过浏览器同源策略的解决方式外,从前端的角度解决跨域问题还有如下一些常见的方法:
JSONP的原理是通过动态创建