Skip to content

Commit 922c897

Browse files
committed
YUNYU_API_BASE_INTERNAL改造
1 parent 01e92d5 commit 922c897

5 files changed

Lines changed: 226 additions & 10 deletions

File tree

docker/.env.example

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
11
# 环境变量配置示例
2-
# 使用方式:cp docker/.env.example .env 然后按需修改
3-
# .env 文件放在项目根目录(Yunyu/),与 docker/ 同级
2+
# 使用方式:cp docker/.env.example docker/.env 然后按需修改
3+
# .env 文件需放在 docker/ 目录下(与 docker-compose.yml 同级
44

55
# ── 数据库 ────────────────────────────────────────────────────────
6-
MYSQL_DATABASE=yunyu
7-
MYSQL_USER=yunyu
86
# 不填则使用内置默认值(仅适合本地体验,正式环境请替换)
9-
MYSQL_PASSWORD=yunyu123456
107
MYSQL_ROOT_PASSWORD=root123456
118
# docker-compose-server.yml 模式下对外暴露的端口(纯 Docker 模式不对外暴露)
129
MYSQL_PORT=3306
@@ -22,7 +19,10 @@ SERVER_PORT=20000
2219

2320
# ── 前端(仅 docker-compose.yml 纯 Docker 模式使用)────────────────
2421
YUNYU_WEB_IMAGE=ghcr.io/idea-flow/yunyu-web:latest
25-
# 纯 Docker 模式下前后端同域,浏览器通过 nginx 转发 /api/*,此处留空即可
22+
# SSR 服务端调用后端的内部地址(容器内网),默认已指向 yunyu-server-native:20000,通常无需修改
23+
# 仅在自定义容器名或网络时才需要覆盖
24+
YUNYU_API_BASE_INTERNAL=http://yunyu-server-native:20000
25+
# 浏览器端 API 地址:纯 Docker 模式下前后端同域,浏览器通过 nginx 转发 /api/*,留空即可
2626
# 若需要跨域访问后端,填写后端完整域名,如:https://api.yourdomain.com
2727
YUNYU_PUBLIC_API_BASE=
2828

