首页 / 文章

URL状态、服务器状态与BFF:用于内部React应用的Vite架构

为何经过身份验证的内部 React 平台使用 Vite、TanStack Router、TanStack Query 以及独立的 BFF 能获得更好支持,而 Next.js 则无法做到;以及这种情况在何时不再成立。

发布者:REACTAPP.TOP阅读约 10 分钟2310 词

来源: Next.js is excellent. That’s exactly why you **shouldn’t** use it everywhere.

当一个新的 React 项目启动时,还来不及描述应用功能,“我们应该使用 Next.js 吗?”这个问题的答案往往早已是肯定的。对于那些以内容为主的公开网站,这种选择通常没错。但对于需要用户登录后整天进行数据筛选和记录编辑的授权操作平台而言,该框架的许多强大功能反而会解决应用本身并不存在的问题。本文将探讨此类平台实际所需的架构,展示如何通过 React、Vite、TanStack Router、TanStack Query 以及前端专用后端(BFF)组成的组合式技术栈来实现该架构,并最后给出一份清单,帮助判断在何种情况下 Next.js 才是更合适的选择。

这里没有任何内容表明 Next.js 劣质。它仍是目前最优秀的 React 框架之一。反对的观点只是认为不应在不先评估其功能是否满足需求的情况下就默认采用它。

一个具体案例

假设有一个由五名工程师组成的小团队,正在构建一个内部平台,该平台拥有超过100条路由、300多个 API 端点,且每月约有百万次 API 调用。他们本可以用 Next.js 来构建这个平台,但却选择了 React 搭配 Vite,使用 TanStack Router 处理路由、TanStack Query 管理数据,并在前端与后端服务之间设置独立的 BFF。在开发六个月后,这一选择依然有效,不是因为 Next.js 会出问题,而是因为该平台几乎没有用到 Next.js 最擅长的那些功能。

如需针对不同项目类型对这两种方案进行更全面的比较,请参阅Next.js还是纯React:如何选择适合您项目的框架。本文的讨论范围更为狭窄,重点在于带有身份验证功能且数据量较大的应用在各个层面上需要什么。

在选择框架之前先描述应用程序

Next.js之所以成为标准选择,是因为它集成了众多功能:基于文件的路由与布局、SSR与服务器组件、缓存层、中间件、同一项目中的后端代码、优化过的图片以及庞大的生态系统。对于许多产品而言,这种集成方式非常理想。不过更重要的第一个问题是您正在构建的是何种类型的应用程序——两个React应用在架构上可能几乎没有任何共同点。

公共商店页面

典型的公共电子商务网站通常具备:

  • 成千上万的页面,任何人都能访问
  • 来自搜索引擎的流量
  • 真正的SEO需求
  • 内容可通过服务器渲染来优化的产品页面
  • 可分享的公共URL及社交平台预览功能
  • 大量静态内容或更新缓慢的内容

内部操作平台

典型的内部工具具备以下特点:

  • 几乎所有界面都需要身份验证
  • 客户搜索功能、筛选条件以及分页表格
  • 表单、控制面板以及权限检查机制
  • 频繁的数据变更及近乎实时的数据更新
  • 大量的服务器状态信息
  • 高度交互式的界面
  • 已存在的API或BFF层

这两种应用虽然都是React开发,但解决的却是不同的问题,架构设计也应体现这一点。

用户是操作应用而非仅阅读内容

在内部平台上,用户登录后一整天都在该应用程序中工作。他们并非通过搜索结果进入、阅读文章或浏览产品列表,而是进行搜索、筛选、排序、打开对话框、提交表单、修改记录、切换标签页、翻阅表格内容、刷新数据以及在不同相关实体之间跳转。他们是在运行应用程序,而非访问网站,这种差异决定了哪些框架功能才是重要的。

SEO不再是一项必要条件

以 /customers/12345 这样的路径为例,它显示的是包含敏感数据的已认证客户记录。你显然不希望搜索引擎对其建立索引,也不需要爬虫发现它,更无需首条 HTML 响应对爬虫友好。该页面的价值在于有授权用户会与其交互,而非因为它容易被找到。因此,选择 Next.js 的主要理由之一其实作用不大。这是需求不匹配的问题,而非框架本身的缺陷。

服务器组件适用的场景与不适用的场景

服务器组件确实是一种非常有用的模型,所以这个问题值得仔细考虑。想象一个包含多个板块的客户操作页面:

Customer
├── Profile
├── Loans
├── Payments
├── Transactions
├── Documents
└── Activity

