首页 / 文章

无需元框架进行开发:Python后端的隐性成本

为什么在非 JavaScript 后端使用 React 加 Vite 可以避开 Next.js 的争论,哪些框架功能需要重新实现,以及如何处理接口衔接问题。

发布者:REACTAPP.TOP阅读约 7 分钟1689 词

来源: Everyone’s Leaving Next.js. I Never Showed Up.

关于 React 框架的多数讨论都只考虑两种选择:继续使用 Next.js,或者转而采用目前被宣传为其继任者的框架,无论是 Astro、Remix 还是 TanStack Start。但实际上,很大一部分 React 应用并未选择这两条路径。它们使用 Vite 渲染单页客户端,并与用 Python、Ruby 或 PHP 编写的后端进行交互,而这些后端已经负责管理数据库、业务规则以及 API。本文将诚实地探讨第三种选择:哪些针对元框架的抱怨其实并不适用,哪些职责会悄然转移到你的代码中,以及如何让由此形成的自研框架保持可维护性。

第三种选择的具体示例

想象一个能够追踪食品强化情况的公共卫生系统。其前端采用 React 和 TypeScript,组件库为 Ant Design;后端则使用 Frappe,也就是 ERPNext 所依赖的 Python 框架。Frappe 通过“doctype”定义数据模型,管理数据库,并将经过白名单筛选的 Python 方法作为 API 接口提供。在生产环境中没有 Node.js 运行时,也没有应用路由器或负责决定界面哪些部分可以访问数据库的服务器组件。屏幕渲染由 React 负责,其余所有工作则由 Frappe 完成。

对于处于这种技术架构下的团队而言,许多针对 Next.js 的激烈批评其实并不相关。既然没有代码部署在 Vercel 上,那么对平台的绑定问题也就无关紧要了。由于开发服务器由 Vite 运行且页面刷新速度极快,几乎无需考虑应用路由器下热模块替换缓慢的问题。

无论将 React 与 Django、Rails、Laravel,还是任何其他已有自身服务器端架构的技术栈结合使用,都适用相同的架构。不过,关于元框架的那些抱怨依然存在,只是来源不同而已。

你仍然需要逐个工具来构建框架

跳过元框架并不会消除它原本要解决的问题,只是把这些问题转移到了你的代码库中,由你自己逐步解决,而且往往不知道自己正在构建什么。fortification 应用中的三个例子展示了这种模式。

采用基于模式的表单而非框架约定

该团队开发了一个DynamicForm组件,能够直接根据后端提供的doctype JSON定义来渲染表单。它读取架构信息,确定存在的字段及其类型,标记必填字段,处理那些可见性或值取决于其他字段的字段,最终渲染出完整的表单。这样一来,新增doctype时就无需再手动编写表单了。

这正是“约定优于配置”的体现,也是每个元框架的核心承诺,而此处则是通过内部实现来达成的。这一方案并非一次就能成功:该组件经历了三次彻底的重写才变得稳定。如果是框架的话,就能在成千上万的用户中使用中自动完成这些迭代。

无需数据层即可实现分页加载

有一份报告指出,需要随着用户滚动页面而分批加载数据行。由于没有getServerSideProps,也没有路由加载器或其他框架提供的数据获取功能,解决方案只能通过多种组件组合实现:一个用于监控列表底部标记元素的IntersectionObserver、手动追踪的页面光标,以及需要从头设计的加载状态机制。是应在底部显示旋转图标,还是让新数据行直接淡入?在缺乏框架指导的情况下,只能由一名开发人员在日常工作中自行做出这些产品决策。

在 React-plus-Vite 的架构中,像 TanStack Query 这样的库可以提供大部分功能(无限查询、游标、加载状态指示等),在自行编写逻辑之前值得先对其进行评估。无论怎样,核心问题始终存在:必须有人来选择并负责数据层。

针对不统一后端数据的适配层

大约有十几种数据类型的下拉菜单都需要显示可读的标签,但每种数据类型对标签字段的命名各不相同。有些使用 label,有些使用 title,还有一种旧版数据类型使用 display_name。通过一个简单的 mapOptions 工具,可以将所有这些命名统一为标准格式,从而使每个下拉菜单组件都能忽略这种不一致性。

没人会将其称为架构,但它恰恰就是框架规范通常会强制实现的那种标准化。每个团队最终都会编写出元框架本应提供的基础结构。唯一的区别在于,你是选择接受框架的设定来为此付出代价,还是用自己的工程时间来解决。

当自研框架变成问题时

