2.4 服务器端技术与 Ajax


2.4 服务器端技术与 Ajax

本节摘要:Ajax 是一场双边协作,前端改了请求方式,服务端也必须跟着改:从渲染页面变成输出数据接口。本节站在前后端交界处,讲清接口设计的四块工程约定——统一响应结构与错误码、无状态鉴权、分页与过滤参数规范、以及服务端如何配合 CORS——并给出可直接落地的约定模板与常见反面案例。

问题现场:三拨人写出了三套接口

一个 midway 项目的真实混乱。同一个产品里,用户模块、订单模块、消息模块分别由三个人开发,前端同学拿到三套风格迥异的接口:

  • 用户模块:成功返回 {"code": 0, "data": {...}},失败返回 {"code": 401, "msg": "未登录"},HTTP 状态码永远是 200。
  • 订单模块:成功直接返回数据本体,失败返回 HTTP 422 加一段纯文本错误信息。
  • 消息模块:成功返回 {"success": true, "result": [...]},失败返回 {"success": false, "error": {...}}

前端的统一请求封装形同虚设:每接一个模块就要写一套判断逻辑;错误提示一会儿是弹窗、一会儿是控制台静默;一个"未登录"要跳登录页的通用逻辑,得写三遍。

这就是没有接口约定的代价。Ajax 模式下,接口是前后端之间唯一的对话渠道——渠道上没有共同语言,协作成本就会以 bug 和扯皮的形式逐月复利。本节讲的就是怎么把这门共同语言定下来。

一、从渲染页面到输出数据:服务端的变化

传统服务端渲染架构里,后端的输出是整份 HTML——模板引擎把数据填进布局,浏览器拿到即可显示。这个模式下前后端代码物理上纠缠在模板文件里,"改个按钮颜色要动后端仓库"是日常。

Ajax 普及后,服务端的职责收窄为数据接口:接收请求、执行业务、返回结构化数据(JSON)。带来三个直接变化:

一是模板层消失或后移。接口不关心布局与样式,只关心业务数据的形状。渲染职责整体转移到浏览器,后端代码量与变更频率双降——改版式不再需要后端发版。

二是接口成为正式契约。既然是契约,就需要文档、版本与变更纪律。接口一变,前端就崩,"随手改个字段名"的事故代价被放大,于是接口版本化(URL 里带版本号)与变更评审成为标配。

三是多端复用成为可能。同一套接口可以同时喂网页、手机 App、小程序、第三方调用者。这正是接口设计值得投入的经济学理由:一次设计,多端受益。

一个数据接口的最小形态(以常见的服务端框架风格示意):

// 服务端:一个"查文章详情"的接口 app.get('/api/v1/articles/:id', async (req, res) => { const id = req.params.id; if (!isValidId(id)) { return res.status(400).json({ code: 'INVALID_PARAM', message: '文章编号格式错误' }); } const article = await db.findArticle(id); if (!article) { return res.status(404).json({ code: 'NOT_FOUND', message: '文章不存在' }); } res.json({ code: 0, data: article }); });

麻雀虽小,里面已经体现了几项关键约定:路径带版本、参数先校验、错误有统一结构、成功有统一包裹。下面逐块展开。

二、统一响应结构与错误码:选一种,贯彻到底

接口返回结构有两大流派,先摆出来再谈选择。

流派 A:HTTP 状态码即真相。业务成败完全用 HTTP 状态码表达:200 携带数据、401 未登录、403 无权限、404 不存在、422 参数错误、500 服务器错误。响应体只在成功时放数据、失败时放错误说明。

流派 B:HTTP 一律 200,业务码放体里。所有响应 HTTP 层都是 200,体里用 code 字段区分成败(0 或 20000 表成功),data 放数据、message 放错误描述。

两派都能工作,真正的工程灾难是混用——开篇的三套接口就是混用的受害者。两派的实质差异在于谁负责第一层分流

维度 流派 A:HTTP 状态码 流派 B:业务码包裹
网关与监控 天然可按状态码告警统计 需要解析响应体才知道失败
缓存与重试语义 4xx/5xx 明确 全 200 缓存层与重试策略要额外约定
前端判断 res.ok 一步分流 先判 code 再取 data 两步
与浏览器机制的一致性 401/403 语义被通用组件理解 需要自建映射
历史包袱 老网关可能对非 200 做拦截 部分代理会重写错误响应

