一、问题背景
前后端分离的 web 项目,前端本地开发的 URL 大多是“http+localhost+端口”,如 http://localhost:3000。用此 URL 开发,会遇到三类问题。一是跨域,二是 cookie 种植,三是调用外部服务需要真实域名。
跨域问题。如果你遇到跨域问题,大概率就会看过如下图片,此时浏览器不让你把请求发送到出去。
为什么会这样?背后的原因是浏览器的同源策略(Same-origin policy)。URL 包括协议、域名和端口。同源策略是指两个源相互之间只有同源,换句话说就是协议、域名和端口都一致,才能相互通信。这样做本意是保证请求的安全,减少恶意攻击,但也给开发带来了麻烦。
cookie 种植问题。如果前后端的鉴权涉及 cookie,那 localhost 之类的域名会影响 cookie 的种植。因为浏览器能否正确种植下 cookie,受到 domain 的影响。
调用外部服务需要真实域名。当我们项目调用外部服务时,经常需要完整的真实域名,比如微信登录。此时本地开发没有完整的真实域名,就无法在本地联调。
那么,该如何解决这些问题呢?
二、解决方案
1. 后端配置 CORS
CORS(跨源资源共享)是一种基于 HTTP 请求头的机制,该机制通过允许服务器标示除了它自己以外的其他源。因此,只要后端在 HTTP 请求的返回头设置好相关配置,允许该域名通过,那么浏览器就不会阻拦前端发送出请求。
比如下面是 python 服务端的配置,该服务除了自身的请求,也允许来自“http://localhost:3000”和“http://127.0.0.1:3000”的请求。
if ENV == 'dev':
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:3000", "http://127.0.0.1:3000"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
使用此方法,有一些细节点要记住。1)对于后端服务,通常只需在测试环境才加入此 middleware,因为线上是用 nginx 代理,不需要此配置。2)对于前端,需要在请求头添加{credentials: "include"},这样才能传送 cookie。
fetch(url, {credentials: "include"});
这种方法可以解决跨域问题,但从开发角度,有几个缺点。一是此方案涉及前后端,增加了交流沟通成本;二是我认为更严重的缺点,那就是本地和线上的解决问题的思路不一致。线上用了 nginx 代理,而本地没代理,结果就是线上和本地的代码不一致,而这些不一致的地方就是容易出现 bug 的地方。并且每一次往线上发布代码,你不得不确认下不一致的地方有没有问题,无形中增加了开发的负担。
2. API 代理
另一种常见解决跨域的方法是 API 代理。既然跨域是由于浏览器的限制才会出现,那么,只要不用浏览器发送请求就不会有跨域问题。因此,让前端先将请求发送到(不会跨域)的代理服务器,再由代理服务器请求真正后端就成了有效的解决方案。
proxy
使用 React、Vue 等前端框架搭建的前端,在本地开发时,通常是有一个本地服务器,而此服务器就可以配置 API 代理。比如 nextjs,只需在 nextjs.config.js 中配置 rewrites:
const nextConfig = {
async rewrites() {
const rules =
process.env.NODE_ENV === "development"
? [
{
source: "/api/:path*",
destination: `http://127.0.0.1:8000/:path*`,
},
]
: [];
return rules;
},
};
使用代理时,从前端发送到后端的请求,就不需要在 url 前面添加后端域名 process.env.BASEURL,而是前端请求统一加上/api 即可(所有带有/api 的请求都会通过此处被转发到后端)。
const url = `/api${api}`;
这样子配置之后,不仅不需要后端配置 CORS,也不需要前端在请求接口时,区分开发和线上不同的后端域名,更加简洁。
最终流程:
whistle
不过如果单纯使用 proxy,本地开发的 URL 还是 http://localhost:3000 的话,那还会出现 cookie 种植和调用外部服务需要真实域名这两个问题。该怎么办呢?
你还可以使用 whistle。whistle 是一个基于 Node 实现的跨平台 web 调试代理工具,类似的工具有 Windows 平台上的 Fiddler,主要用于查看、修改 HTTP、HTTPS、Websocket 的请求、响应。
在本地开启 whistle 服务之后,只需要填入域名和代理的域名以及端口,就实现真实域名关联到本地带有端口的前后端服务。
当然,光是在 whistle 这样配置还不够。当你在浏览器输入域名之后,浏览器并不知道要先将请求发送到 whistle 代理服务器,而是直接发送到互联网上。因此,配合 whistle,需要一个浏览器代理插件,通常是 Proxy SwitchyOmega。
使用 Proxy SwitchyOmega 很简单,在 whistle 的文档中就有详细说明。在下载插件之后,只需配置 Proxy SwitchyOmega 要代理 8899 端口(因为 whistle 在本地的代理端口是 8899。),当你开启该情景模式 proxy 之后,通过浏览器发送的请求,就会先到达 whistle,从而正确代理。
最终流程:
nginx
whistle 很灵活,大部分前端本地开发时的 URL 配置问题都能够解决。但它还有一个缺点,那就是和线上环境不完全一致。作为 web 开发多年,我的一个感受是本地环境与线上环境越类似越好。既然线上是使用 nginx 代理,那本地也用 nginx,就能省却许多环境问题。但是我们大家也都知道,本地开启 nginx 并不方便。一来 nginx 的服务列表没有一个可视化工具,我们经常忘记该服务是开启还是关闭;二来 nginx 配置文件也很分散,我们不容易知道配置的内容。所以,许多人本地开发是不会开启 nginx。
不过有了 dokcer 之后,上述两个问题都不见了,因为你可以通过 docker 部署 nginx。你只需将 nginx.conf 和 docker-compose 写在一个统一的地方。那么是否开启本地 nginx,以及开启之后 nginx 有哪些能力,就变成一行命令的问题,非常简单。
配置好 nginx.conf,以及相关的 nginx 证书;然后写好 docker-compose.yml 文件;在当前文件夹运行 docker-compose 就能一键开启本地 nginx。
以下是一个例子。所有来自www.example.com的请求,都会被代理到本地3000端口(假设本地前端3000端口);所有带有/api的请求都会被代理到8000端口(假设本地的后端服务在8000端口)。这样配置之后,就可以在本地用nginx代理URL。
# nginx.conf
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log notice;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
# access_log /var/log/nginx/access.log main;
sendfile on;
#tcp_nopush on;
keepalive_timeout 65;
#gzip on;
client_max_body_size 15M;
server {
listen 80;
server_name www.example.com;
return 301 https://www.example.com$request_uri;
}
server {
listen 80;
server_name example.com;
return 301 https://www.example.com$request_uri;
}
server {
listen 443 ssl;
server_name example.com;
ssl_certificate /etc/nginx/ssl/server.crt;
ssl_certificate_key /etc/nginx/ssl/server.key;
ssl_session_cache shared:SSL:1m;
ssl_session_timeout 5m;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
return 301 https://www.example.com$request_uri;
}
server {
listen 443 ssl;
server_name www.example.com;
# 本地https证书
ssl_certificate /etc/nginx/ssl/server.crt;
ssl_certificate_key /etc/nginx/ssl/server.key;
ssl_session_cache shared:SSL:1m;
ssl_session_timeout 5m;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
# 前端代理
location / {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://host.docker.internal:3000;
}
# 后端代理
location /api/ {
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Origin $http_origin;
proxy_set_header X-Forwarded-Host $server_name;
proxy_read_timeout 300s;
add_header 'Access-Control-Allow-Origin' $http_origin always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
add_header 'Access-Control-Allow-Methods' 'GET, PUT, POST, DELETE, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'authorization,Content-Type,DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Range';
# 其他的配置...
if ($request_method = 'OPTIONS') {
return 204;
}
rewrite ^/api/(.*) /$1 break;
proxy_pass http://host.docker.internal:8000;
}
}
}
# docker-compose.yml
# 后端部署
version: "3.8"
services:
nginx:
image: nginx:1.25-perl
restart: always
volumes:
- ./nginx/conf:/etc/nginx
ports:
- "80:80"
- "443:443"
- "8443:8443"
networks:
- shared-network
networks:
shared-network:
external: true
当然,如果你在本地使用 nginx,还得修改 hosts,不然在浏览器的请求不会到达本地 nginx。有一些工具也能帮你快速切换本地 hosts 的状态,我用 Mac 电脑,使用的工具是 iHost。
最终流程:
总结
本文讨论了前端开发时,如何设置本地的 URL。如果目标是“保持本地环境和线上环境越相似越好”,那么可以使用 docker 开启本地 nginx 服务的方式;如果需要更灵活的代理,可以使用 whistle+Proxy SwitchyOmega。
参考
Using HTTP cookies - HTTP | MDN
Cross-Origin Resource Sharing (CORS) - HTTP | MDN
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。