我一直没搞明白一件事:同样是给 AI 提需求,为什么有人一句话丢过去,页面、接口、数据库全给铺得整整齐齐;轮到我,AI 却老在反问------"这个组件你想放哪个目录?""接口前缀用 /api 还是 /service?"
直到我打开一个同事用 Next.js 写的项目,盯着 app 目录看了半分钟,才反应过来:不是 AI 偏心,是我没给它一套规矩。
我以前理解的框架,太浅了
说出来不怕笑话,我以前觉得框架就是"帮我少写几行代码的东西"。路由自己写也能写,目录自己建也能建,要它干嘛?
后来才想明白,框架真正替我做的,是一大堆"放在哪"的决定。
就像租房子。毛坯房当然自由,卧室改厨房都行,但代价是每件事都得自己拍板:水电怎么走,橱柜打多高。而精装修的房子,厨房在那、厕所在那、插座在那,你拎包入住,只需要决定挂什么窗帘。框架就是后者------它把项目的"户型"定死了,你专心往里填家具,也就是业务。
先聊聊 React 把我从什么日子里救出来的
在说 Next 之前,得先说 React,因为 Next 是长在 React 身上的。
早年写原生 JS 改一个按钮上的数字,我得这样伺候 DOM:先找到元素、绑事件、改完数据还得手动把文字塞回去,一套命令式流水线,少一步都不行。React 给我的东西说起来就一个钩子:
javascript
const [count, setCount] = useState(0)
我的任务从"操作页面"变成了"描述状态长什么样"。count 是 0,页面就显示 0;我把 count 改成 1,React 自己去跟 DOM 对账。我不再关心按钮怎么更新,只关心数据现在是什么。
说白了,原生 JS 是我指挥浏览器一步一步干活;React 是我把结果画出来,它替我想办法变成那个结果。
不过这里我栽过一个跟头,当时盯着屏幕愣了好一会儿:
javascript
onClick={() => {
setCount(count + 1)
setCount(count + 1) // 我以为会 +2,结果还是 +1,人傻了
}}
后来翻了更新机制才懂,两次调用拿到的都是这一轮渲染里的旧 count,等于两次都说"给我变成 1"。改成函数式写法就对了:
javascript
onClick={() => {
setCount(prev => prev + 1) // prev 永远是最新的那个
setCount(prev => prev + 1) // 这才是真的 +2
}}
这个坑让我第一次真切体会到:我描述的是"状态怎么变",不是"帮我去点按钮"。
打开 app 目录,我突然懂了 AI 的感受
React 只管视图,真要做一个能跑的网站,问题马上就回来了:页面文件放哪?图片放哪?后端接口谁来写?
以前我的做法是 Vite + React 一个仓库,Express 另起一个,中间用接口连。每次让 AI 加功能,它都得先摸清我这套野路子,上下文一半浪费在"认路"上。
Next.js 干的事,就是把这些路全部提前修好:
public/:扔图片、字体这些静态资源,别问,问就是这;app/:页面和接口都住这里,一个目录基本对应一个网址;components/:可复用的组件。
我管这叫"一个厨房两个出餐口"。以前是前端一个厨房、后端一个厨房,中间还得雇个传菜的(接口联调);现在同一个项目里,页面在服务器上先炒得差不多再端给浏览器,接口也在隔壁出餐,人马是一套,语言也是一种。
而这对 AI 太重要了。它不用再猜我把东西藏哪了,因为全世界的 Next 项目长得都差不多。我给它的不是一堆散乱的积木,而是一套带编号的乐高底板。
掏个能跑的最小 demo
光说不练假把式,这是我当时跑通的那份,就几行。
先起项目,终端里跟着提示选,Tailwind 和 App Router 我都选了 Yes:
bash
npx create-next-app@latest my-notes
cd my-notes
npm run dev
起完的目录长这样:
text
my-notes/
├── app/
│ ├── page.js # 首页,访问 /
│ ├── api/hello/route.js # 接口,访问 /api/hello
│ └── components/
│ └── Counter.js # 计数器组件
└── public/ # 图片往这扔
计数器因为用到了状态,得明确告诉 Next 这是个在浏览器里跑的组件。注意第一行,坑了我半小时:
javascript
// app/components/Counter.js
'use client' // 没这行,下面的 useState 构建时直接报错,别问我为什么知道
import { useState } from 'react'
export default function Counter() {
const [count, setCount] = useState(0)
return (
<button
className="rounded bg-black px-4 py-2 text-white"
onClick={() => setCount(prev => prev + 1)}
>
点了 {count} 次
</button>
)
}
首页本身默认在服务器渲染,直接把组件请进来就行:
javascript
// app/page.js
import Counter from './components/Counter'
export default function Home() {
return (
<main className="p-10">
<h1 className="text-2xl font-bold">我的第一个 Next 页面</h1>
<Counter />
</main>
)
}
接口更是简单得离谱,一个文件、一个函数:
javascript
// app/api/hello/route.js
export async function GET() {
return Response.json({ message: '接口也好了,就这么点代码' })
}
打开终端验证一下:
bash
$ curl http://localhost:3000/api/hello
{"message":"接口也好了,就这么点代码"}
页面能点,接口能通,而我连一个配置文件都没碰。
我忍不住多扒了一层:AI 为什么格外吃这套
跑通之后我好奇,为啥 Claude Code、Codex 这些工具生成 Next 代码的成功率明显高?翻了几个项目我大概想明白了。
一是文件名即路由。新建 app/about/page.js,/about 这个网址自动就有了,不用去路由表里登记。AI 不用猜我的路由配置长什么样,文件往那一放,规矩自动生效。
二是 Tailwind 的类名自带语义。flex、gap-4、mt-8,名字本身就在说话,AI 写 mt-8 可比给我的 card-wrapper-inner-box 起名、再回头补 CSS 靠谱多了。
三是生态。像 shadcn/ui 这种组件库,组件代码是直接复制进我项目里的,AI 看得见、改得动,不是对着一个黑盒 npm 包瞎猜。再加上写完往 Vercel 一推,域名都给你备好,这种从写到发的完整链路,传统拼凑式技术栈很难给到。
说到底,约定越统一,AI 的"猜测空间"就越小。我把规矩交出去,换来的是它少问我十句话。
我掉进去的坑,给你们垫垫脚
第一个坑前面提了:在默认的服务器组件里用 useState,忘了写 'use client',构建时被一顿教育。
第二个坑更隐蔽。本地 Windows 上跑得好好的,推到 Vercel 构建直接失败:
javascript
// 文件实际叫 Counter.js,我手一滑写成了小写
import Counter from './components/counter'
Windows 不区分大小写,本地惯着我;线上是 Linux,六亲不认。这事之后我 import 路径一律靠编辑器自动补全,再也不手敲了。
错误姿势:路径手敲、凭感觉建目录、客户端和服务端组件全凭心情。 正确姿势:跟着 app 目录的约定走,能自动补全就别动键盘,用到状态和浏览器事件先想想要不要加
'use client'。
也不是说它就天下无敌
聊到这得说句公道话,Next.js 不是什么场景都该上。
如果后端是强一致性要求的复杂交易系统,Java、Go 那一摊子成熟的中间件更让人睡得着觉;如果团队全是 Vue 技术栈,为了 AI 支持好就硬换框架,属于本末倒置;就一个纯静态的介绍页,硬上 Next 也是大炮打蚊子。
它最趁手的,是我现在经常干的事:前后端一个人包、用 AI 加速、追求从页面到接口一把梭。
最后把我这几天最实在的三个感受拎出来:框架帮我省的不是代码,是做决定的精力,顺便给了 AI 一套共同语言;React 让我描述状态而不是操作 DOM,Next 把这套描述延伸到了服务器和接口;我越守约定,AI 能帮我干的活就越多。
要是你也有过"AI 怎么老不懂我项目"的憋屈,真的可以花一个下午,顺着 app 目录摸一遍。搞懂了记得回来留个言,我也想看看你的理解。