我个人的倾向是接近 A 的混合方案:HTTP 状态码负责传输层与通用语义(401 未认证、403 无权限、404 不存在、5xx 服务器错误),业务细节错误(余额不足、库存不够)用 200 加业务码。理由是通用组件(网关、监控、浏览器)都按状态码工作,放弃这层信号太可惜;而"余额不足"这类业务语义,硬塞进 HTTP 数字反而别扭。无论选哪派,写进接口文档并用代码检查器强制,比选哪派重要十倍。

统一包裹结构的推荐模板:

// 成功 { "code": 0, "message": "ok", "data": { "id": 42, "title": "周报" } } // 失败 { "code": 40012, "message": "评论内容包含敏感词", "data": null }

要点:code 用数字且分段(1xxxx 用户类、2xxxx 订单类……),message 面向开发者、可含上下文;给用户的提示文案由前端决定,不要把 message 直接弹给用户——它是排障信息不是产品文案。

HTTP 本身是无状态协议——服务器默认不记得"上一个请求是谁发的"。传统页面靠 Cookie 维持会话:服务器 Set-Cookie,浏览器自动在后续请求带上。这套机制对 Ajax 同样有效,但有两个 Ajax 特有的注意点。

跨域下的 Cookie 需要双方声明。同源场景浏览器自动带 Cookie;跨域时默认不带,前端要显式声明 credentials: 'include'(Fetch)或 withCredentials = true(XHR),且服务端必须在 CORS 响应里指定具体源并允许凭证——通配符与凭证不兼容。漏配的表现很有迷惑性:请求 200,但服务器认为你没登录。

Token 方案在 Ajax 时代成为主流。登录后服务端签发访问令牌,前端存下,之后每个请求放在 Authorization 头里携带。与 Cookie 相比:不自动发送(跨站请求伪造的攻击面天然更小)、不绑定域名(App、小程序、第三方都能用同一套)、可以携带权限声明。代价是前端要自己管理令牌的存储、注入与刷新——这个"注入"动作正是 5.1 节拦截器的典型用途:

// 请求发出前统一注入令牌 演示拦截思路 const token = getStoredToken(); if (token) { xhr.setRequestHeader('Authorization', 'Bearer ' + token); }

令牌过期的处理约定也要提前定好:返回 401 加特定业务码时,前端静默用刷新令牌换新令牌并重放刚才失败的请求——用户完全无感。这套逻辑写在统一封装里(3.5 节的错误分流正好挂这一层)。

四、分页、过滤与排序:列表接口的参数规范

列表类接口是 Ajax 的高频消费者,参数规范不统一会持续制造小摩擦。约定模板:

GET /api/v1/orders?page=2&pageSize=20&status=paid&sort=createdAt:desc
  • 分页用 page 加 pageSize(或游标式 cursor 加 limit,数据变动频繁的 feed 流用游标更稳,能避免"翻页时数据平移导致的重复与跳过")。
  • 过滤参数按资源字段命名,多值用逗号分隔(status=paid,refunded)。
  • 排序用"字段:方向"的形式,支持多字段。

响应配套返回总数或下一页游标。总数放包裹结构的 meta 里还是响应头里是个小争议:放头里(自定义头如 X-Total-Count)省体积,但跨域读取需要服务端暴露该头(2.2 节的坑);放体里最省心。没有强约束时,选体里。

一个容易被服务端忽视的点:空数组与 null 要分清。"没有订单"应返回空数组而不是 null——前端对空数组可以直接渲染空态,对 null 得先判一遍,漏判就是"页面白了"。这类细节写进约定,能省下无数空值判断代码。

五、服务端如何配合 CORS

