上一篇,我们完成了后端接口转发。这一篇把前面学过的配置组合起来:页面能打开、前端路由能刷新、静态资源不乱回退、接口由后端处理。
继续使用 Windows + CMD。本篇提供一个不依赖框架的最小单页应用,部署行为与常见客户端 Vue、React 项目类似,不需要先创建一套管理后台。
一、先确定四类请求的分工
本篇使用 Nginx 8095 端口作为浏览器统一入口,后端监听本机 3009 端口。
| 请求 | 应该怎样处理 |
|---|---|
/ |
返回首页 |
/users |
返回首页文件,由前端渲染用户页 |
/assets/app.js |
返回真实脚本文件,不存在就 404 |
/api/users |
转发到后端 /users 接口 |
这里 /users 是页面路由,/api/users 是接口路径。两者看起来接近,处理方式不同。
最重要的边界是:
- 页面路由找不到磁盘文件,可以回退到
index.html。 - 静态资源找不到,应该返回 404。
- 接口交给后端,不能错误返回前端首页。

二、准备最小前后端示例
1. 放置文件
下载随文示例包,将文件按下面的位置复制到 Nginx 安装目录。本文以 D:\devtools\nginx 为例,路径不同就替换成自己的。
| 示例包中的位置 | 复制到安装目录中的位置 |
|---|---|
demo/html/lesson09 |
html/lesson09 |
demo/lesson09-backend |
lesson09-backend |
复制后确认以下文件存在:
text
html/lesson09/index.html
html/lesson09/assets/style.css
html/lesson09/assets/app.js
lesson09-backend/server.cjs
index.html 应直接在 lesson09 中,不要多套一层 demo 或 html。
本篇重点是部署配置,前端完整源码已随包提供。它通过浏览器 History API 切换 / 与 /users,用户页按钮使用:
javascript
fetch('/api/users')
前端使用当前站点的接口地址,不写死后端端口。样式与脚本同样使用根路径:
html
<link rel="stylesheet" href="/assets/style.css">
<script src="/assets/app.js" defer></script>
2. 启动后端
确认已经有现代 Node.js 环境:
cmd
node -v
在 CMD 中执行:
cmd
cd /d D:\devtools\nginx
node lesson09-backend\server.cjs
后端不需要 npm install。保持窗口打开,再开一个 CMD 操作 Nginx。
本篇后端完整代码如下,也可以手动创建对应文件:
javascript
const http = require('node:http');
const routes = {
'/health': { status: 'ok' },
'/users': [{ id: 1, name: 'Maple Hush' }]
};
http.createServer((req, res) => {
const url = new URL(req.url, 'http://127.0.0.1:3009');
const found = Object.hasOwn(routes, url.pathname);
const status = req.method !== 'GET' ? 405 : found ? 200 : 404;
res.writeHead(status, {
'Content-Type': 'application/json; charset=utf-8',
...(status === 405 ? { Allow: 'GET' } : {})
});
res.end(JSON.stringify({
service: 'lesson09-backend',
receivedUrl: req.url,
data: status === 200 ? routes[url.pathname] : null,
message: status === 200 ? 'success' : status === 404 ? 'route not found' : 'method not allowed'
}, null, 2));
console.log(`${req.method} ${req.url} -> ${status}`);
}).listen(3009, '127.0.0.1', () => {
console.log('Backend ready: http://127.0.0.1:3009');
});
先通过浏览器直连:
text
http://127.0.0.1:3009/health
应看到 data.status 为 ok 的 JSON。用户列表地址为:
text
http://127.0.0.1:3009/users
后端只提供 GET /health、/users;不存在的接口返回 404 JSON。这个简单服务用于本地实验。
三、组合完整站点配置
1. 备份并打开配置
在第二个 CMD 窗口执行:
cmd
cd /d D:\devtools\nginx
copy conf\nginx.conf conf\nginx.conf.lesson09.bak
notepad conf\nginx.conf
如果备份已存在,先确认是否要保留,再决定是否覆盖。
2. 在现有 http 内新增 server
将下面的 server 与其他站点平级放置,不要覆盖整份 nginx.conf:
nginx
server {
listen 127.0.0.1:8095;
server_name localhost;
root html/lesson09;
index index.html;
charset utf-8;
access_log logs/lesson09_access.log;
error_log logs/lesson09_error.log;
location = /api {
return 404;
}
location ^~ /api/ {
proxy_pass http://127.0.0.1:3009/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
location ^~ /assets/ {
try_files $uri =404;
}
location = /index.html {
try_files $uri =404;
}
location / {
try_files $uri $uri/ /index.html;
}
}
确认现有 http 内保留 MIME 类型映射,已有时不要重复添加:
nginx
include mime.types;
default_type application/octet-stream;
不要复制第六篇诊断实验中的 types { }。本篇需要正确返回 HTML、CSS 和 JavaScript 的类型。
两个服务都只绑定本机回环地址,属于本地学习配置。端口被占用时,修改监听端口和相关访问地址;后端换端口时,proxy_pass 也要一起改。
3. 检查并加载
cmd
nginx -t
检查通过后,实例已运行时执行:
cmd
nginx -s reload
尚未运行时执行:
cmd
start nginx
四、为什么要分开写这些 location?
1. /api/:明确交给后端
nginx
location ^~ /api/ {
proxy_pass http://127.0.0.1:3009/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
本例会将 /api/users 转发成后端 /users。因为后端没有 /api 前缀,所以使用带 / 的 proxy_pass。2
^~ 使本篇的 /api/ 最长前缀匹配跳过正则检查。当前配置没有正则,行为本来就清晰;保留这个写法,明确该前缀由代理处理。以后新增更长前缀时,仍需重新分析匹配结果。1
接口没找到时,后端返回的 404 会传回浏览器。不要为了展示友好页面,在这个站点里全局把 404 都改成 /index.html。
对于单独的 /api,本例用精确匹配直接返回 404,避免把它当作前端页面。真正的接口仍使用 /api/...。
2. /assets/:只返回真实资源
nginx
location ^~ /assets/ {
try_files $uri =404;
}
资源存在就提供文件,不存在就返回 404。1
例如 /assets/missing.js 不应该返回首页 HTML。否则浏览器可能报脚本类型错误,而你看到的状态码却是 200,容易误判。
这里保护的是 /assets/ 这一个资源目录。实际项目如果还有 /images/、/fonts/ 等目录,需要按真实布局设置同类规则;不能认为所有资源路径都已经被覆盖。
3. /:允许前端页面回退
nginx
location / {
try_files $uri $uri/ /index.html;
}
请求 /users 时,磁盘里没有对应文件或目录,于是内部回退到 /index.html,再由前端脚本根据当前 URL 渲染用户页。1
浏览器地址仍是 /users。这解决的是 History 路由的直接访问和刷新问题。
回退并不会替前端创建页面:如果访问 /unknown-page,Nginx 也可能返回首页文件,但示例脚本会显示"页面不存在"。本例的 HTTP 状态仍是 200,这与后端接口的 404 是不同的概念。
如果确实存在同名目录,$uri/ 的目录检查也会影响后续行为。不要在部署目录里随意混入与页面路由同名的文件夹。
4. /index.html:入口文件也必须真实存在
nginx
location = /index.html {
try_files $uri =404;
}
这是回退的终点。如果入口文件缺失,直接返回 404,避免继续把 /index.html 回退给自己而形成内部重定向循环。
首页回退只适用于对应的前端应用,不等于任何错误都返回首页。

五、按这张清单验证部署
先打开:
text
http://127.0.0.1:8095/
应看到浅绿色背景的测试页面。点击"用户页",地址变为 /users,再点击"请求用户接口",应显示 HTTP 200 和用户 JSON。
接下来逐项检查:
| 操作或请求 | 预期结果 |
|---|---|
| 从首页点击用户页 | 地址变成 /users,显示用户页 |
在 /users 按 F5 |
仍显示用户页,样式与脚本正常 |
新标签页直接打开 /users |
能正常进入用户页 |
/assets/app.js |
返回 JavaScript 内容 |
/assets/missing.js |
404,不返回首页 |
/api/users |
200 JSON,后端收到 /users |
/api/missing |
后端 404 JSON,不返回首页 |
/api |
Nginx 404,本例没有定义该接口 |
/unknown-page |
返回应用入口,前端显示"页面不存在" |
浏览器按 F12,打开 Network 面板,再刷新一次。不要只看页面有没有内容,还要确认:
- HTML、CSS、JS 请求都返回对应内容与类型。
/api/users的响应是 JSON。- 缺失资源和接口没有被错误包装成首页。
使用缓存时可能看到 304 或缓存标记,不必把它们当作失败。
六、换成自己的 Vue、React 项目
1. 先构建,再部署产物
在前端项目目录执行项目提供的构建脚本,例如:
cmd
npm run build
Vite 项目的默认输出目录通常是 dist,但可以通过配置修改。3
把输出目录中的全部内容 复制到站点目录,让 root 指向真正包含 index.html 的位置。不要只复制 HTML,也不要直接部署 src 源码目录。
本篇手写应用没有构建步骤;换成真实项目时,才执行你自己项目的构建命令。
2. 对齐接口地址
生产构建中的接口地址应按项目约定使用当前站点的 /api 前缀,例如:
javascript
fetch('/api/users')
如果开发时配置过 Vite 的 server.proxy,要记住它服务于开发服务器。构建文件交给 Nginx 后,请求转发需要由 Nginx 等部署层完成。
环境变量改了,通常还要重新构建,不能只改本地 .env 就期待已经发布的静态文件自动变化。
3. 对齐后端路径
如果真实后端接口就是 /api/users,需要保留前缀:
nginx
proxy_pass http://127.0.0.1:3009;
这与本篇后端 /users 的约定不同。先确认后端路由,再决定斜杠,不要两边重复加或重复去掉前缀。2
4. 对齐部署根路径
本篇部署在站点根路径,资源地址使用 /assets/...。
如果以后部署到 /admin/ 这样的子路径,构建工具的资源基础路径、前端路由基础路径和 Nginx 映射都要同步调整,不能只修改 root。
SSR 应用可能还需要 Node.js 等应用服务,不能直接套用纯客户端静态部署。3
七、常见问题
1. 点击页面能用,刷新 /users 却 404
先检查请求是否进入正确的 8095 站点,再看是否配置了对应的首页回退,以及 index.html 是否真实存在。
前端点击跳转可能只修改浏览器历史记录;刷新则会向服务器重新请求 /users,这是两种不同的过程。
2. 接口返回 200,但前端提示 JSON 解析失败
先查看响应正文。如果是 <!DOCTYPE html> 开头,通常说明接口被页面回退接走了。
检查 /api/ 匹配、前端请求地址,以及是否配置了全局 404 首页回退。
3. 页面正常,接口返回 502
静态文件与后端进程独立。先直连:
text
http://127.0.0.1:3009/health
再查看错误日志:
cmd
type logs\lesson09_error.log
检查后端是否启动、上游地址和端口是否正确。不要因为首页能打开,就认为后端也一定运行正常。
4. 缺少脚本,却返回了 HTML
核对脚本的真实请求路径:是否确实在 /assets/ 下?构建产物是否完整复制?资源基础路径是否正确?
如果实际资源位于其他目录,要为真实目录设置处理规则,而不是只保留本篇的 /assets/。
5. 修改后没有生效
修改静态文件后,先保存并刷新,必要时禁用缓存检查。
修改 Nginx 配置后,执行 nginx -t,通过后 reload。修改后端代码时,本篇简单 Node.js 服务需要停止后重新运行。
本篇还记录了访问日志:
cmd
type logs\lesson09_access.log
根据请求路径、时间和状态码排查,不要只靠反复重启。
八、本篇回顾
现在我们把静态资源服务、location 匹配、路径处理和反向代理组合成了一个可验证的站点。
最重要的是分清边界:
- 页面路由由前端处理,必要时回退到入口文件。
- 静态资源必须真实存在,缺失时返回 404。
- 接口由后端处理,不混入前端页面回退。
- 浏览器统一访问 Nginx,后端地址由代理配置管理。
下一篇:《【Nginx 从入门到实战 10】负载均衡入门:把请求分配给多个后端实例》。
我们会启动两个可辨认的后端实例,观察请求分配,并模拟其中一个实例停止后的表现。