用Next.js 构建一个简单的 CRUD 应用:集成 API 路由和数据获取

发布于:2025-09-12 ⋅ 阅读:(18) ⋅ 点赞:(0)

用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':
      // 获取所有 todos
      res.status(200).json(todos);
      break;
    case 'POST':
      // 创建一个新的 todo
      const { 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':
      // 删除一个 todo
      todos.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;

    // 乐观更新 UI
    const 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}>
        <input
          type="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 来优化应用的图片性能,让我们的应用不仅功能强大,而且速度飞快。敬请期待!


网站公告

今日签到

点亮在社区的每一天
去签到