Next.js | 基于优秀开发人员体验的整洁架构
9 月末,Next.js 12.3 发布。本文不是要介绍其中的酷炫新特性,而更多的是关于用 Next.js 来构建企业级 JavaScript 应用程序时不时冒出的一些奇异观点。
技术有两个方面。一方面,只要你能构建真正解决用户问题且价格合理的解决方案,技术就无关紧要。例如,Python 和 Ruby 之间的选择,以及用 Java 或 Kotlin 来构建你的 Web 应用程序。我们称之为您或您的团队选择生态系统的偏好。
然而,另一方面更有趣——为正确的工作选择正确的工具。这就是会导致困惑的地方。没有人是无所不知的。即使我们试图进行客观分析时,也是基于主观经验的。这正是要讨论 Next.js 的地方。围绕 Next.js 的好处的宣传模糊了现实情况。
如果你不是在构建面向 SEO 的网站,你真的不需要 Next.js
毫无疑问,Next.js 是一个用来构建基于 React 的应用程序的非常棒的框架(控制反转)。而且,它也完全称得上是最好的框架,因为当什么都没有时,它是第一个针对 React 的真正框架。在此之前存在的替代方案——create-react-app
只是一个简单的脚手架工具附带一个 CLI。2022 年,我们有很多选择,例如 Fusion.js、Electrode、Remix 等等。
有很多非常棒的文档介绍了 Next.js 提供的最佳内容。但它们都没有回答这个基本问题——Next.js 是适合你的工作的正确工具吗?但在回答这个问题之前,我们先来谈一谈 Next.js 的缺点。
DX——开发人员体验是一回事,而 AX——架构体验是另一回事。Next.js 在前者很棒,但在后者很糟糕。关于软件架构,有一些古老的黄金法则根本不适合 Next 生态系统。
由于 SSR 功能,Next 的设计选择非常特立独行。这在 Next 为应用程序强加的架构类型中是显而易见的。例如:
基于文件的路由减少了应用程序的可组合性。例如,我不能将几个页面作为单独的 NPM 包由不同的团队开发。可组合性是大型应用程序必须具备的。
在 Next 中虽然也有一些方法可以进行组合,但是没有比较整洁的方法。在构建 Web 应用程序时,路由本质上应该是声明式的,这是一个由来已久的教训。
Next 12.2 大大削减了 middleware(中间件)的功能,来使其适配 Vercel 的 serverless/边缘计算模型。你不能再从middleware返回响应体。这对于框架来说,不是一个好的特性。框架是一种抽象,它应该隐藏复杂性,而不是完全扼杀一个特性。
Next API 存在的唯一原因是用来编写 BFF——Backend for Fronted,因为你想要处理服务器端秘钥。好的 API 服务器允许我们在应用程序上下文(application context)或请求上下文(request context)中维护状态。Next 中唯一维护状态的方法是有状态的 ESM 模块,我们知道真正的单例很难维护,特别是当我们有bundlers位于两者之间时。而且,模拟静态 ESM 导入并不容易。上下文对象是必要的,因为它们通过支持依赖注入模式和提供各种应用程序状态对象的延迟或紧急初始化来简化测试。
在我个人看来,Next 是一个被设计成专门用于边缘平台或 serverless 平台的框架。
广义来说,从前端的角度来看,有 4 类应用程序:
1.单体应用程序
2.迷你应用程序
3.微型应用程序
4.基于插件的应用程序(微前端)
这些类别因应用程序的规模、生命周期和运行时上下文而异;这个话题值得另外再写一篇文章。Next 被设计用来构建单体前端应用程序。在许多组织中,将一个大型应用程序拆分为多个比较小的应用程序是解决扩展性和复杂性问题的最简单的方法。这可以通过一个简单的 Node.js 服务器服务多个前端应用程序来实现。
如果我们决定采用 Next.js 技术路线,最终的结果是我们会创建一个完全单体的应用程序。之后没有简单的迁移路径。每个应用程序都部署在一个子域名或完全不同的域名。所有其他问题,例如访问控制、session 管理、跟踪等等都会变得复杂起来。
要让所有应用程序通过不同的路径在同一个域名运行,在 Next 中,唯一直接可用的方法是使用Zones。Zone 不是一个 API,而是一个仅在 Vercel 上支持的部署概念。如果您没有使用 Vercel,那么您必须构建自己的系统来实现反向代理或类似网关来映射传入的 URL。
除非我们牺牲 Next 的许多现成功能并添加一些神奇的构造,否则我们永远无法将它打包成一个库,毕竟,它是一个框架和构建工具的集合体。
例如,我需要构建一个通用堆栈来构建多个基于 Next 的应用程序,这些应用程序运行在不同的子域名,但具有通用的日志、身份验证、安全等。这些关注点应该对应用程序开发人员隐藏起来,但 Next 因为它的工作方式而强制要求这些关注点是可见的。
这在 StackOverflow 上仍是一个开放的问题:
我们实现这点的唯一方法是使用一个自定义的使用Koa.js的 Node.js 服务器,Koa.js 会负责处理这些事情并能够将它作为库发布:
即使我们可以将它作为一个库提供,模本文件的数量也会大而繁多。另一种替代方法是 CLI 或一些模板解决方案,由于需要在所有应用程序中保持更新,因此这些解决方案并不实用。
从构建到部署,Next 应用程序在前端活动之间紧密耦合。应用程序如何在服务器和客户端运行也存在耦合。这意味着我们将自己锁在这个框架中。几乎不可能部分地对系统进行组装/升级/实验。如果应用程序的生命周期比较长,那么这是一种不可接受的架构选择。
具体来说,每个 Next 应用程序有三个控制周期:
1.服务器周期
2.客户端周期
3.构建周期
这三个周期是紧密耦合的。从那时起,这种反模式突然变得可以接受了!虽然它确实改善了构建 Web 应用程序的开发体验,但长远来看,显式解耦的方案更值得考虑。
如果可能,软件应该是组合的,而不是构建的,只需要添加代码将现有部分粘合起来。这种组合应该受我们的控制。而 Next 使我们无法控制。就像我们不买零件不能更换的汽车一样,一种技术如果不能更换部分组件就不应该被采用。
为了突出组合性,我们可以将 Next.js 当作三个东西:框架、CLI 和编译器。
Webpack 是 Next.js 使用的编译器。问题是,不用 Webpack 就无法使用 Next。一个长寿的软件需要好的架构,这个架构应该使我们能够部分地或整体地升级。能够独立工作的抽象是必须的。
整洁的应用程序架构应该始终遵循某种形式的良好定义的模式。它可以是传统的分层架构(应用程序的第 n 层应该只与 n+1 层对话,并被 n-1 层调用),也可以是六角形/整洁/洋葱架构(反向依赖关系)。而且它更适合用于编写 API 驱动的服务。
可能有争论说,Next 只是一个前端框架而不是一个端到端的全栈框架。但 Next 并不是这样宣传的。下面是 Next 官网的功能清单:
Next.js 功能集
这在 Reddit、Twitter、Hacker News 等社交媒体上得到了进一步的推波助澜,像这样的争论帖子还有很多。
链接:https://www.reddit.com/r/nextjs/comments/esj0l8/is_it_recommended_to_use_nextjs_for_a_fullstack/
可以肯定的是,Next 并没遵循这条设计原则。
使用 Next 构建应用程序时,很难遵循12因素方法。
Next 支持 4 种类型的渲染:
1.服务器端渲染(SSR)
2.静态站点生成(SSG)
3.客户端渲染(CSR)
4.增量静态生成(ISG)
不要介意 CSR 不是默认的或被视为一等公民!为了通用,Next 中有些东西分得并不那么明显。处理环境就是一个典型的例子。根据 Next 文档:
为了确保服务器端密钥安全,Next.js 在构建时会用正确的值取代 process.env.*。
实际上,环境变量是构建时环境变量,而运行时配置是大多数应用程序在使用 SSR 或 CSR 时需要的配置。而且,消息传递也是错误的:
Next.js 优先事项
问题不在于是否支持某些东西,而在于架构体验是否合理!目前看来,并非如此!
理想的部署模型确保了正确的软件配置管理,并且默认构建一次可以随处部署。
同构问题:一个 App 顶两个
我并不热衷于状态同构。这简直是假的。我们还没有做到状态同构。它太复杂了,而且很难解释清楚:
正如Miško Hevery所说,这种一种棘手的变通方法。我不会详细解释为什么同构好或者不好!这是一个单独的话题。我所关心的是架构层面:
应用程序状态同构通常是一种特性。应用程序不应该依赖于特定的逻辑。很少有应用程序需要这种特性。而且,您构建这些应用程序的可能性要小得多。
使用 SSR 并非易事。你要使用的任何库都要和 SSR 兼容,或者你必须确保那些代码只在浏览器端运行。你不能为你的库简单地使用一个闭包状态(不建议库有状态)。当你有多个团队单独工作时,这个问题会迅速升级。
在代码级别,某些事情是不可能的。例如,我不能使用某些动态条件处理静态文件。我可以使用 API 路由,但这与其说是一种解决方案,不如说是一种折中方案。
链接:https://stackoverflow.com/questions/71618052/how-to-serve-static-file-dynamically-using-next-js
再次说明,本文不是为了抱怨 Next,而是为了确定正确的用例。开箱即用,Next 使得许多事情变得简单:
1.配置得相当好的 Webpack 编译器
2.优化——静态 HTML 页面、图像优化、polyfills 等。
3.默认路由
4.预配置的服务器——压缩、缓存等。
5.完备的渲染策略
但是大多数企业应用程序(如下所述)都有一定程度的定制。当我们在使用 Next 时进行这些更改或者脱离它所提供的功能时,我们就会遇到问题。
与其为边缘案例寻找解决方案,不如编写我们自己的网站配置,启动服务器,构建一个 React 应用程序。
尽管存在一些架构问题,但 Next.js 还有许多适用的用例。
比如,网站和 Web 应用程序。网站通常是静态的,内容丰富。具有 SSG 和 ISR 渲染的 Next.js 绝对是一个理想的选择。它比基于 Gatsby 的网站简单地多,并且与其它静态网站构建器相比,它通过 React 提供了更好的组件模型。
Next.js 擅长构建 SEO/Open Graph 兼容的网站和应用程序。另外,对于 Amazon、Flipkart 等内容获取时间非常关键的网站来说,Next.js 也是一个很好的选择。
至于 Web 应用程序,面向 SEO 的应用程序(例如电子商务应用程序)和社交软件应用程序(例如 Twitter)都非常值得用 Next.js 或基于 SSR 的解决方案来构建。
我们普通开发者开发的绝大多数应用程序都不是 Twitter 或亚马逊。我们构建的都是通过 B2B 或 B2C 渠道向小微企业销售的企业软件。这样的 SaaS 软件通常需要身份验证,并不怎么需要 SEO。我称之为企业软件。
我们的日常任务管理器、项目管理工具、笔记应用程序、电子表格、记账工具、人力资源工具都是这类软件。
尽管客户端渲染存在首屏渲染的性能问题,但它仍然是解耦的和比较简单的架构模型。服务器端和客户端之间的状态是无联系的。通过配合使用 Service Workers、离线体验、CDN、简单的身份验证服务器和 HTTP2,客户端渲染能够提供惊人的性能。
客户端渲染仍然很棒。不要被你看到的东西愚弄了!
作为一个企业应用程序框架,Next 不是 Django 或者 Rails。你要做的就是更好地思考决策:
一个用来选择适当的前端渲染方案的简单决策树
Next 团队一直主张将其作为一个全栈框架,但并未真正提供良好的上下文,例如:
Next.js 作为一个全栈框架
很难说 Next 的发展方向,但它在预期用例之外的更多使用可能会导致它以自己独特的方式支持所需的模式,从而朝着更加特立独行的方向发展。
毫无疑问,使用 Next 将使事情变得复杂。新的 Layout RFC 相当复杂:
而且,核心团队的回答并不完全令人信服,至少对我来说是这样!
在我个人看来,Next 应该进一步研究可组合性,而不是试图成为 Node.js 世界的 Django。最大的工程价值在于使 SSR 渲染作为一个简单的库而不是一个成熟的框架。用传奇人物乔·阿姆斯特朗(Joe Armstrong)的话来说,Next 感觉就像一个“香蕉”。
你想要一根香蕉,但你得到的确是一个拿着香蕉的大猩猩和整个丛林 — Joe Armstrong (Erlang)
50 年以前,一个无法定义良好应用程序边界的框架应该是一个糟糕的设计!今天依然如此。Next.js 只是放大了糟糕的方面。也许,Next.js 的创造者并没有打算让用户在其适用范围之外使用它。但很难控制其适用性,主要是因为不了解领域或确切的业务需求。
简单性不是敌人。它是你能拥有的最好的朋友。如果你的应用程序需要先登录再访问,而且对于边缘渲染、极致的 Web 性能敏感性没有要求,那么客户端渲染或者使用渐进式 JavaScript 的老派服务端渲染网站可能是更好的选择。
尽管我们是在 Next.js 的上下文中讨论服务器端渲染,但大多数争论也适用于提供这种通用渲染的其它框架。
再次提醒你,Next 是一个令人惊叹的框架,但你应该使用的用例有限。正如埃里克·雷蒙德(Eric Raymond)所言,Next 正在经历面向对象编程(OOP)繁荣:
由于其简单性,本文针对非公开面向 SEO 的应用程序介绍了企业软件并推荐客户端渲染(CSR)。但正确使用客户端渲染是一门艺术。我们将在后续的文章中讨论这个话题。
感谢Charushila Patil提供的精彩插图和无数分享 Next.js 使用经验的人们。
作者介绍
专注于用户界面、狂热功能、写作并沉迷于可读代码,热爱机器学习和 LISP.....但每天都在写 JavaScript。
原文链接
领取专属 10元无门槛券
私享最新 技术干货