Appearance
第 5 讲|前端和后端
打开一个网站,屏幕上看到的所有元素——按钮、表格、图表、动画——都属于前端;但保存数据、查询列表、做计算这些"重活",前端无法独立完成,需要交给后端。前端和后端是两套完全独立的代码,跑在不同的位置,通过 HTTP 接口通信。
排查时,前端和后端是两套完全不同的思路:页面没反应,先看浏览器 Console 和 Network;接口报 500,直接去后端日志找 trace id;接口返回 200 但数据是空的,大概率是字段名前后端没对齐。前后端排查的工具、视角、关键日志位置都不同——分清楚问题在哪一侧,是高效排查的前提。
本讲覆盖:前端的构成、后端的核心对象、JSON 接口契约、前后端分离的部署模式、跨域(CORS)机制、联调时的排查清单。
一、前端
前端就是运行在浏览器中的代码。技术栈由三种语言构成,各自负责一个维度:
| 技术 | 职责 | 类比 |
|---|---|---|
| HTML | 页面结构 — 有哪些元素、层级关系 | 房屋的框架 |
| CSS | 样式 — 颜色、字体、布局、动画 | 装修和家具摆放 |
| JavaScript | 交互 — 事件、表单、调接口、动态更新 DOM | 房屋里的电器与按钮 |
一个"删除按钮能用"背后的链条
页面上一个删除按钮能正常工作,需要满足:

- HTML 中存在该按钮元素
- CSS 没有把它
display: none隐藏 - JavaScript 给它绑定了 click 事件监听
- 点击后触发的 API 请求返回成功
- API 返回后,JS 把对应行从 DOM 中移除
任何一步断裂,用户看到的现象都是"点击没反应"——但排查方向完全不同。Console 面板有 JS 报错 → 第 3 步或第 5 步;Network 面板没看到请求发出 → 第 4 步之前断;请求发出但服务器返回错误 → 第 4 步;请求成功但页面没更新 → 第 5 步。
这种"现象相同、根因多种"的特点决定了排查必须分层使用工具,不能凭经验猜。
前端构建产物
React、Vue 等现代前端框架的源码(JSX、Vue 文件)不能直接被浏览器执行,需要经过构建工具(Webpack、Vite)编译:
源码 构建产物
├── src/ → ├── index.html
│ ├── App.vue ├── assets/
│ ├── ... │ ├── index-abc123.js
└── package.json │ ├── index-def456.css
│ └── logo-xxx.png
└── ...构建产物全部是静态文件——任何能托管静态文件的地方都能部署:
- Nginx — 最传统、最灵活
- 对象存储(S3、OSS、COS)+ CDN — 不需要运维 Web 服务器,自动全球分发
- CDN 边缘脚本(Cloudflare Workers、Vercel)— 极致的边缘部署
文件名哈希:前端缓存的标准做法
注意上面构建产物里 index-abc123.js 中的 abc123 是文件内容哈希。每次构建,只要文件内容变了,哈希就变,文件名也变。

这种命名方式的价值:HTML 引用了具体哈希的 JS 文件,缓存可以无限期(Cache-Control: max-age=31536000)——内容没变,哈希也不变,浏览器永远用缓存;内容变了,哈希变,HTML 也会引用新的文件名,自然命中新文件。HTML 文件自身不带哈希,设短缓存(几分钟),确保用户每次访问都能拉到最新的 HTML。
这种做法叫"哈希命名 + HTML 不缓存",是现代前端部署的标配。
二、后端
后端是跑在服务器上的程序(虚拟机、容器、K8s Pod 都可以),核心职责:
- 监听端口,接收 HTTP 请求
- 解析请求(URL、Header、Body)
- 执行业务逻辑
- 访问数据库、缓存、消息队列、其他服务
- 返回响应(通常是 JSON)
后端的语言/框架选择
| 语言 | 主流框架 | 特点 |
|---|---|---|
| Python | FastAPI、Django、Flask | 开发快,性能中等,适合中小型业务 |
| Java | Spring Boot、Quarkus | 生态最完整,适合大型企业级 |
| Go | Gin、Echo、net/http | 性能高,部署简单,云原生主流 |
| Node.js | Express、NestJS、Koa | 与前端共用 JS,适合实时通信 |
| Rust | Actix、Axum | 极致性能,学习曲线陡 |
技术选型本身不是核心 — 任何主流语言都能完成绝大多数业务需求,真正重要的是团队熟悉度和生态匹配。
后端代码里的核心对象
无论用哪种语言,后端代码里的核心对象大同小异:
| 对象 | 作用 | 示例(Python FastAPI) |
|---|---|---|
| 路由(Route) | 把 URL 路径绑定到处理函数 | @app.get("/users") |
| 处理函数(Handler) | 实际处理请求的代码 | def get_users(): |
| 模型(Model) | 描述业务对象/数据库表 | class User(BaseModel): |
| 数据库连接 | 管理到数据库的连接池 | engine = create_engine(...) |
| 中间件(Middleware) | 横切关注点(认证、日志、CORS、限流、错误处理) | app.add_middleware(...) |
| 配置(Config) | 环境相关参数 | class Settings(BaseSettings): |
后端端口不直接暴露公网
后端通常监听 8080、5000、8000 等内部端口,但公网用户不直接访问这个端口——中间隔着 Nginx、负载均衡或 API 网关。
原因:
| 直接暴露的问题 | 入口层提供的能力 |
|---|---|
| 没有 HTTPS 终结 | TLS 证书统一管理 |
| 没有统一认证 | 网关层做 token 校验、限流 |
| 后端进程崩了立刻 502 | 多实例负载均衡 + 健康检查 |
| 难以做缓存、压缩 | Nginx 自带缓存、gzip |
| 安全风险 | WAF、IP 黑白名单 |
合理的部署架构:公网 → Nginx/网关(80/443)→ 后端(内网 8080)。后端端口只在内部网络可达。
三、JSON:接口契约的载体
前后端分离架构下,后端返回的几乎都是 JSON 格式。一个典型的列表接口:

