看清现象说本质: 跨域
五一假期结束,内卷即将开始。
在五一之前,看了阿里大佬代金权对跨域知识点的讲解(vite 跨域代理配置);在五一期间,也看了 coderwhy 老师对跨域知识点的讲解(webpack 跨域代理配置)。突然间,感觉自己对跨域的内功更加的浑厚了,让我手痒的想,记录下此秘诀。
webpack 和 vite 的代理配置截图:
好了,话不多说,直接开始。
跨域的理论描述
从两个方面来论证。
前端的发展历史
- 早期的服务器端渲染,是没有跨域问题的(服务端返回字符串进行渲染)。
- 随着的前后端分离,前端的代码和服务端的接口是分开的,甚至部署在不同的服务器上,那么这时候跨域就产生了。
跨域的知底解析
上面的前端发展历史中可知,客户端会调用服务端提供的接口,也就是通过浏览器会发起请求,而浏览器为了保证安全性,提供了一种同源策略
的方针。
同源策略是一个重要的安全策略,它用于限制一个源的文档或者它加载的脚本如何能与另一个源的资源进行交互。它能帮助阻隔恶意文档,减少可能被攻击的媒介。
满足同源策略的条件:三者一致。
- 协议(protocol)
- 主机(host)
- 端口(port)
只要三者都相同,那么浏览器就能验证通过;反之,验证失败。
跨域案例演示
在早期的时候,浏览器采用 XMLHTTPRequest
技术实现不刷新页面的情况下请求特定 URL,获取数据。(Ajax 编程实现 )
随着浏览器的发展和完善,浏览器推出了一项新的技术 fetch
,基于 promise,也可以实现发送请求,获取数据。
模拟跨域案例:
- koa 搭建服务器
// server/index.js
const Koa = require("koa");
const KoaRouter = require("@koa/router");
const app = new Koa();
const router = new KoaRouter({ prefix: "/user" });
router.get("/list", (ctx) => {
ctx.body = [
{ name: "copyer", id: 1 },
{ name: "james", id: 2 },
];
});
app.use(router.routes());
app.use(router.allowedMethods());
app.listen(8000, () => {
console.log("The Server is running");
});
创建一个 http://localhost:8000 的服务器,接口地址为 /user/list
- 客服端请求接口,然后通过 Live Server 运行 html 文件,打开一个端口为 5500 的服务器
<!-- client/index.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>跨域测试</title>
</head>
<body>
<h1>跨域静态资源</h1>
<script>
// 方式一:采用 XMLHTTPRequest 方式
const xhr = new XMLHttpRequest();
xhr.open("GET", "http://localhost:8000/user/list", true);
xhr.onreadystatechange = function () {
if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
console.log(JSON.parse(xhr.responseText));
}
};
xhr.send();
// 方式二:采用 fetch 方式
fetch("http://localhost:8000/user/list")
.then((res) => res.json())
.then((res) => {
console.log(res);
});
</script>
</body>
</html>
那么这时候,跨域就产生了。
经典的报错信息。既然出现了报错信息,那么就要解决,该怎么做呢?
跨域的解决方案
解决跨域的方案有很多种,但是常用的解决方案就两三种(开发常用),其他的偏方了解即可。
常用的解决方案本质
在了解解决方案之前,应该先了解一下,解决跨域方案的本质,了解了本质,才能更好理解跨域的解决方案。
解决方案本质一:
跨域由同源策略而诞生,那么想要解决跨域,满足同源策略就可以从根本去除。
- 部署在统一服务器,协议,主机,端口都是一致,不存在跨域。
- 服务器之间的代理,同源协议是由浏览器产生的,但是针对服务器是没有同源策略的,可以用来请求和接受(也会存在不足的地方,代理服务器与客户端也会存在跨域,代理的目的也许就是为了让接口服务器更加的存粹吧,个人感觉)
解决方案本质二:
跨源资源共享(CORS:Cross-Origin Resource Sharing):是一种基于 http 头的机制,该机制通过允许服务器标示除了它自己以外的其他源(域、协议或端口),使得浏览器允许这些源访问加载自己的资源。
简单的理解就是:在 http 的头部信息设置一个凭证,供浏览器的识别,返回获取的资源。
看一种场景:
跨域的凭证类似上面的场景:
小结:
跨域常用的解决方案实现思路就从上面的三点来触发(也可以说成两种,因为代理的解决方案也需要借助其他形式的解决方案)。
常用的解决方案
1、部署在同一个服务器
把前端代码部署在同一服务器,那么这时候的接口请求就完成满足了同源策略,所以可以获取数据。
那么服务端应该怎么做呢?就是把前端代码当初静态资源处理,直接访问服务器的静态资源(跟图片类似)
const Koa = require("koa");
const KoaRouter = require("@koa/router");
+ const static = require("koa-static");
...
+ app.use(static('../client'))// 这里是根据我的目录结构来编写的
...
app.listen(8000, () => {
console.log("The Server is running");
});
通过 http://localhost:8000 就可以访问 client/index.html 文件,在控制台就会发现数据已经获取到了。
2、CORS
它是一种基于 http header 的机制。就是配置一个凭证(Access-Control-Allow-Origin)
// 指定某一个(http://localhost:5500)
Access-Control-Allow-Origin: http://localhost:5500
// 指定所有
Access-Control-Allow-Origin: *
浏览器又把 cors 请求分为两种:简单请求和非简单请求
简单请求和非简单请求的区别:就是是否需要先进行 cors 预检请求(preflight request)
满足下面条件的是简单请求,否则就是非简单请求:
请求方式:HEAD、GET、POST
HTTP 的头部信息不超出以下几种字段:
- Access
- Accept-Language
- Content-Language
- Content-Type(三种):
text/plain
,multipart/form-data
,application/x-www-form-urlencoded
- Range
针对简单请求,设置了 Access-Control-Allow-Origin 就可以了。
针对非简单请求,除了设置上面的,还需要设置一些其他的头部信息:
- Access-Control-Allow-Headers (允许其他的头部信息)
- Access-Control-Allow-Credentials (携带cookies等信息)
- Access-Control-Allow-Methods (其他的请求方法)
那么服务端只需要简单的设置一下,即可
// 这里只是简单的配置一下简单请求
const Koa = require("koa");
const KoaRouter = require("@koa/router");
...
+ app.use((ctx, next) => {
+ // ctx.set("Access-Control-Allow-Origin", "*"); // 所有
+ ctx.set("Access-Control-Allow-Origin", "http://127.0.0.1:5500"); // 单独:http://127.0.0.1:5500
+ next();
+ });
...
app.listen(8000, () => {
console.log("The Server is running");
});
设置之后,直接运行 Live Server,也可以正常拿取到数据。
3、Node 代理服务器
在前面已经说到了,也可以通过代理来解决跨域问题(其本质是服务器与服务器之间是没有同源策略,是可以进行交互的),然后客户端只需要和代理服务器进行交互(当然这里也会存在跨域,需要解决)。
那么这里为什么叫做 Node 代理服务器呢?
因为现在无论是使用 React 开发,还是 Vue 开发,都使用到了脚手架(webpack / vite 等),在配置 webpack.config.js
或者 vite.config.js
都可以进行代理配置(proxy),那么它们内部底层的实现原理是什么?
你可能想到了,没错,无论是 webpack ,还是 vite 都是开启了一个 node 代理服务器,用来处理跨域的类似功能(当然不仅仅是这方面哈)。
- Webpack: 采用的是 Express 搭建的
- Vite: 采用的是 Koa 搭建的
简单的说说 webpack 吧,webpack 搭建的服务器叫做 webpack-dev-server, 里面内部借助了一个中间件 http-proxy-middleware
,可以简单的实现一下。
// proxy/index.js
const express = require("express");
const { createProxyMiddleware } = require("http-proxy-middleware");
const app = express();
app.use(express.static('../client')) // 部署同一资源,处理跨域
// 在代理的时候,一般前端就是配置 /api
app.use(
"/api",
createProxyMiddleware({
// 接口服务器
target: "http://localhost:8000",
// 去掉 /api
pathRewrite: {
"^/api": "",
},
// 是否改变源,其实也就是服务器对源的验证
//(设置了服务器得到的源来自于客户端,不设置服务器得到的源来自于代理服务器)
changeOrigin: true,
})
);
app.listen(3000, () => {
console.log("Express Server is running~");
});
- 使用 createProxyMiddleware 中间件,进行代理转化到接口服务器
- 代理服务器也会存在与客户端存在跨域,所以需要配置静态资源,处理跨域
<!-- 请求地址发生变化 -->
<script>
// 方式一:采用 XMLHTTPRequest 方式
const xhr = new XMLHttpRequest();
xhr.open("GET", "http://localhost:3000/api/user/list", true);
xhr.onreadystatechange = function () {
if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
console.log(JSON.parse(xhr.responseText));
}
};
xhr.send();
// 方式二:采用 fetch 方式
fetch("http://localhost:3000/api/user/list")
.then((res) => res.json())
.then((res) => {
console.log(res);
});
</script>
然后通过 http://localhost:3000 直接读取静态资源(index.html), 发现也会获取到数据,跨域也解决了。
Vite 的代理服务器的原理也基本类似。
4、Nginx 反向代理
Nginx 是一款轻量级的 Web 服务器、反向代理服务器,由于它的内存占用少,启动极快,高并发能力强,在互联网项目中广泛应用。
高大尚吧!!!
但是还是一个服务器,所以也可以借助这个服务器向接口服务器进行交互,然后再把资源返回给客户端。
Nginx 需要下载(windows 和 Mac 的方式不一样,自行百度搜索并下载,尝试)。
假设你们都安装好了 nginx 后,就可以启动 nginx,对其配置文件进行配置。
nginx #启动服务 http://localhost:8080 就可以看见 nginx 首页
nginx -s stop #停止服务(直接走)
nginx -s reload #重新加载
nginx -s reopen #重新启动
nginx -s quit #退出(处理完事情走)
所以要使用 nginx,那么就先启动 nginx;打开终端,输入 nginx,就可以启动了,输入 http://localhost:8080 就可以看见 nginx 的欢迎界面。
接下来,就是配置一下代理,代理到接口服务器。打开 nginx.conf
配置文件(修改之后,需要重启)
# 匹配 /nginx/ 路径
location /nginx/ {
add_header Access-Control-Allow-Origin *; # 解决跨域
proxy_pass http://localhost:8000/; # 设置代理路径
}
在这里啰嗦一句, proxy_pass 的代理路径后面添加
/
与否,是完全不同的。自己在这么摸索了一半天,才达到了自己效果// 测试路径:http://localhost:5500/nginx/user/list 加/,表示绝对根路径 没有/,表示相对路径,把匹配的路径部分也给代理走 // 情况一: location /nginx/ { proxy_pass http://locahost:8000/; // 代理路径:http://locahost:8000/user/list } // 情况二: location /nginx/ { proxy_pass http://locahost:8000; // 代理路径:http://locahost:8000/nginx/user/list } // 情况三: location /nginx/ { proxy_pass http://locahost:8000/abc/; // 代理路径:http://locahost:8000/abc/user/list } // 情况四: location /nginx/ { proxy_pass http://locahost:8000/abc; // 代理路径:http://locahost:8000/abcuser/list }
上面只是记录一下犯错的地方,及学习到的新东西。
回归正题,nginx 配置了,那么客户端请求代码也需要改一下
<script>
// 方式一:采用 XMLHTTPRequest 方式
const xhr = new XMLHttpRequest();
xhr.open("GET", "http://localhost:8080/nginx/user/list", true);
xhr.onreadystatechange = function () {
if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
console.log(JSON.parse(xhr.responseText));
}
};
xhr.send();
// 方式二:采用 fetch 方式
fetch("http://localhost:8080/nginx/user/list")
.then((res) => res.json())
.then((res) => {
console.log(res);
});
</script>
然后通过 Live Server 运行 html 文件,在控制台发现,获取资源成功,成功解决了跨域。
小结一下
在所有的跨域解决方案基本上都是和服务器息息相关的,无论是部署在同一个服务器,还是配置 http 头,甚至是搭建一个代理服务器,都是离不开服务器的知识的。
虽然前端开发人员不会去写服务器代码,但是或多或少的知道一点服务端知识是真的点赞,因为可以让你对前端的某些知识理解更加的透彻,而不是使劲的背八股文。
适用场景:
- Node 代理服务器的解法方案:适用于在开发阶段(因为 webpack 和 vite 是我们的伙伴)
- Nginx 代理服务器的解决方案:适用于在生产阶段
不常用的解决方案
- jsonp
- postMessage
- websocket
- ...
对于不常见的解决方案,可能也许对我来说,最熟悉的就是 jsonp,利用 script 标签,src属性里面传递回调函数,根据参数获取数据;
~~
不常用的方案,有兴趣的去了解一下即可(实现方案和实现本质)。
总结
站在两位老师的肩膀上,使其对跨域有了比较更加深层次的认知。从为什么产生跨域,到解决跨域的本质(实现思路),到最后跨域的解决方案的过程,形成了一个对跨域理解的体系结构。
希望你也学习到了吧。如果存在错误的地方,多多指教,共同进步!!!
转载自:https://juejin.cn/post/7229923818347954213