☰
ASP.NET Core前后端分离实战:从架构分层到JWT与部署
2026/10/9 10:39:56 网站建设 项目流程

前后端分离开发这个词,放到 ASP.NET Core 的语境里,基本是每个 .NET 后端工程师都躲不开的坎。我从最早用 Razor 页面堆功能,到后来负责把一套单体系统拆成 Web API + SPA,中间踩过的坑足够写一本小册子。这篇内容不打算做泛泛的原理介绍,而是把我实际拆项目时验证过的思路、结构和操作细节整理出来:项目怎么分层、Web API 怎么写、跨域和 JWT 怎么配、部署到底该用哪种姿势,以及联调期最容易爆雷的几点。不管你是刚把第一个 API 跑通的新手,还是已经在用 MVC 写内部系统的老开发,这套东西应该都能帮你少走几趟弯路。

1. 前后端分离开发的核心思路与项目架构设计

1.1 为什么要把前后端拆开:单体应用的真实痛点

很多团队第一次动“前后端分离”的念头,不是因为技术时髦,而是因为痛。一个典型的 ASP.NET Core MVC 项目里,Controller 返回 View,View 里写 Razor,页面逻辑和后端逻辑混在一起。刚开始三五个人做个小系统,这套打法很顺手,改个按钮、调个列表,直接在 cshtml 里改完就发布,效率确实高。

但业务一旦复杂起来,问题就藏不住了。前端要换个 UI 框架,比如从 jQuery 换到 Vue 或 React,你必须动整个 MVC 项目,稍有不慎连带后端一起改。产品说页面上某个模块要 A/B 测试,前端同学需要独立调试和发布,后端却被绑在一起,谁都不敢随便上线。最要命的是,后端每次发布都会影响在线用户,哪怕你只是改了某个页面的标题。前后端分离之后,这些问题从架构层面被拆开:后端只提供 API,前端只消费 API,彼此通过一份清晰的接口契约协作,发布节奏也能彻底分开。

这个演进逻辑和微服务有点像:不是所有系统都需要微服务,同样也不是所有系统都必须前后端分离。判断标准始终是交付效率。如果你的团队里前端和后端已经是两个角色,或者你打算引入专业前端框架,那“Web API + 静态资源/SPA”就是最稳妥的拆法。只要接口契约稳定,前端可以先拿 mock 数据开发,后端可以专注性能和稳定性,两边并行推进,等到联调阶段再合流。

1.2 项目结构设计与目录规划

很多人以为前后端分离就是“后端建一个 Web API 项目,前端建一个 Vue 项目”,结果代码写了两周就开始乱。真正值得先想清楚的,是后端项目内部怎么分层。我建议先按职责把一个大的解决方案拆成几个层次,而不是把所有 Controller、Service、Repository 全部塞进同一个项目。

一个我常用的后端结构大致是这样:

项目/目录职责典型内容
WebApi呈现层,只负责接收 HTTP 请求和返回响应Controllers、Filters、Middleware、DTO
Application业务用例层,处理业务逻辑和事务边界Services、Interfaces、业务规则
Domain核心领域层,不依赖任何基础设施Entities、ValueObjects、领域服务
Infrastructure基础设施层,封装外部依赖DbContext、仓储实现、第三方 SDK

前端项目建议单独建仓,或者至少在同一个解决方案里用独立目录隔离,不要跟后端代码混在一起。这样做的原因有两个:一是避免后端 CI/CD 构建时被前端 node_modules 拖慢;二是前后端发布节奏不同,放一起容易被相互牵制。

目录分层时有几个容易忽略的细节。第一,WebApi 项目里的 DTO 不要随手放在 Controller 文件里,更不要把 Entity 直接暴露给前端。实体类往往带着导航属性、审计字段甚至一些内部状态,直接序列化出去既有过度暴露风险,又容易产生循环引用。第二,Application 层不要引用 WebApi 层,依赖方向应该是向内的,保证核心业务规则不被 HTTP 细节污染。第三,DbContext 放在 Infrastructure 层,但它的接口定义可以放到 Domain 或 Application,这样切换存储实现时不会把整个上层都拖下水。