docker/docker-compose.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ services:
5353
- yunyu-server-native
5454
environment:
5555
# SSR 服务端调用后端使用容器名(内部网络),浏览器端 API 请求经 nginx 同域转发,无需单独配置
56+
YUNYU_API_BASE_INTERNAL: ${YUNYU_API_BASE_INTERNAL:-http://yunyu-server-native:20000}
5657
NUXT_PUBLIC_API_BASE: ${YUNYU_PUBLIC_API_BASE:-}
5758
TZ: ${TZ:-Asia/Shanghai}
5859
# 不对外暴露端口,只在内部网络中被 nginx 访问
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
# Nuxt SSR 服务端 API 地址配置说明
2+
3+
## 一、问题背景
4+
5+
使用纯 Docker 模式部署后,访问首页出现报错:
6+
7+
```
8+
Page not found: /api/site/home
9+
```
10+
11+
而直接访问 `https://your-domain.com/api/site/home` 却能正常返回数据。
12+
13+
---
14+
15+
## 二、根本原因
16+
17+
Nuxt 是 SSR(服务端渲染)框架,首页数据在**服务端渲染阶段**就需要调用后端接口获取。
18+
19+
### 两个运行环境的请求链路完全不同
20+
21+
| 环境 | 请求发起方 | 请求路径 |
22+
|---|---|---|
23+
| 浏览器端(CSR) | 用户浏览器 | 浏览器 → nginx → 后端 |
24+
| 服务端渲染(SSR) | yunyu-web 容器内部 | 容器内 → ??? |
25+
26+
**问题就在 SSR 阶段**
27+
28+
```
29+
用户访问首页
30+
31+
nginx 转发请求给 yunyu-web:3000(Nuxt SSR)
32+
33+
Nuxt 服务端需要调用 /api/site/home 获取数据
34+
35+
YUNYU_PUBLIC_API_BASE 为空
36+
37+
代码 fallback:http://127.0.0.1:20000
38+
39+
yunyu-web 容器内的 127.0.0.1 没有 20000 端口 → 连接失败
40+
41+
Page not found: /api/site/home ✗
42+
```
43+
44+
浏览器直接访问 `/api/site/home` 能通,是因为走的是:
45+
```
46+
浏览器 → Cloudflare → nginx → yunyu-server-native:20000 ✓
47+
```
48+
完全不经过 Nuxt SSR,所以没问题。
49+
50+
---
51+
52+
## 三、为什么不能把 YUNYU_PUBLIC_API_BASE 设为容器名
53+
54+
看起来把 `YUNYU_PUBLIC_API_BASE=http://yunyu-server-native:20000` 能让 SSR 访问后端,但这个配置是 `public`**服务端和浏览器共享同一个值**
55+
56+
```
57+
SSR 服务端:http://yunyu-server-native:20000 ✓ 容器内网能解析
58+
浏览器端: http://yunyu-server-native:20000 ✗ 用户浏览器不认识这个域名
59+
```
60+
61+
页面首次加载正常,但用户交互时所有浏览器端请求全部失败。
62+
63+
---
64+
65+
## 四、解决方案
66+
67+
利用 Nuxt `runtimeConfig` 的分层机制:
68+
69+
- `runtimeConfig.public.*` — 服务端 + 浏览器**共享**
70+
- `runtimeConfig.*`(非 public)— **仅服务端可见**,浏览器拿不到
71+
72+
新增一个仅服务端可见的 `apiBaseInternal`,SSR 阶段优先用它,浏览器端用 `public.apiBase`
73+
74+
### 改动一:`yunyu-web/nuxt.config.ts`
75+
76+
```js
77+
runtimeConfig: {
78+
// 仅服务端可见:SSR 阶段调用后端使用容器内部地址
79+
apiBaseInternal: process.env.NUXT_API_BASE_INTERNAL || '',
80+
public: {
81+
// 浏览器端:留空时走相对路径,由 nginx 同域转发到后端
82+
apiBase: process.env.YUNYU_PUBLIC_API_BASE || ''
83+
}
84+
}
85+
```
86+
87+
### 改动二:`yunyu-web/app/composables/useApiClient.ts`
88+
89+
新增 `resolveApiBase()` 函数,在 SSR 阶段自动选择内部地址:
90+
91+
```js
92+
function resolveApiBase() {
93+
if (import.meta.server && config.apiBaseInternal) {
94+
return config.apiBaseInternal // SSR:容器内网直连
95+
}
96+
return config.public.apiBase // 浏览器:走 nginx 转发
97+
}
98+
```
99+
100+
所有请求中的 `config.public.apiBase` 替换为 `resolveApiBase()`
101+
102+
### 改动三:`docker/docker-compose.yml`
103+
104+
`yunyu-web` 服务注入内部地址:
105+
106+
```yaml
107+
yunyu-web:
108+
environment:
109+
NUXT_API_BASE_INTERNAL: http://yunyu-server-native:20000
110+
NUXT_PUBLIC_API_BASE: ${YUNYU_PUBLIC_API_BASE:-}
111+
```
112+
113+
---
114+
115+
## 五、各部署模式下的配置方式
116+
117+
### 模式 A:纯 Docker(nginx 统一入口)
118+
119+
`docker-compose.yml` 已内置 `NUXT_API_BASE_INTERNAL`,无需额外配置:
120+
121+
```
122+
NUXT_API_BASE_INTERNAL = http://yunyu-server-native:20000 (已硬编码在 compose 文件)
123+
YUNYU_PUBLIC_API_BASE = (留空,浏览器走 nginx 同域转发)
124+
```
125+
126+
**最终调用链:**
127+
128+
```
129+
浏览器访问首页
130+
↓ nginx → yunyu-web:3000SSR
131+
import.meta.server = true → 使用 http://yunyu-server-native:20000
132+
↓ 容器内网直连,不走公网 ✓
133+
134+
浏览器端后续请求
135+
↓ apiBase = "" → 相对路径 /api/xxx
136+
↓ nginx 转发给 yunyu-server-native:20000
137+
```
138+
139+
---
140+
141+
### 模式 B:Cloudflare Pages(纯静态/SSR 托管在 Cloudflare)
142+
143+
Cloudflare Pages 运行的是静态构建产物,没有 Node.js 服务端,**不存在 SSR 问题**。
144+
145+
浏览器直接请求后端域名,只需配置一个变量:
146+
147+
```
148+
YUNYU_PUBLIC_API_BASE = https://api.yourdomain.com
149+
```
150+
151+
`NUXT_API_BASE_INTERNAL` 不需要配置。
152+
153+
---
154+
155+
### 模式 C:本地开发
156+
157+
本地开发时 Nuxt dev server 运行在宿主机,直接能访问后端,不需要容器内网地址。
158+
159+
`yunyu-web/.env` 中配置:
160+
161+
```bash
162+
# 留空,走代码默认值 http://127.0.0.1:20000
163+
YUNYU_PUBLIC_API_BASE=
164+
165+
# 或者明确指定本地后端地址
166+
YUNYU_PUBLIC_API_BASE=http://127.0.0.1:20000
167+
```
168+
169+
`NUXT_API_BASE_INTERNAL` 留空即可,`resolveApiBase()` 会 fallback 到 `public.apiBase`
170+
171+
---
172+
173+
### 模式 D:Cloudflare Pages + 1Panel(前端 Cloudflare,后端服务器)
174+
175+
前端部署在 Cloudflare Pages,运行环境是 Cloudflare 的边缘网络(无 Node.js SSR 容器),所有请求都是浏览器发出,只需:
176+
177+
```
178+
YUNYU_PUBLIC_API_BASE = https://api.yourdomain.com
179+
```
180+
181+
`NUXT_API_BASE_INTERNAL` 不需要配置。
182+
183+
---
184+
185+
## 六、配置速查表
186+
187+
| 部署模式 | `NUXT_API_BASE_INTERNAL` | `YUNYU_PUBLIC_API_BASE` |
188+
|---|---|---|
189+
| 纯 Docker(nginx 统一入口) | `http://yunyu-server-native:20000`(compose 已内置) | 留空 |
190+
| Cloudflare Pages | 不需要 | `https://api.yourdomain.com` |
191+
| Cloudflare Pages + 1Panel | 不需要 | `https://api.yourdomain.com` |
192+
| 本地开发 | 不需要 | 留空或 `http://127.0.0.1:20000` |
193+
194+
---
195+
196+
## 七、注意事项
197+
198+
1. `NUXT_API_BASE_INTERNAL` 是服务端私有变量,**不会暴露给浏览器**,安全。
199+
2. 纯 Docker 模式下 `NUXT_API_BASE_INTERNAL` 已在 `docker-compose.yml` 中硬编码,`.env` 中无需配置。
200+
3. SSR 容器内的请求走内网,不经过公网和 Cloudflare,延迟更低,也不消耗外部流量。
201+
4. 如果在纯 Docker 模式下把 `YUNYU_PUBLIC_API_BASE` 设为公网域名,SSR 会绕出公网再回来,能用但会增加延迟,不推荐。

yunyu-web/app/composables/useApiClient.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,17 @@ export function useApiClient() {
2323
sameSite: 'lax'
2424
})
2525

