Nextjs/React之踩踩坑
Next.js
React

Nextjs/React之踩踩坑

关于Client Component的获取数据方式

Server Actions

优点
  • TypeScript 类型安全,完美契合 Next.js 的全栈开发模式
  • 天然支持服务端数据操作,无需暴露 API 端点
缺点
  • 本质是 POST 请求,多个数据源会形成瀑布流请求链,显著降低页面加载速度
  • 官方推荐仅用于数据变更操作(如表单提交),而非数据获取

 传统 Fetch + useEffect

  • 需手动处理缓存策略、错误边界、加载状态管理
  • 易出现竞态条件(Race Conditions)和内存泄漏问题
  • 缺少自动重试机制,代码冗余度高

使用第三方库如React Query, SWR

// 结合 Server Actions 的类型安全优势 const { data } = useQuery({ queryKey: ['todos'], queryFn: () => fetchFromServerAction() }) // SWR 自动重试示例 const { data, error } = useSWR('/api/data', fetcher, { onErrorRetry: (error, key, config, revalidate, { retryCount }) => { if (retryCount >= 3) return setTimeout(() => revalidate(), 5000) } })
 
我的感想是如果页面上只有单一的数据源,直接使用server action就行,如果有多个数据源,就是使用第三方库来获取数据,但是好像有第三方库配合server actions的用法,既能保证parallel数据获取,又可以保证type safe,有时间研究一下

关于Static Rendering

如果想要在build time预渲染好dynamic routing的页面可以使用generateStaticParams
当然前提是页面必须为server component
// app/blog/[slug]/page.tsx export async function generateStaticParams() { const posts = await fetch('https://.../posts').then(res => res.json()) return posts.map(post => ({ slug: post.slug, })) } export default async function Page({ params }: { params: { slug: string } }) { const post = await fetchPost(params.slug) return <Article content={post.content} /> }

Tailwind 深色模式

可以通过css variable使用Semantic naming的方式来定义浅色和深色模式对应的颜色
这样就不用写”dark: XXXX”
/* globals.css */ :root { --primary-bg: hsl(0, 0%, 100%); /* 浅色模式背景 */ --primary-text: hsl(240, 10%, 15%); /* 浅色模式文字 */ } .dark { --primary-bg: hsl(240, 10%, 15%); /* 深色模式背景 */ --primary-text: hsl(0, 0%, 100%); /* 深色模式文字 */ }
现在写组件方便多了
<div class="bg-primary-bg text-primary-text"> 管你深色浅色,我只认 semantic name! </div>

Next-Theme

一般使用next-theme来把用户选择的主题保存在local storage
创建 Client Component 包裹 ThemeProvider
// providers/theme-provider.tsx 'use client' import { ThemeProvider as NextThemeProvider } from 'next-themes' export function ThemeProvider({ children }: { children: React.ReactNode }) { return ( <NextThemeProvider attribute="class" storageKey="theme-preference" enableSystem={false} > {children} </NextThemeProvider> ) } // 根布局配置 <html lang="en" suppressHydrationWarning> <body> <ThemeProvider>{children}</ThemeProvider> </body> </html>
把rootlayout包住,然后在html tag上加上suppressHydrationWarning来阻止hydration报错
  • suppressHydrationWarning:因为主题从 localStorage 读取(浏览器 API),服务端无法预知,SSR 阶段必然导致 HTML 与客户端不一致。这个属性让 React 闭嘴别报警告。
  • attribute="class":让 next-theme 通过修改 <html> 的 class(dark/light)切换主题,正好对接我们的 CSS 变量方案。

Server Component的滥用

感觉我陷入了一个误区,不管什么page都要写成server component,它虽然好,但是一旦涉及到state management就吃大亏,比如要实现一个搜索框,server component虽然也能实现,通过读取url的参数来获取query,但是这样就等于多写了一个页面,如果想要实现例如弹出一个搜索框modal,只能老老实实写client component
所以一旦涉及数据实时更新,很多的数据交互,还是得用client component才可以获得更好的UX还有DX的体验,像是博客这种全是静态页面的,就是server component大展身手的时候了

🚫 Server Component 禁区

  • 任何需要 useState / useEffect 的交互逻辑
  • 事件监听(点击、滚动、表单输入)
  • 需要浏览器 API 的功能(弹窗、动画、localStorage)

✅ Server Component 快乐屋

  • 纯静态内容(博客、文档、产品介绍页)
  • 重度依赖后端数据的页面(电商列表、仪表盘)
  • SEO 关键路径(用服务器数据生成完整 HTML)