json
{
"items": [
{"id": 1, "name": "task A", "status": "success"},
{"id": 2, "name": "task B", "status": "running"}
],
"total": 2,
"page": 1
}前端代码按约定的字段名读取:用 items 数组渲染表格、用 total 计算分页。
字段命名约定:接口契约的核心
字段名、类型、嵌套结构构成了前后端之间的契约——双方必须严格遵守,否则即便接口返回 200,前端也无法正确显示。
字段对不齐的典型场景:
| 后端返回 | 前端期望 | 结果 |
|---|---|---|
task_status | status | 状态列空白 |
created_at(ISO 字符串) | createdAt(timestamp 数字) | 时间显示异常 |
items 嵌套了一层 data: {items: ...} | 直接读 data.items | 列表为空 |
null | undefined | 部分前端代码可能崩溃 |
这种问题极其隐蔽——接口状态 200,Network 中有数据,后端日志也无错误。需要对照响应 JSON 和前端代码逐字段比对才能定位。
减少字段对不齐的工程实践
| 实践 | 作用 |
|---|---|
| 接口文档(Swagger / OpenAPI) | 前后端共享字段定义,自动生成客户端 SDK |
| 类型生成工具(TypeScript) | 后端 schema 自动生成前端 TS 类型,编译期发现不匹配 |
| 强契约协议(gRPC、GraphQL) | 协议层面保证类型一致 |
| 接口变更评审 | 后端改字段名前必须通知前端 |
| 灰度发布 + 监控 | 字段变更后观察前端报错率 |
推荐的接口响应格式
不同公司有不同规范,常见的标准化响应格式:
json
{
"code": 0,
"message": "ok",
"data": {
"items": [...],
"total": 100
}
}json
{
"code": 40001,
"message": "Invalid parameter: name is required",
"data": null
}code = 0 表示成功,非 0 是业务错误码;data 字段统一包裹实际数据。这种格式让前端可以用同一套逻辑处理所有接口的成功/失败判定。
四、前后端分离部署
前后端分离的典型部署架构:
各层的部署位置
| 内容 | 部署位置 |
|---|---|
| 前端静态文件 | Nginx、对象存储 + CDN |
| 后端 API | 虚拟机、容器、Kubernetes |
| 数据库 | 云数据库(RDS)、自建集群 |
| 缓存 | Redis、Memcached |
| 入口层 | Nginx、API 网关、Ingress |
域名拆分:常见做法
| 域名 | 用途 | 部署 |
|---|---|---|
www.example.com | 主页面入口 | CDN |
static.example.com | 静态资源 | CDN |
api.example.com | 后端 API | 入口层 + 应用集群 |
admin.example.com | 管理后台 | 独立部署,IP 白名单 |
m.example.com | 移动版 | 独立的前端构建 |
这种拆分的好处:
- 静态资源走 CDN,加速 + 减少源站压力
- API 域名独立,可以做精细的限流、监控
- 管理后台域名分开,便于做访问控制(只允许公司 IP / VPN)
- 不同域名的故障域隔离,某个域名挂了不会影响其他
五、跨域(CORS)
同源策略
浏览器有一项核心安全机制叫同源策略(Same-Origin Policy):两个 URL 的协议、域名、端口任一不同,即视为"不同源",JavaScript 默认不能跨源读取响应。
| URL 1 | URL 2 | 是否同源 |
|---|---|---|
https://example.com/a | https://example.com/b | 同源 |
https://example.com | https://example.com:8080 | 不同源(端口不同) |
https://example.com | https://api.example.com | 不同源(域名不同) |
https://example.com | http://example.com | 不同源(协议不同) |
CORS:跨域的合法机制
为了让"前端域 ≠ 后端域"这种合理场景能工作,浏览器提供了 CORS(Cross-Origin Resource Sharing) 机制:后端在响应中显式声明哪些来源被允许跨域访问,浏览器据此放行或拦截。
跨域不是网络层的问题
这一点必须明确:CORS 是浏览器单方面加的安全限制——后端实际上正常处理了请求、返回了响应,只是浏览器看到响应头里没有 Access-Control-Allow-Origin,单方面拦住 JS 不让读响应。