页面一加载,用户就开始对其进行修改。例如,他们会筛选付款列表:

Status: Failed
Type: ACH
Date: Last 30 days

接着他们会分页、排序、搜索,开启事务并修改某些内容。视图会不断因用户输入和最新的服务器数据而发生变化。主导流程并非这种文档式的循环:

Request → Render document → Read document

它更接近于一个交互循环:

User interaction
       ↓
Application state
       ↓
Server state
       ↓
API
       ↓
Updated UI

如果你的界面越像第二张图所示,那么客户端架构在决定质量方面就越重要:包括状态的存储方式、数据的缓存方式以及用户界面对变化的响应方式。服务器组件仍然可以渲染此类页面的部分内容,但它们并不能消除对强大客户端数据层的需求。

将URL视为应用状态

假设某操作员当前处于某个客户的支付页面:

/customer/12345/payments

他们会应用一组过滤器:

status=failed
type=ach
amountMin=500
page=2

如果这些过滤器被编码在地址中,那么URL就会变成:

/customer/12345/payments
  ?status=failed
  &type=ach
  &amountMin=500
  &page=2

此时,URL已不仅仅是导航工具,更是应用程序状态的一部分。工程师可以将它复制给同事,对方会直接进入相同的客户信息、相同的分页标签、相同的筛选条件以及相同的查询结果页面。此外,URL在页面重新加载后依然有效,也能让返回按钮按用户预期工作。在多个界面之间实现这一功能,正是类型安全路由器发挥作用的关键所在。

TanStack Router的贡献

TanStack Router将路由视为设计中的核心要素。路由、路径参数和搜索参数都可以在类型系统中进行定义,当应用程序的路由数量超过一百个时,这一点尤为重要。到了这种规模,路由已不再仅仅是“哪个组件在此渲染”的问题,而成了架构的一部分。例如对于如下这样的路由:

/customer/$customerId/payments