跨域是前后端必须一起解决的问题(4.2 节讲完整机制,这里先列服务端义务清单):

  • 简单请求:响应头带上 Access-Control-Allow-Origin(具体源或按白名单回显 Origin),需要带 Cookie 时同时带 Access-Control-Allow-Credentials: true,且源不能是通配符。
  • 预检请求(OPTIONS):对带自定义头或非简单方法的请求,服务端要正确响应预检,声明允许的方法、头部与缓存时间;预检结果可缓存,别让它每次都来。
  • 暴露自定义响应头:加 Access-Control-Expose-Headers,否则前端读不到分页总数这类自定义头。

服务端框架一般都有 CORS 中间件,把允许的源配成白名单列表即可。生产环境禁止无脑通配符加凭证全开——那等于把接口向任意网站敞开。

六、接口文档与联调纪律

约定不落成文档等于没有。最低限度的接口文档应包含:路径与方法、请求参数表(名、类型、必填、说明)、响应结构与各字段语义、错误码表、以及至少一对请求响应示例。现在更常见的做法是服务端代码里写注解、由工具自动生成在线文档,保持文档与代码同步演化。

联调阶段的两条纪律值得坚持:先定契约再动手——前端按文档用 Mock 数据并行开发,等服务就绪再切换;变更要打招呼——改字段、改结构必须提前通知并在文档留痕,"顺手优化"是联调期事故的第一来源。前端同学也可以在网络面板里保存请求响应样本,作为接口的"活文档"。

常见疑问解答

接口该用 REST 还是 RPC 风格

中小团队与常规业务,REST 风格(资源名加标准方法)够用且生态好;动作密集的业务(如"审批""转账")不必硬凑资源语义,动词路径也可接受。风格是工具不是信仰,团队内统一比选哪种重要。

分页用页码还是游标

后台管理类、数据相对静止的场景用页码(可跳页);feed 流、消息流这类持续追加的场景用游标(不重复不遗漏)。用页码做 feed 是"刷新后丢两条消息"的经典根因。

服务端为什么要把长整型 ID 输出成字符串

双精度浮点数装不下 64 位整数,超过安全范围后前端拿到的 ID 末位会静默变零,查库查不到、去重去不掉。2.3 节讲过精度细节,这是服务端侧的对应义务。

本节要点回顾

  • Ajax 要求服务端从"页面工厂"转为"数据服务",接口成为前后端唯一的契约,值得投入设计纪律。
  • 响应结构二选一贯彻到底:HTTP 状态码派或业务码包裹派都可以,混用才是灾难;通用语义交给状态码、业务细节交给业务码的混合方案是务实解。
  • 鉴权约定先定:Cookie 方案注意跨域凭证的双端声明;Token 方案约定注入、刷新与失败重放。
  • 列表接口规范:分页(页码或游标按场景选)、过滤、排序参数统一命名;空数组不返回 null。
  • 服务端的 CORS 三义务:允许源、响应预检、暴露自定义头;生产环境禁开通配符加凭证。
  • 文档与纪律:先契约后编码、变更提前打招呼——协作成本靠约定压下来,不靠人自觉。

一份可直接抄走的接口约定模板

把本节内容压成一页可以贴进团队文档的约定模板,新项目拿去改名字就能用。其一,路径规范:全部接口以版本号开头,资源名用复数名词、小写连字符;参数用查询串传过滤与分页,路径段只放资源标识。其二,响应规范:统一包裹三字段(业务码、消息、数据);业务码分段管理,每段一个模块;消息面向开发者,用户文案由前端维护一份映射表。其三,错误规范:字段级错误返回字段到文案的映射,非字段错误给统一结构;HTTP 层至少正确使用 401、403、404、429、5xx,别全 200。其四,鉴权规范:访问令牌放 Authorization 头,有效期与刷新机制写明;过期返回 401 加特定业务码,前端据此静默刷新重放。其五,分页规范:管理列表用页码、流式内容用游标,二选一后全线统一;空结果返回空集合不返回 null。其六,跨域规范:允许源白名单制、凭证必须显式、预检缓存十分钟起步、自定义响应头逐个暴露。

模板的使用说明比模板本身更重要:约定一旦冻结,执行靠工具不靠自觉——代码评审清单里放对应检查项、接口测试里断言响应结构、持续集成里跑契约测试。见过太多团队的约定文档写得漂亮,三个月后没人记得,问题都出在"靠自觉"三个字上。工具化的约定才是活的约定。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U