正确姿势

最终把组件拆成「双胞胎模式」:
// app/articles/page.tsx(Server Component) export default async function Page({ searchParams }: { searchParams?: { q?: string } }) { // 服务端获取数据(SEO 友好) const initialData = await fetchArticles(searchParams?.q) return ( // 把交互逻辑甩锅给 Client Component <ArticlesClientComponent initialData={initialData} /> ) }
// components/articles-client.tsx(Client Component) 'use client' export function ArticlesClientComponent({ initialData }: { initialData: Article[] }) { // 客户端状态管理 const [searchQuery, setSearchQuery] = useState('') const [showFilterModal, setShowFilterModal] = useState(false) // 优雅的实时搜索(防抖 + 客户端过滤) const filteredData = useMemo(() => ( initialData.filter(item => item.title.includes(searchQuery)) ), [searchQuery]) return ( <> <input value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} placeholder="实时搜索..." /> <button onClick={() => setShowFilterModal(true)}> 高级筛选 </button> {/* 弹窗自由! */} {showFilterModal && ( <FilterModal onClose={() => setShowFilterModal(false)} /> )} {filteredData.map(article => <ArticleCard key={article.id} />)} </> ) }

Edge Runtime

🔥
最新消息Next.js版本15.2支持了Middleware配置Node.js Runtime
事情是这样的...
最近给 Next.js 项目加鉴权中间件,想着直接在 middleware 里查数据库拿用户 session 多优雅。结果代码一跑就报错,定睛一看:Error: PrismaClient can't run on Edge Runtime!
当场傻眼——原来 Next.js 的 middleware 默认跑在 Edge Runtime,只支持部分API,看了看GitHub Issues也有很多开发者抱怨何时支持nodejs run time

临时抱佛脚的土方法

搞了个曲线救国方案:
1️⃣ 先写个走 Node.js Runtime 的 API 端点查数据库
// 这个藏在 /pages/api/session.ts 里 export default async function handler() { const session = await prisma.session.findUnique(...) return res.json(session) // 当个无情的传话筒 }
2️⃣ 在 middleware 里厚着脸皮调自己的 API
// middleware.ts 里的骚操作 const session = await fetch('/api/session').then(r => r.json())
虽然能用,但每次鉴权都要绕路调接口,性能直接扑街,代码看着也像打补丁。

后来。。。

后来发现原来Prisma支持Edge Runtime,只要。。
// 旧代码(会报错) import { PrismaClient } from '@prisma/client' // 新代码(原地复活!) import { PrismaClient } from '@prisma/client/edge'

Session Token/JWT token安全性

1. HTTP-only Cookie 防篡改机制

将 Session Token 或 JWT Token 存储在 HTTP-only Cookie 中,确保浏览器端的 JavaScript 无法通过 document.cookie 读取或修改。此设计直接阻断 XSS 攻击窃取凭据的路径。

2. Session Token 的服务器端验证逻辑

  • 服务端存储:Session Token 的完整数据(如用户 ID、权限、过期时间)存储在服务端数据库(如 Redis),仅向客户端返回 Session ID,且这个ID很长,不可能通过暴力破解猜到。
  • 防篡改验证:客户端每次携带 Session ID 请求时,服务端会校验该 ID 是否存在有效关联数据。若 ID 被篡改(如伪造随机 UUID),服务端因无对应数据而直接拒绝请求。

3. JWT Token 的密码学签名验证

JWT 由三部分构成(以 . 分隔):
  • Header:声明令牌类型(typ: "JWT")和签名算法(如 alg: HS256),经 Base64Url 编码。
  • Payload:存储声明(Claims),如用户身份(sub)、过期时间(exp)等业务数据,经 Base64Url 编码。
  • Signature:对 Base64Url(Header).Base64Url(Payload) 的原始二进制数据,使用服务端私钥按 Header 声明的算法生成签名。
篡改防御原理
若攻击者修改 Header 或 Payload 内容(如将 role: "user" 改为 role: "admin"),必须重新生成合法签名。由于缺乏服务端私钥(HS256)或无法破解非对称加密(如 RS256),新生成的签名与服务端验签结果必然不匹配,导致令牌失效。
方案
存储位置
防篡改机制
失效控制
性能
Session
服务端数据库
篡改不了一点
服务端主动清除/手动Revoke
依赖于服务器性能,使用Redis可显著提高性能
JWT
客户端 Cookie
签名验证
依赖过期时间