1.3 技术选型与配套方案

选型这件事没有银弹,核心是看团队熟悉度和维护成本。后端只要确定用 ASP.NET Core,建议直接选 LTS 版本,比如 .NET 6、.NET 8,不要因为追求新特性去用 Preview。前端框架目前在 Vue 和 React 之间选哪个都行,重点是团队能长期维护。API 风格上,绝大多数业务系统用 REST 就足够了,没必要为了“先进”直接上 GraphQL,除非你有强列的字段级定制需求,且愿意承担查询复杂度和缓存成本。

围绕前后端分离开发,有一组配套方案几乎是标配:

  • 接口文档:Swashbuckle / NSwag 生成 OpenAPI 文档,Swagger UI 调试。
  • 认证方案:JWT Bearer Token,适合前后端完全分离的场景。
  • ORM:EF Core,配合迁移机制管理数据库变更。
  • 前端 HTTP 库:axios,统一封装请求与响应拦截。
  • 部署:Nginx 反代 API 和静态文件,或 IIS 承载后端。

这套方案的好处是不折腾。也许每一样单独拎出来都不算“最酷”,但它们组合在一起踩坑最少。我见过一上来就搞 Duende IdentityServer、K8s、Blazor 混合渲染的团队,最后往往因为这些方案太重,反而拖慢了业务进度。前后端分离开发的核心目标是让团队跑得更快,选型一定要服务这个目标。

2. 基于 ASP.NET Core 搭建 Web API 的核心细节

2.1 创建项目与基础配置:Startup 与 Program 的取舍

现在创建 ASP.NET Core Web API 项目很简单,命令行一条dotnet new webapi就搞定。.NET 6 之后模板做了大瘦身,不再强制拆 Program.cs 和 Startup.cs,所有配置都可以集中在 Program.cs 里完成。我建议中小项目直接沿用这个新模式,配置简单、阅读顺序清晰;只有当你觉得 Program.cs 开始膨胀到影响阅读时,再按功能拆成扩展方法或自定义中间件。

一个最基础的 API 项目配置大概是这样的:

using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.IdentityModel.Tokens; using System.Text; var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); var app = builder.Build(); if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseHttpsRedirection(); app.UseCors("AllowWebClient"); app.UseAuthentication(); app.UseAuthorization(); app.MapControllers(); app.Run();

这段代码里最关键的是中间件顺序。UseCors要在UseAuthentication和UseAuthorization之前,否则带 Token 的跨域请求可能还没走到认证就被拦截。UseAuthentication又必须排在UseAuthorization前面,这是 ASP.NET Core 的固定约定,顺序错了,[Authorize]特性会直接失效或导致 401 逻辑异常。

项目创建后第一件事,不是写业务接口,而是把配置项补齐。连接字符串、JWT 密钥、允许跨域的域名、文件上传路径,这些都应该进appsettings.json,并且敏感内容在本地用user-secrets管理,部署环境用环境变量覆盖。把密钥写死在代码里这种事,至今仍然在不少团队出现,审计起来非常尴尬。

2.2 控制器设计、路由约定与 DTO 边界

控制器是前后端分离开发里的“门面”,前端看到的接口列表好不好用,规范不规范,几乎就看控制器这一层。我比较推荐 RESTful 风格,但不必走到教条的地步。资源名用复数、URL 用小写、用 HTTP 动词表达操作,这些基础约定值得坚持:

  • GET/api/orders:获取订单列表
  • GET/api/orders/{id}:获取单个订单
  • POST/api/orders:创建订单
  • PUT/api/orders/{id}:更新订单
  • DELETE/api/orders/{id}:删除订单

路由使用属性路由,控制器上加[Route("api/[controller]")],这样控制器叫OrdersController,路由自动变成/api/orders。如果团队对命名敏感,也可以显式写死[Route("api/orders")],避免未来把控制器改名时无意中改动对外路径。

真正决定接口可维护性的,是 DTO 的边界。前端需要什么字段,DTO 就给什么字段,不要直接把 EF Core 实体返回出去。一个实体可能有IsDeleted、CreatedBy、UpdatedAt这类内部字段,前端不该知道,也不该被反序列化时接收。另一个常见错误是在查询列表时返回了包含导航属性的实体,把整个关联表都序列化出来,导致响应体又大又慢,甚至循环引用 500。

