最近,我撰写了一篇博客文章,深入探讨了 next.js 的中间件在应对服务器组件的某些限制方面的作用。这引起了广泛讨论,大家纷纷探讨这种方法是否切实可行,以及 next.js 的开发体验是否真的不尽如人意。
在我看来,next.js 的 app router 存在两大主要问题,导致其难以被广泛应用:
你需要深入了解其内部机制,才能完成看似简单的任务。
其中存在诸多潜在的陷阱,而且这些陷阱默认存在,并非需要用户主动选择才会遇到。
为了更好地理解这些问题,我们可以回顾一下它的前身——pages router。
当我首次接触 next.js 时,它当时的“竞争对手”是 create react app(简称 cra)。当时我所有的项目都是基于 cra 来开发的,但之后我选择转向 next.js,主要有两大原因:
我偏爱基于文件的路由方式,因为它让我能够减少样板代码的编写。
每次启动开发服务器时,cra 都会自动打开 http://localhost:3000 页面(这种做法很快就让我感到不便),而 next.js 则没有这样的“贴心”设计。
第二个原因或许显得有些滑稽默,但对我而言,它确实表明了 next.js:提供了更优秀的 react 默认设置。
这正是我所追求的。直到后来,我才发现 next.js 还有更多功能。api 路由非常吸引我,因为它们无需额外的基础设施配置就能提供无服务器函数,这对于像营销网站的“联系我们”表单这样的功能来说非常便利。getserversideprops
允许我在页面加载前在服务器端运行基础函数。
这些概念不仅功能强大,而且操作起来也十分简单。
api 路由与其他路由处理程序在外观和运作方式上都很相似。如果你曾使用过 express 或 cloudflare workers,那么你只需浏览一下路由处理程序,就能发现其中许多概念都是相通的。至于 getserversideprops
,尽管它有些特别,但一旦你掌握了获取 request
和响应格式的方法,就会发现它也相当容易上手。
next.js 13 版本发布了 app router,带来了众多新功能。其中,server components 的引入使得 react 组件可以在服务器端进行渲染,从而减少了需要发送给客户端的数据量。
此外,新版本还引入了 layouts 功能,允许开发者定义多个路由共享的 ui 元素,并在每次导航时无需重新渲染,从而提高了页面加载效率。
然而,在缓存方面,新版本却变得更加……复杂。
尽管这些新功能十分有趣,但最大的损失在于简单性的减少。
作为开发者,我们都曾有过这样的经历:面对代码难题时,往往会感到困惑并大声问道:“为什么这不起作用?”
这种体验每个人都曾有过,而且总是让人沮丧。对我来说,如果问题并非源于代码本身的 bug,而是源于对事物工作原理的误解,那就会更加令人头疼。
此时,你不再只是疑惑:“为什么这不起作用?”而是开始思考:“为什么它这样工作……而不是那样?”
不幸的是,app router 就充满了这样的微妙之处。
让我们回到我的最初问题:我仅仅希望在服务器组件中获取 url。关于这个主题,github 上有一个非常热门的问题的解答,我将在这里分享部分内容:
当我们深入思考时,问题“为什么我无法访问 pathname 或当前 url?”其实只是冰山一角,其背后隐藏着更大的疑问:“为什么我无法直接访问完整的请求和响应对象?”
next.js 作为一个既能静态也能动态渲染的框架,它巧妙地将工作划分为多个路由段。尽管直接暴露请求 / 响应对象能带来极大的灵活性,但这些对象本质上具有 动态性,它们会影响整个路由的处理。这种设计限制了框架在当前(如缓存和流式传输)以及未来(如部分预渲染)优化方面的能力。
为了解决这一问题,我们曾考虑过直接暴露请求对象并追踪其访问位置(比如使用代理)。但这样的做法会使我们难以追踪这些方法在代码库中的使用方式,并可能导致开发者在不经意间选择了动态渲染。
因此,我们采取了另一种策略,即暴露 web 请求 api 中的特定方法,并针对不同的使用场景进行了统一和优化:这些 api 覆盖了组件、服务器操作、路由处理程序和中间件等场景。通过这些 api,开发者可以明确选择框架的启发式方法,如动态渲染,同时也让 next.js 更容易追踪使用情况,分解工作并尽可能优化性能。
举例来说,当使用 headers 时,框架会选择动态渲染来处理请求。而在处理 cookies 时,你可以在 react 渲染上下文中读取 cookies,但只能在变更上下文中(如服务器操作和路由处理程序)设置 cookies,因为一旦开始流式传输,就无法再设置 cookies 了。
这个回答确实非常出色。它不仅写得清晰易懂,而且帮助我对一些底层问题有了更深入的理解,更让我认识到了不同方法之间的权衡,这些我之前完全没有思考过。
然而,话虽如此,如果你是一名开发人员,只是希望在服务器组件中获取 url,那么在阅读完这篇回答后,你可能还需要进一步查询五个相关问题,最后才会意识到可能需要重新构建或调整你的代码结构。
这篇文章很好地总结了我对此的感受:
这并不意味着它一定是错误的——而是有些出乎意料。
那篇原始文章还提到了一些其他微妙的细节。其中一个常见问题涉及处理 cookies 的方式。你可以在任何地方调用cookies().set("key", "value")
,尽管这能通过类型检查,但在某些情况下,运行时可能会出错。
与“旧”方法相比,那时我们可以轻松获取一个完整的request
对象,并在服务器上随心所欲地操作,现在的复杂性确实有所增加。
我还要指出的是,“默认开启”的激进缓存策略带来了糟糕的体验。我认为,大多数人更希望自主选择是否使用缓存,而不是在大量文档中苦苦寻找如何关闭它。
在 propelauth,我们经常收到的错误报告并非真正的错误,而是用户误以为自己发起了一个 api 调用,但实际上只是读取了缓存的结果。
所有这些都引出了一个问题:这些特性和优化究竟是为了谁而设计的呢?
我所描述的这些过于复杂的特性对一些人来说确实具有重要意义。比如,如果你正在构建一个电子商务平台,这里提供的某些功能就十分出色。
这些功能可以显著提升页面加载速度。因为发送给客户端的数据量减少了,页面加载速度得以加快;由于积极的缓存策略,页面加载速度也得以提升;并且,当用户导航到新页面时,只有页面的部分内容需要重新渲染,这也进一步加快了加载速度。在电子商务领域,页面加载速度的提升意味着更多的收入,因此,为了获得这些优势,你完全会接受使用更为复杂的框架。
然而,如果我是在为我的 saas 应用程序构建仪表板,我可能就不会太关心这些功能了。我更注重的是新功能发布的速度,而所有这些复杂性对我的开发团队来说反而成了负担。
我个人对 app router 的体验和挫折与其他人有所不同,因为我们拥有不同的产品、不同的用例和不同的资源。尤其作为一个长时间投入于编写并帮助他人编写 b2b saas 应用程序的人,我认为使用 app router 的开发体验远不如 pages router。
随着产品 / 框架的不断发展,它们往往会变得更为复杂。客户的需求会不断增加,大客户更是会提出更为具体的要求。由于大客户支付更多的费用,因此你会优先考虑并构建这些更为具体的功能。
然而,那些曾经喜欢一切简单的客户可能会对不断增加的复杂性感到困扰,然后……瞧,一个全新的框架诞生了,它看起来简单多了。这时,人们会开始呼吁:我们都应该转移到那个新框架上去!
要避免这种局面并不容易,但缓解的一个有效方法是,不要强求所有人都去应对只有部分人需要的复杂性。
app router 面临的一个主要问题是:
next.js 在 app router 尚未真正准备好投入生产使用之前就正式推荐了它。next.js 并未就 typescript、eslint 或 tailwind 是否适合你的项目给出明确建议(尽管在 typescript 和 eslint 上默认选择了“是”,tailwind 则选择了“否”—— 抱歉,tailwind 的粉丝们),但 next.js 坚定地认为你应该使用 app router。
然而,react 官方文档却持有不同观点。它们目前推荐的是 pages router,并将 app router 描述为“前沿的 react 框架”。
从这个角度来看待 app router 会更有意义。与其将其视为 react 的推荐默认选项,不如将其视为一个 beta 版本。它的体验相对复杂,一些原本简单的事情现在变得困难 / 不可能,但这正是“前沿”技术所预期的情况。
因此,当你为下一个项目选择框架时,请注意 app router 仍存在许多不足。你可能会更容易找到一个更适合你用例的不同工具。
propelauth 提供端到端的 b2b 产品管理身份验证服务。
https://medium.com/@propelauth/its-not-just-you-next-js-is-getting-harder-to-use-5ab30a24282a
声明:本文为 infoq 翻译整理,未经许可禁止转载。