26+
/**
27+
* 选取实际后端地址。
28+
* SSR 阶段优先使用容器内网地址(apiBaseInternal),浏览器端使用 public.apiBase(留空则走 nginx 同域转发)。
29+
*/
30+
function resolveApiBase() {
31+
if (import.meta.server && config.apiBaseInternal) {
32+
return config.apiBaseInternal
33+
}
34+
return config.public.apiBase
35+
}
36+
2637
/**
2738
* 从浏览器缓存恢复访问令牌。
2839
* 作用:在前台页面刷新后优先恢复本地保存的登录凭证,避免仅依赖 Cookie 导致前台登录态丢失。
@@ -96,7 +107,7 @@ export function useApiClient() {
96107
) {
97108
const { withAuth = true, ...fetchOptions } = options
98109
const headers = buildHeaders(fetchOptions.headers, withAuth)
99-
const requestUrl = `${config.public.apiBase}${path}`
110+
const requestUrl = `${resolveApiBase()}${path}`
100111

101112
try {
102113
const response = await $fetch<ApiResponse<T>>(requestUrl, {
@@ -117,7 +128,7 @@ export function useApiClient() {
117128
error?.data?.message ||
118129
responseMessage ||
119130
(responseStatusCode === 404
120-
? `请求的接口不存在:${path}。当前前端连接的后端地址为 ${config.public.apiBase},请确认是否连到了最新后端服务。`
131+
? `请求的接口不存在:${path}。当前前端连接的后端地址为 ${resolveApiBase()},请确认是否连到了最新后端服务。`
121132
: null) ||
122133
error?.statusMessage ||
123134
error?.message ||
@@ -143,7 +154,7 @@ export function useApiClient() {
143154
const headers = buildHeaders(fetchOptions.headers, withAuth)
144155

145156
try {
146-
return await $fetch<T>(`${config.public.apiBase}${path}`, {
157+
return await $fetch<T>(`${resolveApiBase()}${path}`, {
147158
...fetchOptions,
148159
headers
149160
})

yunyu-web/nuxt.config.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,11 @@ export default defineNuxtConfig({
2121
},
2222
css: ['katex/dist/katex.min.css', '~/assets/css/main.css'],
2323
runtimeConfig: {
24+
// 仅服务端可见:SSR 阶段调用后端使用容器内部地址,避免走 nginx 或公网
25+
apiBaseInternal: process.env.YUNYU_API_BASE_INTERNAL || '',
2426
public: {
25-
apiBase: process.env.YUNYU_PUBLIC_API_BASE || 'http://127.0.0.1:20000'
27+
// 浏览器端使用:留空时请求走相对路径,由 nginx 同域转发到后端
28+
apiBase: process.env.YUNYU_PUBLIC_API_BASE || ''
2629
}
2730
},
2831
colorMode: {

0 commit comments

Comments
 (0)