DTO 和实体之间的映射,可以用 AutoMapper,也可以手动写static映射方法。项目不大时我甚至建议手动写,逻辑直观,排查时不用去翻 Profile 配置。等映射逻辑真的多到痛了,再上 AutoMapper 也来得及。

2.3 统一响应格式与全局异常处理

前后端分离之后,前端拿到的响应必须稳定可预测。我习惯在 API 层包一层统一响应格式,让前端能统一处理成功和失败。最简单的一种设计是:

public class ApiResponse<T> { public int Code { get; set; } public string Message { get; set; } = string.Empty; public T? Data { get; set; } }

成功时Code=0,前端判断code === 0再取数据。业务异常时返回业务错误码,比如库存不足、权限不足,这些是通过正常 HTTP 200 但业务失败的场景。而系统异常、参数校验失败这类情况,我倾向返回对应的 HTTP 状态码,避免前端把所有错误都当成业务错误处理,掩盖了真正的故障。

这里有一个度的问题:不要把统一响应格式当教条。文件下载、OAuth 跳转这类接口,返回原始内容更合理。所以实践上我会定义一个统一基础类,但允许部分接口直接返回FileResult或RedirectResult,前端在拦截器里针对 Content-Type 再做分支。

全局异常处理是前后端分离里最容易疏忽但价值极高的部分。ASP.NET Core 里有几种实现方式,最简单的是自定义一个中间件,包住整条请求管道:

