我自己的博客从最开始选择 Next.js 一路走过来已经 2 年半了,中间也经历过一次大规模重写。当初只觉得是一个脚手架的选择,如今终于变成了那个要面对的技术债。

从 CRA 到 Next.js
在接触 Next.js 以前,我一直在用 CRA 来创建项目。相比需要手动维护的复杂配置,我更喜欢面向约定的开发方式。CRA 在当时解决了很多繁琐的工程问题:TypeScript 支持、CSS、Service Worker、测试和构建等等。虽然这些都可以通过 fork 一个 template 项目来解决,但我还是想用最简单方式完成相同的事情。
在我看来,约定胜于配置,我想我应该永远都不会去修改 CRA 里托管的配置。
后来在 Next.js 推出的很长一段时间里,我都没有怎么接触前端开发,直到我想重新开始写博客。既然 CRA 现在 deprecated 了,官方也推荐使用 Next.js,我也很自然地就选择它作为新的脚手架了。简单研究了一番,发现它不仅包含 CRA 的所有能力,还内置了更多方便我快速“把东西糊出来”的东西。用一句时髦的话说就是 batteries included,我几乎不用引入其他的东西,就可以开发一个简单的多页面 app 了。
与此同时,Vercel 提供的托管服务在项目前期的 DX 方面做得非常出色,开发部署过程很丝滑。很快,博客的第一版就上线了。
没有银弹
在我看来,约定胜于配置,我想我应该永远都不会去修改 Next.js 里的底层配置。
但这次,我错了。
虽然在文章格式上我选择了 MDX,但我不希望每一篇文章都是以代码的方式加入到路由中。为了能够静态生成目录和路由,我写了一个简单的 Webpack loader,这样我就能在编译时 SSG 所有的文章,而不用通过 function 动态渲染。
起初,这个方案工作得非常好。不过我还没有意识到 Next.js 其实不只是一个框架,它更像是一个绑架。这个使用 loader 来枚举所有文章的方案绑定了 Webpack 这一构建环境,而 Next.js 并没有承诺会一直使用 Webpack 作为构建工具。它对技术选型有近乎独裁的控制,在 v16 中,Turbopack 变成了默认的打包器,且许多特性与之绑定。虽然现在还可以手动 opt-out 回 Webpack,但我们不知道 Vercel 何时会彻底移除对 Webpack 的支持。
对于我的博客来说,迁移到 Turbopack 其实并不是什么难事,完全交给 AI 来做也是从从容容,游刃有余。但对于公司级的项目来说,Next.js 无疑是一个定时炸弹,除非我们完全锁定版本(包括 React 的版本),否则技术选型就可能要被 Vercel 牵着鼻子走。
React 和 Next.js 都是巨大的依赖风险,前者近乎垄断了前端开发的生态,而后者如今还有逃离的机会。
又一次重写
我没有选择迁移到 Next.js v16,而是决定完全抛弃它。这意味着大部分代码都需要重写,严格意义上说是手动补上 Next.js 里我使用到的能力(例如路由)。
重新审视我的博客,它其实非常简单,我没有在里面加入很多互动性的元素。基本上来说,它就是一个纯粹的静态站,我想即便是 Vanilla JS 也已经足够了。但为了日后可能的扩展性,我还是保留了 React 作为基础的“开发模式”。这样一来,很多组件代码还可以复用。
我重新创建了项目,选择 Vite 作为开发和构建工具,它也提供了非常好的 DX,并且近乎 zero config。路由上我选择了 React Router,目前来说也是完全够用。当然,文章依然以资源的形式存在,通过 Vite plugin 导入到 app 代码中,并用于生成路由配置。
SSG 也是重写中很重要的一环,由于没有了框架加持,所有的渲染逻辑都需要手动实现。我选择了遍历路由配置,通过 react-dom/static 来渲染静态的 HTML,并按路由结构存储到文件中。为了优化包体积,我大量使用了 lazy 和 import() 来做 code splitting。得益于 Suspense 这个一等公民特性,它和 prerenderToNodeStream 配合得非常好,我们可以很轻松地实现这种依赖异步加载的静态渲染。
部署方面我还是保留了 Vercel 的选择,因为没有使用到 middleware,其实我对它的依赖也不会太重。即使以后要迁移到其他平台,也只是重新适配一下构建脚本,输出其他的路由配置文件而已。不得不说,Vercel 的 Build Output API 还是挺方便的。
写在最后
这次重写看起来像是原地打转,把一样的功能用不一样的方法又做了一遍。但对我来说感觉如释重负,再也不需要被 Next.js 这个庞然大物束缚。我可以更自由地做各种技术选型,不用按照它的方式写代码,重新掌握对自己的代码控制权。不过我没有在工作中维护过大型前端项目,因此这里一定也有很多可以改进的空间。
我始终认为,只有当你了解了事物背后的细枝末节,你才算真正地学会了它。也许以后我会试着自己实现路由、打包器,甚至 React 这种基础库。我想这个过程本身,可能也是一种乐趣。