尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Next.js 用 Server Components 直连数据库:去掉 API 层的边界,和三条别踩的安全红线

Next.js 用 Server Components 直连数据库:去掉 API 层的边界,和三条别踩的安全红线 Next.js 用 Server Components 直连数据库:去掉 API 层的边界,和三条别踩的安全红线写惯了 React REST 的人第一次看 Next.js App Router 会懵:组件里直接await db.query(...)?那我的/api/users路由呢?说好的前后端分离呢?其实 Server Components 允许你在组件里直接查数据库,省掉一整层 API,但这不是「随便写」——用错了会把数据库连接、密钥甚至整张表都泄露到浏览器。这篇讲清楚边界在哪,以及三条不能碰的红线。传统写法:多一层 API 的样板代码以前拿一个用户列表,你得写两头。后端一个路由:// app/api/users/route.tsexportasyncfunctionGET(){constusersawaitdb.user.findMany();returnResponse.json(users);}前端一个组件,还要处理 loading、error、竞态:use client; function UserList() { const [users, setUsers] useState([]); useEffect(() { fetch(/api/users).then(r r.json()).then(setUsers); }, []); return ul{users.map(u li key{u.id}{u.name}/li)}/ul; }数据本来就在服务器上,却要序列化成 JSON、走一趟 HTTP、再在浏览器反序列化。API 路由纯粹是个中转,没有任何业务价值。Server Components 写法:组件即查询App Router 里,默认组件就是Server Component,可以是 async 函数,直接在里面查库:// app/users/page.tsx —— 默认就是 Server Component,不用加任何标记 import { db } from /lib/db; export default async function UsersPage() { // 直接查库,这段代码只在服务器跑,永远不会进浏览器 bundle const users await db.user.findMany(); return ( ul {users.map(u li key{u.id}{u.name}/li)} /ul ); }没有/api/users,没有useState,没有useEffect,没有 loading 状态样板。组件渲染时数据已经在了。这段db.user.findMany()的代码、你的数据库连接字符串、ORM 全都留在服务器,发给浏览器的只有渲染好的 HTML。边界在哪:Server 和 Client 的分水岭核心心智模型:默认全是 Server Component,加了use client才变 Client Component。Server Component:能await、能直连数据库、能读环境变量密钥,但不能用useState/useEffect/onClick等交互。Client Component:能交互、能用 hooks,但不能直接查库,拿数据得靠 props 或 Server Actions。典型的组合是:Server Component 查数据,把结果作为 props 传给 Client Component 做交互:// page.tsx (Server) —— 查数据 export default async function Page() { const users await db.user.findMany(); return UserTable users{users} /; // 数据当 props 传下去 } // UserTable.tsx (Client) —— 负责交互 use client; export function UserTable({ users }) { const [sort, setSort] useState(name); // ...排序、点击等交互逻辑 }红线一:props 会被序列化发到浏览器Server Component 传给 Client Component 的 props会被序列化并发到浏览器。如果你把整条数据库记录塞过去,里面的敏感字段(password hash、内部 token)就全泄露了——即使 UI 上没显示,打开 DevTools 的网络面板照样能看到:// 危险!整个 user 对象(含 passwordHash)被序列化发到浏览器 return Profile user{user} /; // 正确:只挑要用的字段传下去 return Profile user{{ id: user.id, name: user.name, avatar: user.avatar }} /;记住:传给 Client 的 props,等于公开数据。查库可以查全字段,传下去前必须裁剪。红线二:密钥别在会被 Client 引入的模块里db客户端、密钥这些只能在服务端用的东西,一旦被某个 Client Componentimport,就会被打进浏览器 bundle。为防止手滑,用server-only包做硬隔离:// lib/db.tsimportserver-only;// 一旦被 Client Component 引入,构建直接报错exportconstdbcreateClient(process.env.DATABASE_URL!);加了这行,任何use client文件误引lib/db都会在构建期炸掉,而不是悄悄把连接串泄露到线上。这是比「小心点」可靠得多的护栏。红线三:别在渲染里做「写」操作Server Component 是渲染逻辑,可能因预取、并发渲染被执行多次,只读是安全的。任何写操作(增删改)必须放到Server Action或 Route Handler,由用户动作显式触发:// 危险!渲染时删数据,页面刷新/预取都可能触发 export default async function Page({ searchParams }) { if (searchParams.del) await db.user.delete({ where: { id: searchParams.del } }); // ... } // 正确:写操作放进 Server Action,由表单提交触发 async function deleteUser(id: string) { use server; await db.user.delete({ where: { id } }); revalidatePath(/users); }渲染只读、变更走 Action——把这条刻进肌肉记忆,能躲开一大类「刷新页面数据莫名其妙被改」的诡异 bug。小结Server Component 可以 async 直连数据库,省掉纯中转的 API 层和一堆 loading 样板。默认全是 Server Component,use client才切到 Client;交互归 Client,取数归 Server。红线一:传给 Client 的 props 等于公开数据,传前裁剪字段。红线二:用import server-only给数据库/密钥模块上硬隔离,防止被打进浏览器。红线三:渲染只读,写操作一律走 Server Action。一句话记忆点:Server Components 不是「前端能连数据库了」,而是「查询代码留在服务器、只把渲染结果和裁剪过的数据发给浏览器」——边界感没了,泄露就来了。
返回列表