backend-service-integration

后端服务集成

YouTeacher 的 web 端是一个 Next.js 前端,它面对的不是单一的巨石后端,而是一组各司其职的后端微服务——认证、档案(profile)、职位、人才搜索、内容、后台管理——每个服务藏在自己的路径前缀之后。有四套机制让这种「散射式调用」保持统一,使得功能代码在取数据时,不必每次都重新解决网络、重试、鉴权,以及「我此刻在服务端还是在浏览器?」这些问题。

一个 HTTP 客户端,所有请求都从这里走

每一次后端调用都经由 createHttpClient 构建的单一客户端(并通过 getHttpClient 提供惰性单例)。集中化的意义在于,把横切关注点都收在一处:

  • 瞬时故障重试。 遇到 502503504 会以指数退避重试(每次尝试的基础延迟翻倍,默认三次)。非取消性的网络错误也同样处理。
  • 限流不重试。 429 会抛出带类型的 RateLimitError,其中携带服务端给出的 retry-after 提示,并只触发一次 onRateLimit 回调——界面会提示「请求过多」,而不是继续猛敲服务。
  • 会话过期是全局处理的。 401 会触发 onUnauthorized 回调,让鉴权层清掉会话状态并跳转,无论是哪一次调用撞上了过期。
  • 区分「取消」与「失败」。 因页面导航卸载而被取消的请求(AbortError,或那种其实意味着「页面已离开」的 "Failed to fetch")会被原样重新抛出,既不重试,也不当作错误记录。
  • 带类型的错误。 HttpError 携带状态码和可选的机器可读 code;RateLimitError 的类型判定被特意写成能在打包后原型链断裂的情况下依然生效。

鉴权——只讲机制层面

客户端会为出站调用附上两层鉴权,而这套设计刻意让二者相互分离:

  • 会话(session) 回答是谁在发这个请求——即用户身份——它会随带凭据的请求自动携带。
  • 一个短时有效的第一方服务令牌(service token) 回答请求来自哪里——它证明这次调用来自受信任的 YouTeacher 服务,而不是某个猜到了内部 URL 的外部调用者。在服务端,令牌在进程内签发;在浏览器端,则通过一个 server action 获取。客户端会把它加到每一个请求上。

二者分离的用意在于:用户身份永远不必被嵌进服务令牌里——令牌管的是服务之间的信任,会话管的是人。

按服务解析 URL

每个后端都有自己可配置的基础 URL,从环境配置中读取,并且有一处刻意的切分:

  • 服务端与客户端两套变体。 服务端代码在运行时动态解析 URL 并做记忆化缓存;浏览器代码则必须引用构建期字面量,因为 Next.js 在构建时就把公开配置值内联了进去,之后无法再动态读取。
  • 回退链。 某个专用基础地址(比如独立的搜索主机)在未配置时,会回退到通用的服务基础地址——于是大多数部署只需配一个 URL,其余的自动得到。
  • override 优先,否则 base + path。 解析器接受一个可选的完整 URL override;没有时,就把规范化后的 base 与 path 拼接起来。占位符构建器负责填充 :id:token:provider 这些片段。

「我在不在浏览器里?」的分支

因为服务端和浏览器从不同来源解析 URL,每个服务 API 函数都会按 typeof window 分支。职位 API 就是范本:在浏览器里读取被内联的公开常量;在服务端则调用带缓存的 getter 函数。服务端路径还为重复搜索保留了一个小的 LRU 缓存。查不到的结果映射为 null(详情请求的 404),而其它任何非 OK 响应都会被转成带类型的错误,而不是裸抛。

网关

在这一切之前,坐着一个单一的反向代理,它按 URL 路径前缀路由——每个服务一个前缀——并原样转发头部和 cookie。浏览器看到的是单一源(origin);请求则悄悄散射到拥有该前缀的那个服务。正是它,让前端可以把「后端」当作一个地址来对待,而它实际上是很多个。

Viewer 策略

这个产品里反复出现的一种形状是:「同一个动作,但观看者可能是雇主,也可能是招聘方(recruiter)。」与其在每个调用点都对 viewer 类型分支,不如用一个策略把它解析一次:fetchViewerContext 并行拉取雇主和招聘方两份档案,判定这个观看者是已授权的(已验证的雇主,或已批准的招聘方),还是未授权的(并给出具体原因)。随后 createViewerStrategy 返回一个适配器——雇主的或招聘方的——它们对外暴露相同的 fetchQuota / fetchUnlockedTalents 接口。功能代码握着一个策略,从此不必再问「这是哪种观看者?」。

为什么它读起来像一个系统

单看每一块都不稀奇——一个会重试的 fetch 封装、一张由环境驱动的 URL 表、一个反向代理、一个策略对象。让这套集成显得连贯的,是它们的层层咬合:URL 表喂给 isBrowser 分支,isBrowser 分支喂给那个唯一的客户端,那个客户端承载鉴权与重试,而网关让这一切看起来像同一个源。要新增一个后端端点,只需在配置里写明它的 base 和 path,再写一个带分支的 fetch 函数——重试、鉴权令牌、错误类型化、路由,都会自动随之而来。

about this entry

One of sijie's wiki entries. The AI on this site is grounded in the same corpus and answers in sijie's voice, with citations back to entries like this one — answering costs sijie money, so it waits behind a code: enter an access code →

backend-service-integration