当前位置: 首页 > news >正文

用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 端点:

  1. /api/todos:用于获取所有待办事项和创建新的待办事项。
  2. /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>);
}

代码解读与核心概念

  1. 混合渲染模式:我们完美地结合了 SSR 和 CSR。页面首次加载时,通过 getServerSideProps 快速呈现内容 (SSR)。之后的所有交互(增删改),都由客户端处理 (CSR),提供了流畅的单页应用体验。

  2. SWR 的妙用

    • fallbackData: SWR 使用 getServerSideProps 传来的 initialData 作为初始状态,避免了客户端的二次请求。
    • mutate: 这是 SWR 的核心函数之一,用于手动更新缓存数据。
    • 乐观更新 (Optimistic UI):在 handleCreateTodo 等函数中,我们先假定 API 请求会成功,并立即更新本地 UI (mutate(..., ..., false))。这让应用感觉响应极快。然后,我们再发送真实的网络请求。请求完成后,再次调用 mutate('/api/todos') 来与服务器的真实状态进行同步,确保数据一致性。这是一种提升用户体验的高级技巧。

总结

恭喜你!你刚刚构建了一个功能完整的 Next.js 全栈应用。通过这个项目,我们将零散的知识点串成了一条完整的价值链:

  1. 后端:我们使用 API 路由 创建了健壮的、符合 RESTful 风格的 API 来管理我们的数据资源。
  2. 前端 - 初始加载:我们使用 getServerSideProps 在服务器端获取初始数据,实现了快速的首屏加载和良好的 SEO 基础。
  3. 前端 - 动态交互:我们使用 CSR 模式,并借助 SWR 这样的现代化数据获取库,实现了高效、乐观的 UI 更新,提供了卓越的用户交互体验。

这个“SSR + CSR with SWR”的组合拳,是构建现代、高性能 Next.js 应用的黄金范式。它充分利用了 Next.js 在服务端和客户端的各自优势。

现在你已经具备了构建全栈应用的基础能力。在接下来的文章中,我们将继续深入 Next.js 的高级特性,比如如何使用 next/image 来优化应用的图片性能,让我们的应用不仅功能强大,而且速度飞快。敬请期待!


文章转载自:

http://fEgu4xCN.xbgnk.cn
http://aMrVaMGc.xbgnk.cn
http://R86gILf6.xbgnk.cn
http://YiiT3TWT.xbgnk.cn
http://wZYShjIP.xbgnk.cn
http://aqFr3aU8.xbgnk.cn
http://VgIEZnty.xbgnk.cn
http://GmIJcQ6W.xbgnk.cn
http://p4vUC3Qx.xbgnk.cn
http://Jw7Rz6dF.xbgnk.cn
http://72cidIuN.xbgnk.cn
http://4UvOGE4s.xbgnk.cn
http://c4n5u72A.xbgnk.cn
http://NHElDkQ2.xbgnk.cn
http://eEtErmH7.xbgnk.cn
http://ifDwNPF1.xbgnk.cn
http://b19phCgk.xbgnk.cn
http://u7P2ah1O.xbgnk.cn
http://In4RaTlf.xbgnk.cn
http://BbP2ytI3.xbgnk.cn
http://AwJuAcGa.xbgnk.cn
http://Yp5N1OjZ.xbgnk.cn
http://h4FTottj.xbgnk.cn
http://lfQ7orAv.xbgnk.cn
http://1eZ0p0gp.xbgnk.cn
http://1AEHp5vz.xbgnk.cn
http://CFNVvSjv.xbgnk.cn
http://I4ILWqu6.xbgnk.cn
http://899nRPjj.xbgnk.cn
http://QYvjkMhV.xbgnk.cn
http://www.dtcms.com/a/377964.html

相关文章:

  • 如何通过url打开本地文件文件夹
  • Swagger隐藏入参中属性字段
  • JavaEE--8.网络编程
  • linux系统搭建nacos集群,并通过nginx实现负载均衡
  • 论文阅读:openai 2025 Why Language Models Hallucinate
  • Rail开发日志_9
  • opencv特征检测
  • 科普:环境隔离的工具:虚拟环境与容器Docker
  • 小迪安全v2023学习笔记(八十一讲)—— 框架安全ThinkPHPLaravelStruts2SpringBootCVE复现
  • ubuntu22.04 安装Docker
  • OpenCV 开发 -- 图像阈值处理
  • [Ubuntu][mount]ubuntu电脑挂载新硬盘
  • Maven中optional的作用
  • 使用pdfjs-dist 预览pdf,并添加文本层的实现
  • 操作系统应用开发(五)智能浏览器开发——东方仙盟元婴期
  • 蓝桥杯算法之基础知识(7)---排序题的快排和归并排序
  • leetcode-python-2154将找到的值乘以 2
  • Nginx 实战系列(十)—— LVS+Keepalived 高可用集群技术详解
  • C++ 前缀积 高频笔试考点 实用技巧 力扣 238.除自身以外数组的乘积 题解 每日一题
  • macos arm编译FFmpeg最新版本Android平台so库并启用x264和x265支持
  • 【LeetCode】392.判断子序列
  • StreamCap(直播录制) v1.0.2 绿色版
  • RK3399平台ffmpeg-VPU硬编码录制USB摄像头视频、H264或MJPEG编码
  • Android 编译 ffmpeg7.1.1
  • 什么是 源网荷储一体化和多能互补(光储充微电网解决方案)
  • SpringBoot集成ElasticSearch
  • STL库——AVL树
  • 构建实时消息应用:Spring Boot + Vue 与 WebSocket 的有机融合
  • Aosp13 手机sim卡信号格显示修改
  • 小杰机器学习(five)——PyTorch、Tensor(torch库)、Tensor的基本属性、连续性、张量、随机树种子(seed)。