狠狠吐槽一下SSR/Next.js(二):服务端和客户端代码缠在一起,剪不断,理还乱。

狠狠吐槽一下SSR/Next.js(二):服务端和客户端代码缠在一起,剪不断,理还乱。

上回我吐槽了Next.js和SSR的“花活”,很多人说我是“因爱生恨”。但今天,我要聊一个真正让我夜不能寐、每天写代码都想砸键盘的核心痛点——

代码混杂。

什么是代码混杂?就是在一个.tsx文件里,服务端的逻辑和客户端的逻辑像两团被猫玩过的毛线,缠在一起,剪不断,理还乱。

Next.js 美其名曰“全栈框架”、“前后端融合”,在我看来,这根本就是一场心智负担的灾难

一、同一个文件,两个世界

让我们来看一个“经典”的Next.js页面:

tsx

// pages/profile/[id].tsx // 这行代码是在哪里执行??? export async function getServerSideProps(context) { const userId = context.params.id; // 这是服务器端的秘密!里面有数据库密码! const dbSecret = process.env.DATABASE_PASSWORD; // 去数据库捞数据... const userData = await db.query('SELECT * FROM users WHERE id = ?', [userId]); return { props: { user: userData } }; } // 这个组件又是在哪里执行??? export default function UserProfile({ user }) { // 这看起来是客户端代码... const [isFollowed, setIsFollowed] = useState(false); // 但是!这个 useEffect 在服务端也跑了一次! useEffect(() => { // 只能客户端做的事 console.log('窗口宽度:', window.innerWidth); // 服务端直接报错! }, []); const handleFollow = async () => { // 这又是在哪里?客户端,因为有点击事件 const res = await fetch('/api/follow', { method: 'POST' }); // ... }; return ( <div> <h1>{user.name}</h1> <button onClick={handleFollow}> {isFollowed ? '已关注' : '关注'} </button> </div> ); }

看到没有?一个文件里,服务器端的逻辑(getServerSideProps)、客户端的逻辑(useState, useEffect, onClick)、以及一些暧昧不清、两头都跑的代码(组件的render),全挤在一块。

我每次打开这种文件,脑子里都要启动一次“双核处理器”:

  • 左脑:这段代码在Node.js环境跑,有process.env,有数据库连接。
  • 右脑:这段代码在浏览器环境跑,有window,有document

问题是,我的脑子不是双核的啊!

二、“水合”灾难:两边都得伺候的祖宗

Next.js 最引以为傲的“水合”(Hydration),恰恰是代码混杂最恶心的体现。

你的组件代码,先在服务器上跑一遍,生成HTML字符串。然后,到了浏览器,再跑一遍,给这个HTML“注入生命”。

这就导致了一个致命问题:

你写的每一行组件代码,都必须同时服务端和客户端“两开花”。

你一不小心在组件顶层用了一个localStorage.getItem('token'),好了,服务端直接报错——“localStorage is not defined”。

你小心翼翼地加了个typeof window !== 'undefined'的判断,代码变得丑陋不堪。

更恶心的是,就算你处理好了报错,水合不匹配(Hydration Mismatch)又会找上门。服务器渲染的HTML和客户端第一次渲染的Virtual DOM对不上,React就给你报警告,甚至直接重渲染,把你“SSR优化”带来的那点性能优势,全部葬送。

我写的是React,不是兼容性测试工具!

三、“我到底该把逻辑放哪?”——一个哲学问题

在Next.js里,你无时无刻不在面临这种灵魂拷问:

  • 这个数据获取逻辑,放在getServerSideProps里?还是放在useEffect里用fetch?还是放在getStaticProps里构建时拿?还是用SWR在客户端“重新验证”?
  • 这个环境变量,是NEXT_PUBLIC_开头的吗?不是的话,它只存在于服务端,客户端用不了。是的话,它又会被打包进客户端的JS里,万一里面有敏感信息呢?
  • 这个工具函数,它用了fs模块读文件,又用了fetch请求API。它到底是一个“服务端工具”还是“客户端工具”?要不要加个'use client'指令?

我每天不是在写业务逻辑,我是在给代码做“环境归属鉴定”。

这种心智负担,不是来自于技术的复杂性,而是来自于框架设计本身对“关注点分离”原则的背叛。

四、所谓的“全栈”,不过是“全混”

有人说:“你可以在/pages/api里写后端逻辑,在/pages里写页面逻辑,这不就分开了吗?”

这更是个笑话。

  • pages/api里的代码,本质上就是一个个Serverless函数。但你依然要在同一个项目里,跟你的UI代码共享node_modules,共享tsconfig.json
  • 你的前端代码可以轻易地import一个原本只想在getServerSideProps里用的工具函数,然后发现它在客户端打包时引用了crypto核心模块,直接导致你的页面体积暴涨。

Next.js 的“全栈”,不是前后端分离,而是前后端“大乱炖”。它用一个模糊的边界,吞噬了开发者对代码执行环境的清晰认知。

五、我只是个前端,我不想当“全栈背锅侠”

我真的只是想写一个漂亮的UI,处理一些用户的点击交互。

但现在,我被迫要关心:

  • Node.js 的内存限制
  • 数据库连接池的配置
  • Serverless 函数的冷启动时间
  • API 路由的 CORS 问题
  • 服务端渲染的缓存策略

我只是个前端啊!我当初入这行,是因为我喜欢视觉反馈、喜欢交互、喜欢用代码创造界面。我不是来给运维和后端擦屁股的。

当我把一个useState写错了,导致整个页面的SSR崩溃,返回一个500错误页面时,我真的想仰天长啸:

“我特么只是想加个按钮的状态啊!!!”

结语:把“关注点分离”还给我

所以,亲爱的Next.js,求求你了。

别再跟我吹什么“全栈”、“统一”、“融合”了。

我怀念的,是那个前端代码只跑在浏览器的时代。
我怀念的,是那个后端接口明明白白写在另一个项目里的时代。
我怀念的,是那个src/下面是客户端,server/下面是服务端,井水不犯河水的时代。

清晰的边界,让团队可以分工协作,让新人可以快速上手,让我的脑子可以不用时刻切换上下文。

你问我为什么还在用?

因为老板看了营销文,说“大家都在用”,因为技术选型不是我能决定的。

但至少,请让我在代码里,保留最后一声呐喊:

别再让我在一个文件里,同时写服务器和客户端的代码了!我的大脑,它真的会过载!