后端服务集成
YouTeacher 的 web 端是一个 Next.js 前端,它面对的不是单一的巨石后端,而是一组各司其职的后端微服务——认证、档案(profile)、职位、人才搜索、内容、后台管理——每个服务藏在自己的路径前缀之后。有四套机制让这种「散射式调用」保持统一,使得功能代码在取数据时,不必每次都重新解决网络、重试、鉴权,以及「我此刻在服务端还是在浏览器?」这些问题。
一个 HTTP 客户端,所有请求都从这里走
每一次后端调用都经由 createHttpClient 构建的单一客户端(并通过 getHttpClient 提供惰性单例)。集中化的意义在于,把横切关注点都收在一处:
- 瞬时故障重试。 遇到
502、503、504会以指数退避重试(每次尝试的基础延迟翻倍,默认三次)。非取消性的网络错误也同样处理。 - 限流不重试。
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 函数——重试、鉴权令牌、错误类型化、路由,都会自动随之而来。