在 Next.js 开发中,服务端组件和 Server Actions 功能强大,却隐藏着不少容易被忽视的陷阱,新手往往因此踩坑。 本文总结了几个新手常见陷阱,帮你快速避坑 🖖
1 缓存未更新导致新增数据不显示
在 Next.js 中,服务端组件默认会缓存渲染的结果。当你执行一个操作,如果没有明确指示要刷新缓存,页面将继续显示缓存的旧数据,而不会显示最新数据。
比如,在页面中添加物品。
// app/products/page.tsx
import { addProduct } from './actions';
import clientPromise from '@/lib/mongodb';
export default async function ProductPage() {
const client = await clientPromise;
const db = client.db('ceshi'); // 指定数据库
const collection = db.collection('ceshi1210'); // 指定集合
// 从数据库获取数据
const products = await collection.find().toArray();
return (
<div>
<h1>Product List</h1>
<form action={addProduct}>
<input type="text" name="name" placeholder="Product Name" required />
<input type="number" name="price" placeholder="Price" required />
<button type="submit">Add Product</button>
</form>
<div>
{products.map((product) => (
<div key={product._id.toString()}>
<h2>{product.name}</h2>
<p>{product.price}</p>
</div>
))}
</div>
</div>
);
}
// app/products/actions.ts
'use server';
import clientPromise from '@/lib/mongodb';
export async function addProduct(formData: FormData) {
const name = formData.get('name') as string;
const price = parseFloat(formData.get('price') as string);
const client = await clientPromise;
const db = client.db('ceshi');
const collection = db.collection('ceshi1210');
await collection.insertOne({ name, price });
}
添加一个包包,价格200,提交之后,页面并没有立马显示出来添加的产品。
并且可以在数据库中看到包包确实添加了:
这是因为服务端组件默认缓存渲染结果,新增数据后可能不会自动刷新页面内容。
如果想解决这个问题,需要使用 revalidatePath()
告诉 Next.js 手动刷新特定页面的缓存。
// app/products/actions.ts
'use server';
import clientPromise from '@/lib/mongodb';
import { revalidatePath } from 'next/cache'; // 导入 revalidatePath
export async function addProduct(formData: FormData) {
const name = formData.get('name') as string;
const price = parseFloat(formData.get('price') as string);
const client = await clientPromise;
const db = client.db('ceshi');
const collection = db.collection('ceshi1210');
await collection.insertOne({ name, price });
// 刷新缓存,确保页面重新获取最新数据
revalidatePath('/products');
}
使用 revalidatePath('/products')
清除 /products
路径的缓存, 数据新增后,页面会立即展示新增内容。
2 Server Actions 在客户端组件中也可以使用
Server Actions 不仅限于服务端组件,也可以在客户端组件中直接调用,从而灵活地在用户交互中执行服务端操作。
还是以新增产品为例,当我们通过表单想要在数据库中增加一个新的产品,并且这个表单是客户端组件,很多开发者可能会这么写:
// app/components/ProductForm.tsx
'use client';
import { useState } from 'react';
export default function ProductForm() {
const [name, setName] = useState('');
const [price, setPrice] = useState('');
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
// 使用 fetch 调用一个单独的 API 路由
await fetch('/api/addProduct', {
method: 'POST',
body: JSON.stringify({ name, price: parseFloat(price) }),
headers: { 'Content-Type': 'application/json' },
});
setName('');
setPrice('');
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
placeholder="Product Name"
value={name}
onChange={(e) => setName(e.target.value)}
required
/>
<input
type="number"
placeholder="Price"
value={price}
onChange={(e) => setPrice(e.target.value)}
required
/>
<button type="submit">Add Product</button>
</form>
);
}
// app/api/addProduct/route.ts
import { NextResponse } from 'next/server';
import clientPromise from '@/lib/mongodb';
export async function POST(req: Request) {
const client = await clientPromise;
const db = client.db('ceshi');
const collection = db.collection('ceshi1210');
const body = await req.json();
await collection.insertOne({ name: body.name, price: body.price });
return NextResponse.json({ success: true });
}
事实上这样写API的方式完全是多余的,因为在客户端组件中,也是可以直接调用 Server Action 的。
在Next.js项目中,很少要自己去写API(除非是像支付、调用第三方接口这种情况),一般和数据库的操作都可以通过 Server Action 完成。
当采用 Server Action 时,上面的例子可以简化成以下代码:
// app/components/ProductForm.tsx
'use client';
import { useState } from 'react';
import { addProduct } from '../actions'; // 直接导入 Server Action
export default function ProductForm() {
const [name, setName] = useState('');
const [price, setPrice] = useState('');
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
await addProduct({ name, price: parseFloat(price) }); // 直接调用 Server Action
setName('');
setPrice('');
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
placeholder="Product Name"
value={name}
onChange={(e) => setName(e.target.value)}
required
/>
<input
type="number"
placeholder="Price"
value={price}
onChange={(e) => setPrice(e.target.value)}
required
/>
<button type="submit">Add Product</button>
</form>
);
}
// app/actions.ts
'use server';
import clientPromise from '@/lib/mongodb';
export async function addProduct({ name, price }: { name: string; price: number }) {
const client = await clientPromise;
const db = client.db('ceshi');
const collection = db.collection('ceshi1210');
await collection.insertOne({ name, price });
}
不需要再去写 // app/api/addProduct/route.ts 文件。这样减少了多余的请求,用户体验更流畅。
3 忘记验证和保护 Server Actions
Server Actions 是 Next.js 提供的一种强大功能,可以在服务端直接执行逻辑,比如新增数据到数据库。但它本质上是一个公开的接口,就像传统的 POST
API 路由。
如果我们不对数据进行验证,也不限制谁可以调用这个接口,任何人都可以通过伪造请求来操作数据库,比如插入错误的数据甚至恶意篡改数据。
常见的问题来源有:
- 数据验证缺失:
传入的表单数据可能是空的、格式错误的,甚至带有恶意内容。
- 用户身份验证缺失:
Server Actions 默认不检查调用者身份,任何人都能访问。如果没有保护,系统可能被恶意用户利用。
对于上面添加产品的例子,可以使用 Zod 进行数据验证并且使用 next-auth 进行身份验证:
// app/actions.ts
'use server';
import clientPromise from '@/lib/mongodb';
import { z } from 'zod'; // 数据验证库
import { getSession } from 'next-auth'; // 用户身份认证库
// 定义数据验证规则
const ProductSchema = z.object({
name: z.string().min(1), // 名称必须是非空字符串
price: z.number().positive(), // 价格必须是正数
});
export async function addProduct(formData: FormData) {
// 验证用户是否登录
const session = await getSession();
if (!session?.user) {
throw new Error('Unauthorized'); // 如果用户未登录,抛出错误
}
// 提取并验证表单数据
const data = {
name: formData.get('name') as string,
price: parseFloat(formData.get('price') as string),
};
const validatedData = ProductSchema.parse(data); // 验证数据是否符合规则
const client = await clientPromise;
const db = client.db('ceshi');
const collection = db.collection('ceshi1210');
// 插入已验证的数据
await collection.insertOne(validatedData);
}
4 错误地使用 use server 指令
use server
是一个特殊的指令,通常用来定义 Server Actions 。但是,许多开发者会错误地认为它可以简单地用来限制某些函数只能运行在服务端。
实际上,使用 use server
会让函数成为一个暴露的服务端接口,这个服务端接口可能被外部用户调用,例如,一个简单的数据库查询函数不应该作为一个公开接口被访问。
比如:
'use server'; // 在普通函数中滥用
import clientPromise from '@/lib/mongodb';
export async function getProducts() {
const client = await clientPromise;
const db = client.db('ceshi');
const collection = db.collection('ceshi1210');
// 返回数据库中所有产品
return await collection.find().toArray();
}
首先,Next.js 中默认都是服务端组件,不需要专门在文件顶部写 'use server' 来声明这是个服务端组件。
其次,加了 'use server' 之后,任何人都可以通过这个接口来直接读取数据库中的内容,并且没有任何权限认证,反而引起了数据安全问题。
5 动态路由和 params
的使用
在 Next.js 中,动态路由允许通过 URL 的某个部分动态渲染内容(比如 /product/4
中的 4
表示产品 ID)。我们可以通过 params
参数获取这些动态部分。但很多开发者误以为 params
是全局可用的,可以在所有组件中随时调用。
比如我们有一个动态路由文件:app/product/[id]/page.tsx
,用于显示产品详情。
export default function ProductPage({ params }: { params: { id: string } }) {
return (
<div>
<h1>Product Page</h1>
<ProductComponent /> {/* 没有传递 params */}
</div>
);
}
// app/product/[id]/ProductComponent.tsx
import { params } from 'next/navigation'; // 错误使用
export default function ProductComponent() {
return <div>Product ID: {params.id}</div>; // 直接尝试访问 params
}
这样运行会报错 params 未定义。
实际上,params
只能在动态路由的页面组件中使用,如果子组件需要访问 params
,必须通过 props
显式传递。
正确的传递给组件的代码应该是:
// app/product/[id]/page.tsx
import ProductComponent from './ProductComponent';
export default function ProductPage({ params }: { params: { id: string } }) {
return (
<div>
<h1>Product Page</h1>
<ProductComponent id={params.id} /> {/* 显式传递 id */}
</div>
);
}
// app/product/[id]/ProductComponent.tsx
import { params } from 'next/navigation'; // 错误使用
export default function ProductComponent({ id }: { id: string }) {
return <div>Product ID: {id}</div>; // 使用 props 中的 id
}
本文为开发者提供了实用的避坑技巧,帮助你更高效、安全地构建应用; 掌握缓存更新、Server Actions、安全验证、use server
和动态路由的正确用法,可以避免常见问题,让开发更加高效安全。