真正的成本会在之后显现。几年后,SonarQube指出DynamicForm组件的认知复杂度过高。这并非库的漏洞或版本不兼容问题,而是组件正常发展的结果:一个文件不知不觉就同时承担了应用程序的路由层、表单层和验证层的功能。

代码重构暴露出其结构已变得极为复杂。将验证逻辑提取到单独的函数中后,发现了四条相互独立的代码路径,它们都依赖于组件内部隐藏的副作用。这就是当你跳过元框架时几乎不会在账单上看到的“代价”——你需要自己编写基础架构,同时也要成为该架构的唯一文档。没有哪个流行的问题追踪工具能让你看到其他人遇到过同样的问题并发布了解决方案。

解决方案采用了常见的分解方法:

  • 将字段依赖关系的处理移至专用的自定义钩子中。
  • 把验证逻辑放在一个独立的模块中,该模块无需了解表单的渲染方式。
  • 让组件本身仅负责渲染功能。

拆分之后,代码复杂度下降,组件也变得更易于理解。更重要的启示是:元框架通过约定强制实现的关注点分离,在自定义技术栈中则必须每次都通过严格的规范来保证。没有工具能提醒你正在错误地重构框架的数据层。在此情况下,有益的习惯包括定期生成复杂度报告、在代码审查时询问“这个组件是否正在变成基础设施?”,以及为那些被多个界面使用的工具编写简短的内部文档。

存在于两个生态系统交界处的漏洞

像 Next.js 这样的框架同时拥有服务器端和客户端功能,因此两者之间的边界被设计为一个整体系统。而当 React 与 Python 后端结合使用时,这个边界就需要由你自己来定义,这就会产生一类特殊的漏洞。

一个例子是MySQL的错误1292,花了将近一天的时间才找到原因。该错误表明日期或时间值无效或格式不正确。React表单以某种格式发送日期,而Frappe的ORM期望另一种格式。从各自的角度来看,前端和后端都没有问题;故障源于两个从未一起设计过的系统之间的格式转换问题,它们只是依靠数月前制定、如今已被部分遗忘的惯例勉强连接在一起。

另一个例子是,在后端的after_insert钩子中意外触发了密码强度验证器,原因是表单发送的数据格式无人测试过。没有任何框架的更新日志会记载这种交互情况,因为没有元框架知道Frappe的存在。调试意味着需要同时查看自己的代码和后端框架的源码,直到两方面的描述一致为止。

一些针对接缝漏洞的实际防御措施:

  • 在客户端仅在一个位置按照后端规定的格式对日期和时间进行序列化,而非针对每个表单分别处理。
  • 将请求载荷视为一份契约:用 TypeScript 定义其结构,并在可能的情况下在双方边界处对其进行验证。
  • 编写集成测试,向真实的后端发送真实的表单载荷,包括那些结构特殊的载荷,因为服务器端的处理机制会对意想不到的数据作出反应。
  • 在后端验证失败时记录原始载荷,以便立即发现不一致之处。

如果你的后端也是 JavaScript,共享的验证方案可以大大减少这类矛盾;详见在 React 前端和 Node 后端之间共享同一个 Zod 验证方案。使用 Python 后端时虽然无法共享代码,但仍然可以共享验证规范。

正确制定决策

关于“选择哪种元框架”的争论往往假设后端是可以协商的。但对许多团队而言并非如此。如果现有的 Django、Rails、Laravel 或 Frappe 应用已经占据了服务器,那么 React 加上 Vite 并非折中方案,而是最自然的架构选择。真正的问题在于,在已有后端的基础上,你愿意自己承担并维护多少框架相关的功能。

作为交换,你可以选择适合该领域的后端技术。对于基于 doctypes 和 Python hooks 构建的系统而言,这种自由度极具价值。但代价则是必须承担这一选择所带来的所有问题。

关键要点

  • 关于 Vercel 依赖问题以及应用路由器迁移的担忧确实存在,但这些并不适用于由非 JavaScript 后端提供的 React 客户端。
  • 即便没有元框架,你仍然需要构建表单、数据加载及规范化层;只不过这些需要由你自己开发,并永久拥有其控制权。
  • 自行开发的基础设施容易变得结构混乱;应刻意保持各功能模块的独立性,并对共享工具进行文档记录。
  • 客户端与服务器之间的边界由你负责管理:需集中处理序列化工作,明确数据格式规范,并针对真实后端进行测试。
  • 两种路径并无简单之分。它们都是在框架理念与开发效率之间做权衡,而何种选择取决于你实际要采用的后端技术。