app.Use(async (context, next) => { try { await next(); } catch (Exception ex) { context.Response.StatusCode = 500; await context.Response.WriteAsJsonAsync(new { code = 1, message = "服务器内部错误" }); // 这里要把 ex 完整信息记录到日志,不能只记 Message } });

生产环境绝不能把异常堆栈抛给前端,那既暴露内部细节,也容易被攻击者利用。日志里倒是要记全,包括堆栈、TraceId、请求路径和用户信息,否则排查问题时两眼一抹黑。.NET 8 里还提供了IExceptionHandler接口,逻辑上比裸中间件更清晰,推荐优先使用。

2.4 数据校验与 Swagger 文档的配合

接口是给前端调的,参数一旦传错,后端必须第一时间给出明确提示。ASP.NET Core 的模型绑定和数据校验支持得很好,控制器方法参数如果用了[ApiController]特性,模型校验失败会自动返回 400,无需手动写一堆if (!ModelState.IsValid)。

简单的规则直接用 DataAnnotations 就够,比如[Required]、[MaxLength]、[Range]。复杂规则建议引入 FluentValidation,把校验逻辑从模型里剥离出来,控制器的代码会干净很多。比如创建订单时,订单金额要大于零、订单项不能为空、收货人和地址必须同时存在,这些写在一个CreateOrderValidator类里,比写在模型属性上更清晰。

Swagger 在前后端分离开发里不只是文档,它还是前后端的“活契约”。配好 Swagger 后,前端可以直接从 Swagger UI 看到每个接口的入参出参,也能直接试调用。为了让 Swagger 更好用,有几个小习惯值得养成:

  • 给 DTO 的属性加 XML 注释,并把 XML 文档生成打开,Swagger UI 里就能显示字段说明。
  • 在Program.cs里配置AddSwaggerGen时带上UseInlineDefinitionsForEnums(),枚举的可读性会好很多。
  • JWT 接口需要配置 Swagger 的认证按钮,否则前端对接时要手动复制 Token。
builder.Services.AddSwaggerGen(options => { var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); options.IncludeXmlComments(xmlPath); });

这里容易踩的坑是 XML 注释路径配置不对,Swagger UI 里字段永远没说明。检查一下项目文件是否包含<GenerateDocumentationFile>true</GenerateDocumentationFile>,没有的话 XML 文件根本生成不出来。

3. 前后端联调中的跨域、认证与部署实操

3.1 CORS 配置的正确姿势与常见坑

前后端分离之后,最常见的第一道坎就是跨域。前端跑在http://localhost:5173,后端跑在http://localhost:5000,前端一把请求发过去,浏览器直接报CORS policy。CORS 是浏览器层面的安全机制,不是后端接口真的拒绝了请求,而是浏览器发现响应里缺少允许标记,就把响应藏了起来。

ASP.NET Core 里配置 CORS 不复杂,但要区分开发和生产。开发时可能前端和后端端口不同,或者用 Vite/Webpack 代理时没有跨域问题;生产时通常用 Nginx 反代同源,也不一定需要开 CORS。真正需要 CORS 的场景是“前端域名”和“后端域名”确实不同,例如前端在https://web.example.com,后端在https://api.example.com。

一个稳妥的配置是这样:

builder.Services.AddCors(options => { options.AddPolicy("AllowWebClient", policy => { policy.WithOrigins("https://web.example.com") .AllowAnyHeader() .AllowAnyMethod() .AllowCredentials(); }); });

这里最常踩的坑是两个。第一,AllowAnyOrigin()和AllowCredentials()不能一起用。带 Cookie 或带Authorization凭据的跨域请求,浏览器要求响应里不允许出现通配符*,必须明确指定来源。第二,配置了 CORS 策略不代表自动生效,必须在管道里调用app.UseCors("AllowWebClient"),而且位置要在匹配请求的中间件之前。

我在实际项目里见过一种很低级的问题:前端配置了withCredentials: true去调后端接口,后端只写了AllowAnyOrigin(),结果浏览器一直报跨域错误。排查了半天才意识到是凭据和通配来源冲突。所以跨域问题排查时,先看 Access-Control-Allow-Origin 具体返回了什么,比猜配置有用得多。

3.2 JWT 认证授权实战

前后端分离下的认证方案,JWT 是主流选择。它的思路是:用户登录成功后,后端签发一个包含用户标识和过期时间的 Token,前端后续请求带上这个 Token,后端验证签名后即可信任用户身份。整个过程不需要服务端存储会话,天然适合无状态 API。

在 ASP.NET Core 中使用 JWT 要先装Microsoft.AspNetCore.Authentication.JwtBearer,然后在服务和管道里注册。一个典型的配置如下:

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidIssuer = builder.Configuration["Jwt:Issuer"], ValidateAudience = true, ValidAudience = builder.Configuration["Jwt:Audience"], ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey( Encoding.UTF8.GetBytes(builder.Configuration["Jwt:SigningKey"]!)), ValidateLifetime = true, ClockSkew = TimeSpan.FromSeconds(30) }; });

这里的SigningKey必须放到安全的配置来源,开发环境可以用dotnet user-secrets,生产环境用环境变量或密钥管理服务,绝不能提交到 Git 仓库。密钥长度也有讲究,HS256 要求至少 32 字节,太短会在运行时直接抛SecurityTokenInvalidSigningKeyException。

签发 Token 的代码通常在 Login 接口里:

var tokenHandler = new JwtSecurityTokenHandler(); var now = DateTime.UtcNow; var token = tokenHandler.CreateToken(new SecurityTokenDescriptor { Subject = new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()), new Claim(ClaimTypes.Name, user.UserName), new Claim(ClaimTypes.Role, user.Role) }), Issuer = config["Jwt:Issuer"], Audience = config["Jwt:Audience"], Expires = now.AddMinutes(60), SigningCredentials = new SigningCredentials( new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config["Jwt:SigningKey"]!)), SecurityAlgorithms.HmacSha256) }); return new { token = tokenHandler.WriteToken(token), expiresAt = now.AddMinutes(60) };

注意时间统一使用 UTC,避免服务器时区问题导致 Token 被判定为过期。在控制器上,需要登录才能访问的接口加[Authorize],需要特定角色的加[Authorize(Roles = "Admin")],登录接口本身加[AllowAnonymous]。JWT 是无状态的,服务端不能主动吊销某个 Token,所以退出登录通常在前端本地清除 Token,敏感操作则要求用户重新登录。

3.3 前端调用与 axios 拦截器的配合