你需要:

  • 定义清晰、带有类型的路径参数
  • 经过验证且类型可预测的搜索参数
  • 由编译器检查的导航调用,因此参数重命名会导致构建失败而非链接错误
  • 按路由声明的加载状态和错误状态
  • 在路由级别设置的身份验证保护
  • 始终有效的深度链接
  • 关键在于,这一切都不依赖于页面是否在服务器上渲染。对于完全在浏览器中运行的内部应用而言,这非常合适。

    服务器状态才是真正的核心

    大型内部应用的重点更多在于远程数据,而非页面。它不断读写的数据实体如下所示:

    Customers
    Loans
    Payments
    Transactions
    Documents
    Users
    Accounts
    Reports
    

    对于每一个这样的数据实体,前端都需要回答相同的问题:

    • 这些数据已经在内存中了吗?
    • 它们是否已经过期?
    • 是否需要重新获取,以及何时获取?
  • 加载时界面会显示什么?
  • 请求失败时会发生什么?
  • 是否有其他组件可以共享该结果?
  • 数据变更后有哪些变化,哪些查询需要失效?
  • 用户返回时应立即显示什么内容?
  • 界面能否以乐观方式更新?
  • 这就是服务器状态管理,也正是 TanStack Query 所要解决的难题:以查询为键的缓存、后台数据重新获取、重复请求的去重处理、数据变更钩子以及有针对性的状态失效机制。由于路由器负责管理 URL 状态,而 Query 负责管理服务器状态,各层职责分明,组件本地状态则仅限于与界面直接相关的内容,比如对话框是否已打开。

    将 BFF 作为 API 边界

    在这种架构下,前端不会直接调用数十个微服务,而是与BFF通信,再由BFF去联系这些服务。这样的分离使得前端无需了解后端的实现细节,比如响应是由一个服务生成还是五个服务共同生成。API契约由BFF负责管理。

    此外,这种架构还能实现清晰的部署分离:前端是从CDN或静态服务器提供的静态资源:

    Frontend
       ↓
    CDN / static hosting
    

    而BFF则作为独立服务运行在微服务之前:

    BFF
       ↓
    Microservices
    

    每一侧都可以独立构建和发布。Next.js 完全可以与 BFF 一起部署。真正的问题在于,你的前端框架是否也必须承担服务器边界的功能。如果组织已经拥有提供该边界功能的后端层,再在前端框架中添加第二个后端层只会重复职责。

    身份验证仍在服务器端进行

    身份验证流程遵循同样的分离原则。用户点击登录后会被重定向到 Okta。成功登录后,回调会发送到后端,后端将访问令牌保存在服务器端,而 Redis 负责会话存储。浏览器仅通过安全的 Cookie 保存会话标识符。从概念上来说:

    Browser
       │
       │ Sign in
       ▼
     Okta
       │
       │ Callback
       ▼
     BFF / Backend
       │
       ▼
     Redis
    

    React 应用并非身份验证架构的所在之处;它的职责是处理产品功能,而 BFF 负责保障安全及维护 API 接口规范。这样做的一个好处是访问令牌永远不会进入浏览器 JavaScript 环境,从而降低了 XSS 漏洞带来的风险。在 Next.js 中实现这一架构并非必然更差,但既然组织内已存在此类架构,将其置于前端之外反而更为简单明了。

    没错,Next.js 完全可以做到这些

    它确实可以。你可以使用 Next.js 及其客户端组件来构建应用,内置 TanStack Query 处理数据请求,实现身份验证功能,与 BFF 相连接,再按需进行部署。能力本身并非问题,更关键的是你实际会使用该框架的多少功能。如果应用程序的大部分最终呈现为这样的结构:

    'use client'
            ↓
    Client-side interactions
            ↓
    API calls
            ↓
    TanStack Query
    

    此时值得停下来思考:服务器端功能是否真的足以让 Next.js 成为整个应用的边界,包括其约定、运行时机制以及升级路径。

    每个组件化技术栈的组成部分都有特定职责

    对于这类应用,合理的默认选择是 React、Vite、TanStack Router 和 TanStack Query,再加上 BFF。这并非要将各种库随意拼接在一起直到无意中构建出自己的框架——每个组件都有明确的角色:

    • Vite 负责提供构建工具和开发服务器。
    • React 负责渲染用户界面。
    • TanStack Router 负责路由处理和 URL 状态管理。
    • TanStack Query 负责服务器端状态管理。
    • BFF 则充当 API 与安全的边界。

    由于各组件的职责十分明确,因此只需更换某个部分而无需重新设计整个应用。

    不使用 Next.js 的权衡

    组合式路由也有其代价。你放弃了“开箱即用”的体验,必须自行决定 Next.js 会帮你看到的那些问题:项目结构、代码分割规则、静态托管服务如何处理客户端路由,以及如何同步升级多个库。需求也可能会发生变化。如果应用的后半部分日后需要对外公开且 SEO 变得重要,Next.js 的优势就会更加明显。同样,如果你需要强大的服务器端渲染或静态生成功能,如果服务端组件能为你的设计带来实际价值,或者如果你确实希望将前端和服务器代码刻意结合在同一个可部署版本中,Next.js 也是更佳选择。因此,问题并非“Next.js 还是 React”,而是哪种架构更适合该应用。

    选择时的检查清单

    在做出决定之前,先思考以下问题:

    • 该应用是否有面向公众的公开界面?这是 Next.js 的一大优势。
    • SEO 是否重要?如果重要,Next.js 会更具吸引力。
    • 是否需要服务器端渲染或静态生成?在这方面 Next.js 也更具优势。
    • 服务器组件或服务器动作能否显著提升应用设计?如果是的话,那就认真考虑 Next.js。
    • 该应用是否主要需要身份验证且交互性很强?建议选择 React 配合 Vite。
    • 组织内部是否已经有了 BFF 或 API 层?可以考虑部署独立的客户端应用。
    • 服务器状态管理是否是核心问题?TanStack Query 会变得非常有用。
    • URL 状态管理和复杂路由是否是重点?TanStack Router 会变得非常有用。

    从这些答案中可以得出一个简明的经验法则:当应用程序有较大的公开接口时,应认真考虑使用 Next.js;而如果主要是需要身份验证的场景,尤其是内部或管理平台,建议从 React 和 Vite 开始,再按需添加其他组件。这两种技术栈本身并无优劣之分,只是更适合不同的问题,一旦某种框架成为默认选择,这种差异就容易被忽视了。

    总结

    最好的架构并非功能最齐全的,而是那些功能能够解决实际问题的架构。对于那种需要登录才能使用、依赖自身后端层、存在大量服务器状态处理、交互频繁且公开内容很少的操作工具,选择 React 配合 Vite、TanStack Router 和 TanStack Query 并非妥协,而是一种经过深思熟虑的决定。

    • 首先对应用进行分类:公共内容网站还是需要身份验证的工具。
    • 将URL和服务器数据视为两个主要的状态存储,并为每个存储提供专用工具。
    • 在您的组织已设定的安全与API边界内进行设计。
    • 如果公共页面、SEO或服务器渲染成为实际需求,需重新评估该决策。