使用 Nuxt Server Routes 减少前端与私有 API 的重复代理
起因
手头有个 Nuxt 项目,后端是比较私有的 API,地址是内网 IP,不对外暴露。开发的时候在 nuxt.config.ts 里配一层 proxy,上线之后又在 nginx 里配一遍 location,两边的规则长得几乎一样,但就是得写两遍。后端说要把接口前缀从 /api 改成 /api/v2,我改完 nginx 忘了改 nuxt 那边,本地开发正常,线上 502,查了半天才想起来。
更烦的是前端代码里到处都是 baseURL。不同环境给不同的值,.env 一份,.env.production 一份,写错一个就白屏,而且这个值还会被打进客户端 bundle。后来想想,这事本来就不该让浏览器知道,浏览器只需要知道「我要调 /api/xxx」,至于 /api/xxx 到底转发到哪台机器的哪个端口,那是服务端自己的事。
Nuxt 本来就有 server routes,dev 和 prod 跑的是同一份 Nitro 代码,那还配什么 proxy,直接写个 catch-all 转发不就行了。
以前的配置长什么样
开发环境:
nitro: {
devProxy: {
'/api': {
target: 'http://10.0.0.12:8000/api',
changeOrigin: true,
},
},
}
生产环境 nginx:
location /api/ {
proxy_pass http://10.0.0.12:8000/api/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
前端:
const { public: { apiBase } } = useRuntimeConfig()
const res = await $fetch(`${apiBase}/users`)
三份配置,三处需要同步。中间只要漏一个,就是「本地好好的,线上不行」这种最难查的问题。
用 server route 做统一入口
新建 server/api/proxy/[...path].ts:
export default defineEventHandler((event) => {
const config = useRuntimeConfig(event)
const path = getRouterParam(event, 'path') ?? ''
const { search } = getRequestURL(event)
const target = `${config.privateApiBase}/${path}${search}`
return proxyRequest(event, target)
})
就这几行。proxyRequest 是 h3 内置的,method、body、header、cookie 都会带过去,响应也会原样回传,包括 set-cookie。我之前手写 $fetch 转发过一次,上传文件死活不行,后来才发现 body 得传原始 stream,还要带 duplex: 'half',踩完这个坑就再也不想自己拼了。
这里的 getRequestURL(event).search 别漏,不然所有带 query 的接口全部失效,而且这种失效不报错,后端就是收不到参数,查起来非常折磨人。
几个必须注意的细节
请求头要过滤。 最少要把 host、connection、content-length 删掉。有些后端框架会校验 Host,比如 Django 的 ALLOWED_HOSTS,你不改它就直接给你 400,报错信息还特别模糊。
const headers = getRequestHeaders(event)
delete headers.host
delete headers.connection
return proxyRequest(event, target, { headers })
cookie 的 domain 问题。 proxyRequest 会把上游的 set-cookie 原样带回,但如果后端在 cookie 里写死了 domain=10.0.0.12,浏览器会拒收。这种情况可以用 cookieDomainRewrite,也可以干脆让后端别写 domain:
return proxyRequest(event, target, {
cookieDomainRewrite: { '*': '' },
})
超时。 Nitro 这边一般不会卡你,但前面还有一层 nginx,默认 proxy_read_timeout 是 60 秒,导出报表这种慢接口很容易撞上,撞上就是 504。这个跟 Nuxt 无关,但既然请求链路变长了,就得一起看。
私有地址别泄露。 privateApiBase 一定要放在 runtimeConfig 的私有字段里,不能放 public。放 public 的话会直接序列化进客户端,内网 IP 全网可见,等于白干。
前端那边就简单了
原来:
const res = await $fetch(`${apiBase}/users`, { credentials: 'include' })
现在:
const res = await $fetch('/api/proxy/users')
不用管环境,不用管 baseURL,打包出来就是一个相对路径。SSR 的时候也是同源调用,省掉了跨域预检。而且打开浏览器 DevTools 就能看到每个请求打到了哪个接口,比去翻 nginx 配置直观多了。
runtimeConfig 怎么写
runtimeConfig: {
privateApiBase: 'http://10.0.0.12:8000/api',
}
线上直接覆盖:
NUXT_PRIVATE_API_BASE=http://backend:8000/api
dev 走内网 IP,线上走容器服务名,前端代码一行都不用动。
代价也得说清楚
这么干不是没有成本。所有 API 请求都要在 Nuxt 服务端绕一圈,多一跳,同机房内网基本感觉不到,但确实多了一次序列化和反序列化。另外 Nuxt 服务端从此就成了单点,以前静态资源挂了 API 还能通,现在一起挂。日志也多了一层,排错时要多看一个地方。
还有一个前提:这条路要求 Nitro 跑在 node 环境(或者它支持的其他服务端 preset)。如果你项目是 ssr: false 加纯静态托管,那就没有服务端,server routes 根本不会执行,这种情况还是老老实实写 nginx。
最后
我不敢说这是标准做法,但如果你的项目正好是「Nuxt + 私有后端 + 多环境」,那把所有转发收口到一个 server route 里,至少能让你少改两份配置、少踩几次「本地正常线上 502」的坑。至少我现在后端说要加个前缀,我只需要改一个环境变量。
这比什么都强。
评论
还没有评论。
发表评论
提交后评论将经过自动审核,审核通过后公开展示。