后端把 API 和 JWT 都准备好了,前端这一侧如果处理得糙,联调照样一团糟。现在前端项目里最常用的 HTTP 库是 axios,我建议在一开始就封装一层统一的 service 实例,而不是每个页面直接用 axios 裸调。

一个比较基础的封装可以包含请求拦截和响应拦截:

import axios from 'axios'; const service = axios.create({ baseURL: '/api', timeout: 10000 }); service.interceptors.request.use(config => { const token = localStorage.getItem('access_token'); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }); service.interceptors.response.use( response => { if (response.data && response.data.code === 0) { return response.data.data; } return Promise.reject(new Error(response.data?.message || '请求失败')); }, error => { if (error.response && error.response.status === 401) { localStorage.removeItem('access_token'); window.location.href = '/login'; } return Promise.reject(error); } ); export default service;

这里有几个细节想特别提醒。第一,baseURL配成/api而不是完整的后端地址,可以让前端和服务端在部署时通过同一域名访问,省掉大量跨域问题。开发环境用 Vite 的 proxy 把/api代理到后端 Kestrel 端口,生产环境用 Nginx 反代,前后端就像在同一个站点里。第二,Token 存localStorage存在 XSS 风险,如果页面上有任何脚本能被注入攻击者,Token 就可能被拿走。更稳妥的做法是 Token 放内存变量,配合刷新 Token 机制,或者后端把 Token 写到 HttpOnly Cookie 里。内部系统可以先上 localStorage,但一定要清楚风险边界。

第三,响应拦截里只处理code === 0会导致文件下载、PDF 预览这类接口出问题。文件接口通常走的是responseType: 'blob',返回结构完全不同。我会在拦截器里判断response.config.responseType === 'blob'就直接返回原始response,避免统一格式逻辑把二进制流弄坏。

3.4 部署方案:IIS、Nginx 与 Docker 的取舍

前后端分离项目的部署,本质上就两件事:前端静态文件怎么托管,后端 API 怎么暴露。最简单的入门方案是让 ASP.NET Core 直接托管前端静态文件。把前端打包后的wwwroot放进 API 项目,调用app.UseStaticFiles(),再用MapFallbackToFile("index.html")兜底前端路由,一个进程搞定一切。但这样做后端发布又会包含前端资源,不算彻底分离,只适合小系统。

更标准的做法是前后端分开部署:

  • 后端发布成 Kestrel 服务,监听某个本机端口。
  • 前端打包成纯静态文件,交给 Nginx 或 CDN 托管。
  • 网关或反向代理把/api请求转发给后端,其他路径服务前端静态文件。

Nginx 的配置大致是:

server { listen 80; server_name web.example.com; root /var/www/web-client/dist; index index.html; location /api/ { proxy_pass http://127.0.0.1:5001; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } location / { try_files $uri $uri/ /index.html; } }

这里try_files $uri $uri/ /index.html是 Vue Router 或 React Router 使用 history 模式的必要条件,否则刷新/orders/123会直接 404。proxy_set_header X-Forwarded-Proto也很关键,否则后端 HTTPS 终止在 Nginx,但 ASP.NET Core 还以为是 HTTP 请求,生成的一些绝对链接会出问题。

如果用 Docker,我一般建议分开构建镜像:前端先用node:20构建产物,再用nginx镜像托管;后端用mcr.microsoft.com/dotnet/aspnet:8.0作为运行时镜像。两个容器之间通过 Docker Compose 编排,Nginx 容器用depends_on指向后端容器,并在配置里用服务名作为后端地址,比如proxy_pass http://backend:5001;。这样一套下来环境复现很容易,团队里每个人拉起来就能联调。

4. 前后端分离开发中的常见问题与排查实录

4.1 跨域请求被拒:预检与凭据的排查思路

跨域问题虽然常见,但排查思路其实很固定。我遇到过的案例里,大多数都出在三个地方:预检请求没过、凭据模式和 CORS 策略冲突、中间件顺序问题。

浏览器发起带非简单头部的请求前,会先发一个OPTIONS预检请求。如果后端没有针对OPTIONS返回合适的 CORS 头,浏览器会直接拦截。排查时打开浏览器开发者工具,看 Network 面板里那条失败的请求,如果有一条OPTIONS请求状态码不是 2xx,或者响应头里没有Access-Control-Allow-Origin,问题就清晰了。ASP.NET Core 里这类问题多半是UseCors没调用,或者调用的位置不对。

凭据模式问题前面提过,AllowAnyOrigin和AllowCredentials互斥。还有一种隐蔽情况是前端用 axios 默认不会带 Cookie,除非显式设置withCredentials: true;后端同时允许了凭据和多个来源,也会因为响应头里的来源没有精确匹配当前页面地址而失败。最简单的排查方式是记录一条 curl 请求带上Origin头,直接看响应头里到底返回了什么。

现象可能原因快速验证
OPTIONS 请求 404不是 API 路径,或中间件顺序错误访问后端根路径检查 CORS 配置
有 OPTIONS 但无 Allow-OriginUseCors 未调用或策略名不匹配检查 Program.cs 里的 app.UseCors
Allow-Origin 是 * 但仍失败与 AllowCredentials 冲突改用 WithOrigins 明确指定域名
http 和 https 混用浏览器安全策略阻止检查页面协议和 API 协议是否一致

4.2 接口 404/405 与静态资源托管问题

前后端联调中,404 和 405 是仅次于跨域的高频问题。接口路径明明存在,前端却收到 404,九成是路径不匹配。ASP.NET Core 的路由区分大小写吗?默认行为并不严格,但前端如果拼错单词、多写个尾斜杠、或者把api写成了apis,都会导致匹配失败。这类问题没有技巧,仔细对一遍 Swagger 里的 URL 最有效。

405 通常是 HTTP 动词不匹配。前端用 POST 去调一个只支持 GET 的接口,或者提交表单时没有指定Content-Type: application/json,请求体绑定失败,都有可能落到 405 或 415。我见过一个典型案例:前端把PUT请求发成了POST,后端路由是PUT /api/orders/{id},结果返回 405,前端盯着代码看了半天都没发现,最后还是抓包才抓到真实请求。

静态资源托管问题的重灾区是 SPA 刷新。前端用的是 history 路由,路径是/orders/123,Nginx 直接去找orders/123这个文件,找不到就 404。解决办法就是前面说的try_files $uri $uri/ /index.html。如果后端托管,则是MapFallbackToFile("index.html")。这两行配置能解决 90% 的“刷新页面就白屏”问题。

4.3 认证票据丢失与过期时间踩坑

JWT 认证不会绝对安全,但很多问题其实是配置细节。我接手过的项目里,Token 相关的故障主要集中在这几点:

第一,ClockSkew默认值是 5 分钟。这意味着哪怕 Token 设置了 1 分钟过期,实际有效时间也可能被放宽到 6 分钟。如果业务对过期时间敏感,比如交易类接口,一定要把ClockSkew调小。第二,签发和验证用的密钥不一致,常见于配置文件被多环境覆盖,比如本地是appsettings.Development.json的 Key,生产环境环境变量没设置成功,导致后端启动时用了默认空值。这个问题只有在并发量上来后才会暴露,非常恶心。

第三,认证中间件没有在授权前面执行。前面提到UseAuthentication()必须在UseAuthorization()之前,如果顺序反了,[Authorize]的接口不会按预期拒绝未授权请求,或者返回的 401 信息很怪。

第四,前端 Token 过期后只清掉了本地 token,但用户还在页面上,之后所有接口都 401,vue-router 或 react-router 也没有做统一跳转。更好的做法是在响应拦截器里收到 401 后,不只会跳转,还要通知状态管理清空用户信息,避免页面显示“已登录”但接口全部失效的假状态。

如果要更精细地控制 Token 生命周期,可以设置短 AccessToken 加长 RefreshToken。每次刷新 Token 时更新内存中的 Token,也能缓解 XSS 盗取长期 Token 的风险,但代价是后端要额外做刷新接口和客户端定时刷新逻辑。内部系统可以先不搞,对外系统迟早要上。

4.4 API 性能优化与团队协作规范

前后端分离以后,接口性能问题会被前端直接体验放大,因为用户感知到的等待时间就是 API 响应时间。最常见的问题不是并发不够,而是代码写得不合适。比如在循环里逐个查询数据库,早该用Include或投影一次查出来,却写成了 N+1 次查询。EF Core 里顺手就能避免:

// 错误:循环里查数据库 foreach (var item in orderItems) { var product = await db.Products.FindAsync(item.ProductId); } // 正确:一次查出所需商品 var productIds = orderItems.Select(x => x.ProductId).Distinct(); var products = await db.Products .Where(p => productIds.Contains(p.Id)) .ToDictionaryAsync(p => p.Id);

列表接口一定要分页,不要一次性把全表数据返回。前端做虚拟滚动也好,后端做偏移量分页也好,必须限制单页大小,同时返回总条数,让前端能渲染分页器。如果统计类接口数据量大,考虑用ResponseCache或 Redis 缓存,设置合理的过期时间。ASP.NET Core 的响应压缩中间件也可以开起来,对 JSON 响应效果很明显。

相比后端性能,我更想提醒的是团队协作规范。前后端分离后,接口契约就是两边的“合同”,没有契约管理,联调就是灾难。我的建议是:接口先行、文档随动。后端写代码前先把 OpenAPI 文档定下来,前端照文档并行开发;接口变更先改文档、再改实现,并在每周评审里知会对端。Swagger 是接口的活文档,但不能取代口头沟通,复杂接口最好在评论里写清业务背景和特殊规则。再加上 Postman/Insomnia 集合共享,联调效率会高很多。

5. 我在实际项目中的几点体会

5.1 并不是所有项目都适合前后端分离

前面讲了这么多前后端分离的好处,但我也要补一句实话:如果你的项目只有十个页面,前后端各只有一个人,而且业务变化极快、根本没时间维护接口文档,那么老老实实用 MVC、Razor Pages 或者 Blazor Server 反而更高效。分离是有成本的:多一层跨域、多一道认证、多一份契约管理、多一套部署链路。这些成本在团队规模和组织复杂度上来之后是值得的,但对一个三天上线的小工具来说,纯属给自己找事。

不要因为“技术先进性”去拆架构。我做过的案例里,最成功的一次拆分,恰恰是因为业务真的复杂到前端需要独立上线、后端需要独立扩容才动手的。真正的架构演进,应该跟着业务痛点和团队结构走,而不是跟着技术潮流走。

5.2 文档先行:接口契约的维护

如果你决定拆,那第一件事就是定接口契约。我在项目启动时会给每个接口规定好命名、参数、响应格式和错误码,Swagger 里的 Schema 也会先画出来。前端可以基于契约生成 TypeScript 类型,或者用 OpenAPI 自动生成 API client,联调时的低级错误能减少一半以上。

契约变更这件事,最容易在项目中期崩掉。产品临时加了个字段、后端删了个字段、前端没跟上,大家就开始互相甩锅。我的做法是接口变更必须同时在 Swagger 里更新,并同步更新客户端类型生成脚本,加字段时默认兼容旧版本,删字段或改类型则必须走评估。这样做虽然繁琐,但留给团队的安全感是巨大的。

5.3 最后分享一个调试小技巧

前后端分离联调时,最烦的就是本地环境和后端不一致。我现在比较习惯用 Vite 的代理做联调,前端只配一个代理目标,后端开发机地址变了,也只改一处配置,而不是让每个前端页面都去改 baseURL。

// vite.config.js export default { server: { proxy: { '/api': { target: 'http://localhost:5001', changeOrigin: true } } } }

另外,后端接口日志不要只记Information,要记录请求路径、请求耗时、用户 ID、TraceId。遇到线上问题,只有结构化日志才能快速串起一条请求链路。我以前就在Program.cs里用 Serilog 输出到控制台和文件,加上请求耗时中间件,排查慢接口基本靠日志就能定位,不必反复远程到服务器上打断点。

前后端分离开发这条路没有终局,技术选型会变,但拆分和协作的思路是稳定的。把接口边界划清楚,把文档和日志工具用起来,剩下的问题大多只是时间问题。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询