Skip to content

第 5 讲|前端和后端

打开一个网站,屏幕上看到的所有元素——按钮、表格、图表、动画——都属于前端;但保存数据、查询列表、做计算这些"重活",前端无法独立完成,需要交给后端。前端和后端是两套完全独立的代码,跑在不同的位置,通过 HTTP 接口通信。

排查时,前端和后端是两套完全不同的思路:页面没反应,先看浏览器 Console 和 Network;接口报 500,直接去后端日志找 trace id;接口返回 200 但数据是空的,大概率是字段名前后端没对齐。前后端排查的工具、视角、关键日志位置都不同——分清楚问题在哪一侧,是高效排查的前提。

本讲覆盖:前端的构成、后端的核心对象、JSON 接口契约、前后端分离的部署模式、跨域(CORS)机制、联调时的排查清单。

一、前端

前端就是运行在浏览器中的代码。技术栈由三种语言构成,各自负责一个维度:

技术职责类比
HTML页面结构 — 有哪些元素、层级关系房屋的框架
CSS样式 — 颜色、字体、布局、动画装修和家具摆放
JavaScript交互 — 事件、表单、调接口、动态更新 DOM房屋里的电器与按钮

一个"删除按钮能用"背后的链条

页面上一个删除按钮能正常工作,需要满足:

一个删除按钮背后串起前端接口后端数据库

  1. HTML 中存在该按钮元素
  2. CSS 没有把它 display: none 隐藏
  3. JavaScript 给它绑定了 click 事件监听
  4. 点击后触发的 API 请求返回成功
  5. 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 都可以),核心职责:

  1. 监听端口,接收 HTTP 请求
  2. 解析请求(URL、Header、Body)
  3. 执行业务逻辑
  4. 访问数据库、缓存、消息队列、其他服务
  5. 返回响应(通常是 JSON)

后端的语言/框架选择

语言主流框架特点
PythonFastAPI、Django、Flask开发快,性能中等,适合中小型业务
JavaSpring Boot、Quarkus生态最完整,适合大型企业级
GoGin、Echo、net/http性能高,部署简单,云原生主流
Node.jsExpress、NestJS、Koa与前端共用 JS,适合实时通信
RustActix、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):

后端端口不直接暴露公网

后端通常监听 808050008000 等内部端口,但公网用户不直接访问这个端口——中间隔着 Nginx、负载均衡或 API 网关。

原因:

直接暴露的问题入口层提供的能力
没有 HTTPS 终结TLS 证书统一管理
没有统一认证网关层做 token 校验、限流
后端进程崩了立刻 502多实例负载均衡 + 健康检查
难以做缓存、压缩Nginx 自带缓存、gzip
安全风险WAF、IP 黑白名单

合理的部署架构:公网 → Nginx/网关(80/443)→ 后端(内网 8080)。后端端口只在内部网络可达。

三、JSON:接口契约的载体

前后端分离架构下,后端返回的几乎都是 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_statusstatus状态列空白
created_at(ISO 字符串)createdAt(timestamp 数字)时间显示异常
items 嵌套了一层 data: {items: ...}直接读 data.items列表为空
nullundefined部分前端代码可能崩溃

这种问题极其隐蔽——接口状态 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 1URL 2是否同源
https://example.com/ahttps://example.com/b同源
https://example.comhttps://example.com:8080不同源(端口不同)
https://example.comhttps://api.example.com不同源(域名不同)
https://example.comhttp://example.com不同源(协议不同)

CORS:跨域的合法机制

为了让"前端域 ≠ 后端域"这种合理场景能工作,浏览器提供了 CORS(Cross-Origin Resource Sharing) 机制:后端在响应中显式声明哪些来源被允许跨域访问,浏览器据此放行或拦截。

跨域不是网络层的问题

这一点必须明确:CORS 是浏览器单方面加的安全限制——后端实际上正常处理了请求、返回了响应,只是浏览器看到响应头里没有 Access-Control-Allow-Origin,单方面拦住 JS 不让读响应

CORS 是浏览器安全策略不是网络不通

排查时的现象:

  • 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 默认会过滤掉后端的某些 Headerproxy_pass_header 显式透传
前端 fetch 请求未带 Cookie未设置 credentials: 'include'fetch 调用时显式指定

六、联调排查清单

前后端联调时,根据具体现象快速定位:

现象大概率问题排查步骤
接口返回 401Token 未携带、Cookie 失效、登录态过期看 Network 中的请求 Header 是否带 Authorization
接口返回 403已认证但无权限看后端权限配置、用户角色
接口返回 404API 路径错、网关未转发、后端路由未注册对照 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

标准排查顺序

  1. 先看浏览器 Network 面板:请求发出了吗?到了哪里?响应状态码?响应内容?
  2. 再看浏览器 Console 面板:有 JS 错误吗?有 CORS 错误吗?
  3. 然后看入口层日志(Nginx access log + error log):请求有没有到入口?转发到了哪个后端?
  4. 最后看后端日志:请求有没有到后端?有没有异常?

这个顺序的核心思想是从外到内,从近到远——先排除前端层的问题,再排除入口层,最后才到后端。每一层都用对应的工具,不跳过、不跨层猜测。