排查时的现象:
- Network 面板:请求发出,返回 200,响应有数据
- Console 面板:CORS 错误,JS 拿不到数据
- 后端日志:无异常,正常处理
判定:curl(不受 CORS 限制)能直接拿到数据,但浏览器拿不到 — 一定是 CORS 配置问题。
简单请求 vs 预检请求
CORS 把请求分为两类:
简单请求(GET、HEAD、POST + 简单 Header):浏览器直接发请求,响应中检查 CORS Header 决定是否放行 JS 读取。
预检请求(其他情况:PUT/DELETE、自定义 Header、Content-Type: application/json 等):浏览器先发一个 OPTIONS 请求询问后端,后端响应允许后才发真正的请求。
CORS 配置示例(后端)
FastAPI:
python
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=["https://web.example.com"], # 不要用 ["*"],尤其在带 Cookie 的场景
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE"],
allow_headers=["*"],
)Spring Boot:
java
@CrossOrigin(origins = "https://web.example.com",
allowedHeaders = "*",
methods = {RequestMethod.GET, RequestMethod.POST})Nginx(在入口层处理):
nginx
location /api/ {
if ($request_method = OPTIONS) {
add_header Access-Control-Allow-Origin "https://web.example.com";
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS";
add_header Access-Control-Allow-Headers "Content-Type, Authorization";
return 204;
}
add_header Access-Control-Allow-Origin "https://web.example.com" always;
proxy_pass http://backend;
}CORS 排查的常见陷阱
| 现象 | 根因 | 处理 |
|---|---|---|
| 预检请求 OPTIONS 返回 405 | 后端未实现 OPTIONS 方法 | 在框架中开启 CORS 中间件或显式处理 OPTIONS |
| 预检通过但真实请求被拦 | Access-Control-Allow-Origin 在简单请求路径上没设 | 用 always 参数或在所有响应中设置 |
Access-Control-Allow-Credentials 不生效 | 同时设了 Access-Control-Allow-Origin: * | 带 Cookie 时,Origin 必须是具体域名而非 * |
| Nginx 反代后 CORS Header 丢失 | Nginx 默认会过滤掉后端的某些 Header | 用 proxy_pass_header 显式透传 |
| 前端 fetch 请求未带 Cookie | 未设置 credentials: 'include' | fetch 调用时显式指定 |
六、联调排查清单
前后端联调时,根据具体现象快速定位:
| 现象 | 大概率问题 | 排查步骤 |
|---|---|---|
| 接口返回 401 | Token 未携带、Cookie 失效、登录态过期 | 看 Network 中的请求 Header 是否带 Authorization |
| 接口返回 403 | 已认证但无权限 | 看后端权限配置、用户角色 |
| 接口返回 404 | API 路径错、网关未转发、后端路由未注册 | 对照 API 文档 + 后端路由表 |
| 接口返回 500 | 后端代码异常 | 看后端日志中的异常堆栈 |
| 接口返回 200 但页面空白 | 字段名对不齐、数据结构变了、前端渲染逻辑出错 | 对照响应 JSON 和前端代码 |
| Console CORS 错误 | 后端跨域配置缺字段,或 Nginx 把 CORS Header 过滤了 | 看 OPTIONS 预检响应、看入口层配置 |
| JS 文件 404 | 前端构建路径错、发布路径错、CDN 缓存了旧路径 | 看 Network 中 JS 文件的请求 URL |
| 跨域 OPTIONS 请求过但 POST 失败 | 后端在 OPTIONS 时配了 CORS,POST 响应没配 | 让 CORS 配置对所有请求生效 |
| 接口慢 | 后端慢查询、外部调用超时、数据库压力 | 看后端日志的请求耗时、看慢查询日志 |
| 偶发 502 | 后端实例重启、负载均衡某节点异常 | 看入口层 error log |
标准排查顺序
- 先看浏览器 Network 面板:请求发出了吗?到了哪里?响应状态码?响应内容?
- 再看浏览器 Console 面板:有 JS 错误吗?有 CORS 错误吗?
- 然后看入口层日志(Nginx access log + error log):请求有没有到入口?转发到了哪个后端?
- 最后看后端日志:请求有没有到后端?有没有异常?
这个顺序的核心思想是从外到内,从近到远——先排除前端层的问题,再排除入口层,最后才到后端。每一层都用对应的工具,不跳过、不跨层猜测。