|
| 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:3000(SSR) |
| 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 会绕出公网再回来,能用但会增加延迟,不推荐。 |
0 commit comments