从 Next.js 到 Rails 再到 Elixir:我的 React.js 倦怠之旅
我从 2019 年开始从事 Web 开发。我使用过 React.js 以及基于 React 的框架,例如 Gatsby、Next、Remix、Astro 和 Hydrogen。我从未对这些工具完全满意,但作为一个深入 JS 生态系统的初学者,我从同行那里听到的评价大多是:“只能这样了,其他编程语言要么慢,要么老旧。”

结果,我习惯了大量的复杂性:多个独立的存储库、数千个用来实现简单事情的库和框架、GraphQL、微服务、无服务器、静态站点生成、增量静态再生、部分水合、redux、redux-thunk、babel、webpack、react 服务器组件、服务器操作等。这个列表还可以再列 10 分钟。
直到有一天我说够了!让我们来看看我慢慢发疯的完整时间线。这可能需要一些时间,读完这篇长文之前,可以先泡杯咖啡!
倦怠的时间表
Gatsby.js
我记得训练营结束后,我心想:“终于可以建立自己的作品集了!” 果然,我做到了。当时只有一个小问题:我想在 Google 上建立索引,但使用老旧的框架create-react-app
几乎无法实现这个目标。很快,我了解了 SEO 和 React 的 Hydration Cycle,这帮助我找到了这个问题的“解决方案”:Gatsby.js。静态网站生成的想法在当时对我来说简直是革命性的,毕竟,没有什么比预渲染的 HTML 文件更快了,不是吗?
我决定通过阅读文档来学习这个新框架,但我得说,这可不是一次愉快的经历。我以前从未听说过 GraphQL,而且显然需要它来生成所有静态文件(这是什么鬼???)。我问过一些网友,学习这些过度设计的垃圾东西是否很困难,他们回答说:“技术问题,再努力点!” 于是我更加努力,最终学会了之后,我将我的个人网站移植到了 Gatsby。
我的大部分页面都成功地被 Google 收录,几个月来,我对结果非常满意。然后另一个问题出现了:我的很多开发者朋友开始说:“Gatsby 死了!Next 是为了简化静态网站生成和提供服务器端渲染而创建的。”
Next.js
我快速浏览了 Next 的文档,立刻就爱上了它。我不用 GraphQL,只用了三分之一的代码就能实现和 Gatsby 一样的功能!我又一次把我的作品集移植到了另一个框架:Next。
这次体验真的很棒。部署到 Vercel 轻而易举,getStaticProps
功能getServerSideProps
简单却极其强大,我可以选择每个页面的渲染样式,总体来说非常灵活。
不幸的是,我通过惨痛的经历学到了一个教训:在 JavaScript 生态系统中,一切美好的事物都会结束。
混音
Remix 发布时的情景我记忆犹新。许多科技大佬(一如既往)开始发布相关内容。然而,当时我在主页上看到它不支持静态网站生成,只支持服务端渲染,于是我心想:“等等,这么多年在JAMstack上的投入都白费了?这框架撑不下去了。” 然而,令我惊讶的是,Remix 不仅活了下来,还被Shopify 收购,成为 Next 的强劲竞争对手。
几个月后,我决定尝试一下。结果再次让我感到惊讶,Remix 的核心理念是使用 Web 基础,而不是像 Next 那样过于复杂的缓存系统。因此,我在 Remix 中编写代码时所需的思维模型要简单 10 倍:没有全局状态管理器,只使用 URL,更少的客户端状态,将所有逻辑都转移到服务器,并使用 Cookie。无需中间的 REST API,全栈开发也非常简单,只需将数据库查询移到loader
函数中即可。
离开矩阵
然后,真相突然出现在我面前,我吞下了红色药丸。我的脑海里开始涌现出许多疑问:Remix 难道不像 Rails、Laravel 和 Django 等其他“老旧乏味”的框架一样吗?几十年来,我们一直在进行服务器端渲染的全栈 Web 开发,但 JavaScript 界的人们集体认定这种方法是垃圾,将所有内容迁移到客户端才是未来。难道是同一个 JavaScript 界的人们一直都认定 Rails 是正确的吗?用 JS 框架来做那些过度设计的怪胎是错误的选择吗?我开始质疑一切。这种“新”的 Web 开发方式要简单得多,也快得多。
我已经完成了 Next 和 Vercel 的工作
我使用Next.js 应用路由器时达到了临界点。以下是 Vercel 向 Next 推送的所有错误的完整列表:
- 曾经简单的“
getStaticProps
和”getServerSideProps
函数,现在变得复杂繁琐。目前,没有专门的地方可以添加 API 调用或数据库查询,你可以在任何地方编写它们!我们再次开始将业务逻辑与 UI 混合,就像多年前在 PHP 上犯过同样的错误一样。前端开发人员难道没有吸取过去的教训吗?如果我删除一个按钮会发生什么?这会破坏我的用户身份验证流程,因为数据库调用就在按钮内部吗?你的前端应该 100% 可丢弃和可替换。你相对于竞争对手的竞争优势在于业务逻辑,它应该与 UI 层完全隔离。
-
Next 现在是服务器优先。这听起来不错吧?毕竟,这解决了 SEO 问题,并立即向用户显示新鲜内容。问题在于,大多数现有的 Next 代码库依赖于客户端库,例如 Styled Components 和一些全局状态管理器。这意味着什么?随着这类重大变更不断发生,您的应用将在几周内(而不是几年)成为遗留软件。您将花费更多时间来更新所有依赖项,而不是专注于真正重要的事情:发布功能。
-
Vercel 从 Meta 挖走了多名 React 核心团队成员。这带来了严重的利益冲突,因为这些工程师现在(据称)正在开发对 Next 有益的功能,而不是优先开发那些能够帮助所有基于 React 的框架(例如 Remix)的功能。
我再也受不了了。我对自己说:你知道吗?我厌倦了一遍又一遍地重新学习同样的框架,而且我完全不认同这种新的范式。
毫不奇怪,其他内容创作者也经历了类似的情况:
通往启蒙之路
简而言之:我太累了。在被各种 React 工具搞得筋疲力尽之后,我开始了寻找更简单的 Web 框架的旅程。以下是我寻找的先决条件:
- 含电池
- 约定优于配置
- 良好的开发者体验
- 现代且高性能的前端
我的第一反应是看看Stack Overflow 2023 年调查中的顶级框架。我立刻就把所有与 JS、C# 和 Java 相关的都从列表中剔除了。我根本没想过学后两个,它们看起来丑陋又冗长。所以剩下的选项是:Laravel(PHP)、Django(Python)、Rails(Ruby)和 Phoenix(Elixir)。
Python 是我在攻读网络工程学位期间使用过的一种语言,使用体验非常愉快。Django 似乎遵循了“约定优于配置”的理念,但最终让我放弃它的原因是它没有一个好的内置前端工具。论坛上大多数人说他们正在使用HTMX和Alpine,然而,这两个都是需要安装的外部依赖项。
放弃 Laravel 非常困难,因为它拥有惊人的成本效益,数百个官方软件包几乎可以处理初创公司可能需要的一切,例如托管、身份验证、Stripe 支付等等。在前端,他们创建了inertia.js,这是一种非常简单优雅的方法,可以在使用 React 的同时,保留 Laravel 的高生产力和强大功能。坦白说,我没有选择 Laravel 的唯一原因是 PHP 的语法,它看起来丑陋不堪,到处都是一堆“和$
” ->
。
Ruby on Rails
Ruby on Rails 无需介绍。它是 Web 开发框架的鼻祖,其革命性的“15 分钟搭建博客”功能至今仍令人印象深刻。在我开始吐槽我发现的所有问题之前,我们先从优点说起。
与 Python 类似,Ruby 是一种可以向非技术人员展示,并让他们理解软件功能语言。它是迄今为止我见过的最易读、最优美的语言。我很快意识到,编写赏心悦目的代码是 Rails 团队的首要任务,这对我来说是前所未有的。
更不用说 Rails 几乎发明了“内置功能”和“约定优于配置”的理念,所以这根本不成问题。只需一个文档,我就能找到任何类型的 Web 应用程序所需的一切。
在前端,有Hotwire,它以一种非常简单轻量的方式实现了 SPA 框架提供的所有用户体验改进。我一直很想测试这个库的极限,它看起来非常有前景。
好吧,理论上 Rails 满足了我想要的框架的所有先决条件。试试吧!我在本地测试的第一件事就是rails scaffold
命令。结果立刻就震惊了。一个命令就能生成我所有需要的 CRUD 功能?不可能!
在 Node + React 的框架下,为了实现同样的目标,我需要手动编写所有代码(这里没有生成器),并安装一堆库,例如:Vite、prisma、express、react router、redux、redux-thunk、vitest、cypress、react 测试库、zod、typescript、eslint、prettier,以及 1000 个不同的插件,甚至可能还需要 GraphQL 或 tRPC。基本上,一个 package.json 文件就已经包含 900 个依赖项了。
在最初的震惊之后rails scaffold
,当我打开控制器的代码时,我再次震惊了:
class ArticlesController < ApplicationController
def index
@articles = Article.all
end
def show
@article = Article.find(params[:id])
end
def new
@article = Article.new
end
def create
@article = Article.new(article_params)
if @article.save
redirect_to @article
else
render :new, status: :unprocessable_entity
end
end
def edit
@article = Article.find(params[:id])
end
def update
@article = Article.find(params[:id])
if @article.update(article_params)
redirect_to @article
else
render :edit, status: :unprocessable_entity
end
end
def destroy
@article = Article.find(params[:id])
@article.destroy
redirect_to root_path, status: :see_other
end
private
def article_params
params.require(:article).permit(:title, :body)
end
end
这就是全部后端代码吗?就几行?不可能!这简直太简单了,看起来像是一个“低代码”工具。它简洁、优雅,而且可读性极高,这在 JS 领域可谓凤毛麟角。
好吧,好吧,你现在肯定在想:“网上那个疯狂的 React 开发者说他最终用的是 Elixir,所以 Ruby 肯定有问题!” 你说得对,我的匿名朋友,有些事情确实让我很恼火,我们来聊聊吧。
首先,我们需要解决一个显而易见的问题:从 React + Typescript 迁移到动态类型语言并不容易。从我开始写代码的那一刻起,我的 VScode 上就没有任何智能提示或代码建议的下拉菜单,我感觉自己迷失了方向。这种感觉糟透了,我可能会在函数名上打错,直到网站上线才意识到!我知道我们可以写测试,但这种错误我希望在 IDE 上就能立即识别,而不是在测试或部署过程中。
另一件我本以为会喜欢,但最终却讨厌的事情是:太多魔法了。在 Typescript 代码库中,我可以点击任何类或函数,跳转到源代码查看它的实现方式。但在 Rails 上,我到底该在哪里做验证(比如)?我应该在控制器内部创建一个私有函数吗?有没有专门的文件夹来做这件事?不,正确的位置是在模型内部。为什么?因为它就是这样运作的,你要么接受惯例,要么写 Ruby 代码就很困难。我根本无法对底层的运作方式形成一种“直觉”,只能盲目地相信维护人员在组织所有事情方面做得很好。
为了彻底解决我的烦恼,我开始写前端代码。我该如何创建组件?部分组件。我该如何定义这个组件的 prop 类型?根本没办法,你得把它打开,然后直观地查看里面的所有变量。那做点互动怎么样?创建状态怎么样?嗯,有 Hotwire 和Stimulus,但正如你所见,你需要手动创建“重新渲染”函数,它不像 React 那样能在状态改变后自动重新渲染页面。
// src/controllers/slideshow_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = [ "slide" ]
initialize() {
this.index = 0
this.showCurrentSlide()
}
next() {
this.index++
this.showCurrentSlide()
}
previous() {
this.index--
this.showCurrentSlide()
}
showCurrentSlide() {
this.slideTargets.forEach((element, index) => {
element.hidden = index !== this.index
})
}
}
我又一次感到沮丧。我差点就找到完美的框架了!如果 Rails 失败了,我下一个想尝试的框架是什么?Elixir。
灵药和凤凰
说实话,我的耐心已经快耗尽了。我尝试了多个不同的生态系统,几乎要放弃追求完美的念头,只用 Ruby on Rails 了。直到我的 YouTube 推荐区出现了一段视频:
等等!这里我们看到一位 React 开发者对函数式编程、Elixir 和 Phoenix Live View 赞不绝口。或许我应该试试!
我做的第一件事就是打开 Elixir 和 Phoenix 的文档,我非常喜欢所有包都使用Hex Docs以相同的方式记录的事实,你只需要习惯一个界面就可以学习新的东西。
另一个好处是,你只需阅读文档就能真正学会 Elixir,无需昂贵的课程!在其他生态系统中,我都必须先通过付费课程学习语言,然后再通过阅读文档来学习框架。
接下来就是开始写代码了。我很快就明白了函数式编程和面向对象编程(OOP)的区别。我们来做个简单的比较:
// JS
const obj = {name: "daniel"}
obj.age = 25
// result: obj = {name: "daniel", age: 25}
# Elixir
obj = %{name: "daniel"}
obj = Map.put(obj, :age, 25)
# result: obj = %{name: "daniel", age: 25}
或者您可以使用管道运算符以更简单的语法实现相同的目的:
# Elixir with pipe operator
obj = %{name: "daniel"} |> Map.put(:age, 25)
# result: obj = %{name: "daniel", age: 25}
一开始你可能会觉得它可读性差,而且更复杂,但我保证随着时间的推移,它会变得有意义!嗯,至少对我来说是这样。作为一名 React 开发者,我已经习惯了到处都能看到多个函数,甚至前端组件也是函数!更不用说创建类有时会被 JavaScript 黑帮视为代码异味。我的大脑已经“成型”了这种新范式,它对我来说感觉很自然。自从我在大学获得网络工程学位以来,我上过几门关于面向对象编程的课程,但一直没能“理解”。我无法将复杂的问题建模成类和对象。使用多个函数来随时间“改变”一个变量,是我在脑海中建模事物的方法。
主框架怎么样?Phoenix 包含所有功能吗?约定优于配置?没错!说实话,它的生态系统不如 Rails 的水平,但 95% 的功能已经达到这个水平了。除非你需要某个极其特殊的功能,否则 Phoenix 完全可以满足你的需求。
我几乎被 Elixir 说服了,但我的清单上缺少两件事:良好的开发人员体验和现代/高性能的前端代码。
José Valim 宣布他正在尝试为 Elixir 添加类型,但 Elixir 目前不支持,所以我很担心。如何在没有类型的情况下获得智能感知和自动完成功能?很快我发现这些功能并不一定相关。在 VScode 上安装ElixirLS 扩展后,我大吃一惊。可以在任意文件夹的任意模块中定义一个函数,将其导入到其他地方,并获得该函数的智能感知和文档!我享受到了静态类型语言的这些好处,而且无需编写类型,真是太棒了!
Phoenix Live View解决了我对前端的最后一个担忧。在代码方面,正是文档主页上的这段话让我信服:
你可以为每个组件定义“props”,如果类型不匹配,IDE 就会报错,就像 React 一样!太棒了!
用户体验如何?用户点击链接时,页面会完全加载吗?当然不会!实时视图会与客户端建立 WebSocket 连接,之后每次页面切换都只是通过 WebSocket 进行内容交换,无需发起新的 HTTP 请求。此外,所有状态均在服务器端管理,这意味着像 Trello 这样丰富的用户体验,过去由于加载过多 JavaScript 代码,在客户端运行非常卡顿,而现在速度非常快!Elixir 会处理所有复杂的状态逻辑,并将页面的更新部分发送到前端。完整解释请见此处:
由于我们使用 WebSockets 来构建 UI,因此创建像 Twitter 这样的“实时”应用程序只需要几行代码!
结论
可以肯定地说,“完美的技术栈”并不存在。解决所有问题的灵丹妙药只是我们为了不断寻找和构建最优化的工具而在脑海中创造的幻想。
然而,就个人而言,完美的技术栈确实存在。因为每个开发者都有自己的偏好,你可以轻松找到符合你标准的工具。如果你的经历和我类似,那么 Elixir 和 Phoenix 或许就是完美的选择!所以,不妨一试,也许你会像我现在一样爱上它。
如果你读完了这篇博文,那你太棒了!非常感谢你抽出时间阅读,希望我能为你的职业生涯带来一些价值。
