用Next.js 构建一个简单的 CRUD 应用:集成 API 路由和数据获取
用Next.js 构建一个简单的 CRUD 应用:集成 API 路由和数据获取
作者:码力无边
在过去的几篇文章中,我们分别深入了 Next.js 的两个核心领域:前端的数据获取策略 (SSG, SSR, CSR) 和后端的 API 路由。我们学会了如何展示数据,也学会了如何创建提供数据的 API。现在,是时候将这两者串联起来,构建一个完整、动态、可交互的全栈应用了。
本文将通过一个经典的实例——一个简单的待办事项 (Todo) 列表应用——来指导你完成一个完整的 CRUD (Create, Read, Update, Delete) 流程。在这个过程中,你将综合运用到:
- API 路由:构建后端逻辑来处理数据的增删改查。
getServerSideProps
:在页面加载时获取初始的数据列表 (Read)。- 客户端数据获取 (SWR):在用户交互后,高效地更新 UI (Create, Update, Delete)。
这个项目将是你从理论走向实践的关键一步,让你真正体验到 Next.js 全栈开发的流畅与强大。
步骤一:搭建后端 - 我们的 API 路由
首先,我们需要为待办事项提供数据支持。我们将创建两个 API 端点:
/api/todos
:用于获取所有待办事项和创建新的待办事项。/api/todos/[id]
:用于更新和删除单个待办事项。
为了简单起见,我们依然使用一个内存中的数组来模拟数据库。
1. 创建 pages/api/todos.ts
// pages/api/todos.ts
import type { NextApiRequest, NextApiResponse } from 'next';export type Todo = {id: number;text: string;completed: boolean;
};// 模拟数据库
let todos: Todo[] = [{ id: 1, text: '学习 Next.js API 路由', completed: true },{ id: 2, text: '构建一个 CRUD 应用', completed: false },{ id: 3, text: '部署到 Vercel', completed: false },
];export default function handler(req: NextApiRequest, res: NextApiResponse) {switch (req.method) {case 'GET':// 获取所有 todosres.status(200).json(todos);break;case 'POST':// 创建一个新的 todoconst { text } = req.body;if (!text) {return res.status(400).json({ error: 'Text is required' });}const newTodo: Todo = {id: Date.now(),text,completed: false,};todos.push(newTodo);res.status(201).json(newTodo);break;default:res.setHeader('Allow', ['GET', 'POST']);res.status(405).end(`Method ${req.method} Not Allowed`);}
}
2. 创建 pages/api/todos/[id].ts
// pages/api/todos/[id].ts
import type { NextApiRequest, NextApiResponse } from 'next';
import type { Todo } from './'; // 从同级 index (todos.ts) 导入类型// 这里的 todos 数组需要在模块间共享,实际项目中会用数据库
// 为了简化,我们假设这里的修改能影响到 todos.ts 中的数组
// 注意:在无服务器环境下,这种内存共享是不可靠的!仅用于演示。
// 更好的方式是从一个共享的文件或真正的数据库中导入/导出
import { todos } from './_db'; // 假设我们将 todos 移动到了一个 _db.ts 文件export default function handler(req: NextApiRequest, res: NextApiResponse) {const { id } = req.query;const todoId = parseInt(id as string, 10);let todoIndex = todos.findIndex((t) => t.id === todoId);if (todoIndex === -1) {return res.status(404).json({ error: 'Todo not found' });}switch (req.method) {case 'PUT':// 更新一个 todo (切换 completed 状态或修改文本)const { text, completed } = req.body;const originalTodo = todos[todoIndex];todos[todoIndex] = { ...originalTodo, text: text ?? originalTodo.text, completed: completed ?? originalTodo.completed };res.status(200).json(todos[todoIndex]);break;case 'DELETE':// 删除一个 todotodos.splice(todoIndex, 1);res.status(204).end();break;default:res.setHeader('Allow', ['PUT', 'DELETE']);res.status(405).end(`Method ${req.method} Not Allowed`);}
}
// 注意:为了让数据在 API 路由间共享,你需要将内存数组 `todos` 提取到一个单独的文件中(如 `lib/db.ts`)并从两个 API 文件中导入。
_db.ts
的说明: 现实中内存数组在Serverless函数间不共享,需要数据库。为模拟,可创建pages/api/_db.ts
,导出todos
数组,再在两个API文件中导入。
我们的后端现在已经准备就绪!
步骤二:构建前端 - 页面和组件
我们将创建一个主页面 pages/index.tsx
来展示和管理待办事项。
1. 页面初始数据加载 (Read - SSR)
我们希望用户打开页面时能立即看到待办事项列表,这对于 SEO 和用户体验都很好。因此,我们使用 getServerSideProps
。
// pages/index.tsx
import type { GetServerSideProps, InferGetServerSidePropsType } from 'next';
import type { Todo } from './api/todos'; // 复用 API 中的类型
import TodoList from '../components/TodoList';// 从我们自己的 API 获取初始数据
export const getServerSideProps: GetServerSideProps<{ initialTodos: Todo[] }> = async () => {const res = await fetch('http://localhost:3000/api/todos');const initialTodos: Todo[] = await res.json();return {props: {initialTodos,},};
};export default function HomePage({ initialTodos }: InferGetServerSidePropsType<typeof getServerSideProps>) {return (<div><h1>我的待办事项</h1><TodoList initialData={initialTodos} /></div>);
}
2. 创建交互组件 (Create, Update, Delete - CSR with SWR)
现在,我们将交互逻辑封装在一个 <TodoList />
组件中。我们将使用 SWR 来管理客户端的数据状态,它能极大地简化数据同步和 UI 更新的逻辑。
首先,安装 SWR:npm install swr
components/TodoList.tsx
import useSWR, { useSWRConfig } from 'swr';
import type { Todo } from '../pages/api/todos';
import { useState } from 'react';const fetcher = (url: string) => fetch(url).then((res) => res.json());export default function TodoList({ initialData }: { initialData: Todo[] }) {const { mutate } = useSWRConfig();const { data: todos, error } = useSWR<Todo[]>('/api/todos', fetcher, {fallbackData: initialData, // 使用 SSR 提供的初始数据});const [newTodoText, setNewTodoText] = useState('');const handleCreateTodo = async (e: React.FormEvent) => {e.preventDefault();if (!newTodoText.trim()) return;// 乐观更新 UIconst tempId = Date.now();const optimisticData = [...(todos || []), { id: tempId, text: newTodoText, completed: false }];mutate('/api/todos', optimisticData, false);// 发送请求await fetch('/api/todos', {method: 'POST',headers: { 'Content-Type': 'application/json' },body: JSON.stringify({ text: newTodoText }),});// 请求结束后,触发 SWR 重新验证以获取最新数据mutate('/api/todos');setNewTodoText('');};const handleToggleComplete = async (todo: Todo) => {// 乐观更新const updatedTodos = todos?.map(t => t.id === todo.id ? { ...t, completed: !t.completed } : t);mutate('/api/todos', updatedTodos, false);await fetch(`/api/todos/${todo.id}`, {method: 'PUT',headers: { 'Content-Type': 'application/json' },body: JSON.stringify({ completed: !todo.completed }),});mutate('/api/todos');};const handleDeleteTodo = async (id: number) => {// 乐观更新const filteredTodos = todos?.filter(t => t.id !== id);mutate('/api/todos', filteredTodos, false);await fetch(`/api/todos/${id}`, { method: 'DELETE' });mutate('/api/todos');};if (error) return <div>加载失败</div>;if (!todos) return <div>加载中...</div>;return (<div><form onSubmit={handleCreateTodo}><inputtype="text"value={newTodoText}onChange={(e) => setNewTodoText(e.target.value)}placeholder="添加新的待办事项"/><button type="submit">添加</button></form><ul>{todos.map((todo) => (<li key={todo.id} style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}><span onClick={() => handleToggleComplete(todo)} style={{ cursor: 'pointer' }}>{todo.text}</span><button onClick={() => handleDeleteTodo(todo.id)} style={{ marginLeft: '10px' }}>删除</button></li>))}</ul></div>);
}
代码解读与核心概念
-
混合渲染模式:我们完美地结合了 SSR 和 CSR。页面首次加载时,通过
getServerSideProps
快速呈现内容 (SSR)。之后的所有交互(增删改),都由客户端处理 (CSR),提供了流畅的单页应用体验。 -
SWR 的妙用:
fallbackData
: SWR 使用getServerSideProps
传来的initialData
作为初始状态,避免了客户端的二次请求。mutate
: 这是 SWR 的核心函数之一,用于手动更新缓存数据。- 乐观更新 (Optimistic UI):在
handleCreateTodo
等函数中,我们先假定 API 请求会成功,并立即更新本地 UI (mutate(..., ..., false)
)。这让应用感觉响应极快。然后,我们再发送真实的网络请求。请求完成后,再次调用mutate('/api/todos')
来与服务器的真实状态进行同步,确保数据一致性。这是一种提升用户体验的高级技巧。
总结
恭喜你!你刚刚构建了一个功能完整的 Next.js 全栈应用。通过这个项目,我们将零散的知识点串成了一条完整的价值链:
- 后端:我们使用 API 路由 创建了健壮的、符合 RESTful 风格的 API 来管理我们的数据资源。
- 前端 - 初始加载:我们使用
getServerSideProps
在服务器端获取初始数据,实现了快速的首屏加载和良好的 SEO 基础。 - 前端 - 动态交互:我们使用 CSR 模式,并借助 SWR 这样的现代化数据获取库,实现了高效、乐观的 UI 更新,提供了卓越的用户交互体验。
这个“SSR + CSR with SWR”的组合拳,是构建现代、高性能 Next.js 应用的黄金范式。它充分利用了 Next.js 在服务端和客户端的各自优势。
现在你已经具备了构建全栈应用的基础能力。在接下来的文章中,我们将继续深入 Next.js 的高级特性,比如如何使用 next/image
来优化应用的图片性能,让我们的应用不仅功能强大,而且速度飞快。敬请期待!