从零开始用 Next.js 构建 Todo 应用:全栈开发初体验

713 阅读6分钟

从零开始用 Next.js 构建 Todo 应用:全栈开发初体验

本文通过一个完整的 Todo 项目,带你领略 Next.js 的全栈开发魅力,体验现代 Web 开发的最佳实践。

引言:为什么选择 Next.js?

在移动互联网时代,Web 应用的开发方式发生了巨大变化。传统的 CSR(客户端渲染)单页应用虽然用户体验流畅,但也存在一些痛点:

  • SEO 不友好:搜索引擎难以抓取动态渲染的内容
  • 首屏加载慢:需要等待 JavaScript 下载和执行后才能显示内容

搜索引擎难以抓取 CSR 应用的内容,是因为初始 HTML 是空的,而执行 JS 渲染内容的过程耗时、不可靠,且很多爬虫根本不执行 JS,导致页面内容无法被正确索引。

这时候,Next.js 闪亮登场!它提供了服务端渲染(SSR) 能力,让组件在服务器端渲染完成后再发送给客户端,完美解决了上述问题:

  • 🚀 页面渲染更快:用户无需等待 JS 执行即可看到内容
  • 🔍 SEO 更友好:搜索引擎可以直接抓取已渲染的 HTML
  • 🌐 全栈能力:前后端一体化开发,无需单独配置服务器

SSR 在服务器端直接生成完整的 HTML,搜索引擎爬虫抓取时无需执行 JS 即可看到完整页面内容,因此更易索引、更高效可靠。

特别是对于需要搜索引擎优化的项目(如内容网站、电商平台)和AI 出海项目(指面向国际市场的 AI 相关产品),Next.js 的 SSR 特性简直是福音!

Next.js 的核心特点

1. 全栈开发能力

Next.js 不只是前端框架,它让你能够在一个项目中完成前后端开发。通过 API 路由,你可以直接编写后端逻辑,无需额外配置服务器。

2. 约定优于配置

Next.js 采用"约定优于配置"的理念,减少开发者的决策负担:

  • app 目录即路由app/todos/page.tsx 自动对应 /todos 路由
  • API 路由app/api/todos/route.ts 自动创建 API 端点
  • 内置 TypeScript 支持:提供完善的类型系统

3. App Router 技术

Next.js 13+ 引入了全新的 App Router,基于 React Server Components 构建,提供了:

  • 布局系统:易于共享的页面布局
  • 流式渲染:逐步渲染页面内容
  • Suspense 集成:更精细的加载状态控制

技术栈介绍

TypeScript:JavaScript 的超集

TypeScript 为 JavaScript 添加了静态类型系统,带来三大优势:

  1. 类型约束:编译时发现错误,减少运行时bug
  2. 代码提示:IDE 智能补全,提高开发效率
  3. 可维护性:类型即文档,便于团队协作

shadcn/ui:现代化的组件库

与 React Vant 等传统组件库不同,shadcn/ui 采用了全新的理念:

  • 按需安装:只安装你需要的组件,减少包体积
  • 代码可控:组件代码直接放入项目,可完全自定义
  • Tailwind CSS:基于实用优先的 CSS 框架

RESTful API:资源导向的接口设计

RESTful 是一种基于 HTTP 协议的架构风格,核心思想是:

  • 资源导向:一切皆资源,通过 URL 标识
  • HTTP 动词:GET(获取)、POST(创建)、PUT(更新)、DELETE(删除)
  • 无状态:每个请求包含所有必要信息

实战:构建 Todo 应用

接下来,我们一步步构建一个功能完整的 Todo 应用。

第一步:项目初始化

首先创建 Next.js 项目:

npx create-next-app@latest next-todos

配置选项如下:

  • TypeScript: ✅ Yes
  • ESLint: ✅ Yes
  • Tailwind CSS: ✅ Yes
  • src/ directory: ✅ Yes
  • App Router: ✅ Yes
  • Turbopack: ❌ No
  • Import alias: ✅ Yes

创建Next.js项目

项目结构说明:

  • app/:应用主要代码
  • components/:可复用UI组件
  • lib/:工具函数和配置
  • public/:静态资源
  • types/:TypeScript类型定义

第二步:安装 shadcn/ui

初始化 shadcn/ui:

npx shadcn@latest init

按需安装所需组件:

npx shadcn@latest add button
npx shadcn@latest add input
npx shadcn@latest add card

安装shadcn/ui组件

第三步:定义数据类型

types/todo.ts 中定义 Todo 类型:

export interface Todo {
  id: number;
  text: string;
  completed: boolean;
}

第四步:创建 API 路由

app/api/todos/route.ts 中实现 RESTful API:

import { NextResponse } from 'next/server';
import { type Todo } from '@/app/types/todo';

// 模拟数据库
let todos: Todo[] = [
  { id: 1, text: '学习 Next.js', completed: false },
  { id: 2, text: '掌握 TypeScript', completed: true },
  { id: 3, text: '了解 shadcn/ui', completed: false },
];

// GET /api/todos
export async function GET() {
  return NextResponse.json(todos);
}

// POST /api/todos
export async function POST(request: Request) {
  const data = await request.json();
  
  const newTodo: Todo = {
    id: +Date.now(),
    text: data.text,
    completed: false
  };
  
  todos.push(newTodo);
  return NextResponse.json(newTodo);
}

// PUT /api/todos
export async function PUT(request: Request) {
  const data = await request.json();
  
  todos = todos.map(todo =>
    todo.id === data.id ? { ...todo, completed: data.completed } : todo
  );
  
  return NextResponse.json(todos);
}

// DELETE /api/todos
export async function DELETE(request: Request) {
  const data = await request.json();
  
  todos = todos.filter(todo => todo.id !== data.id);
  return NextResponse.json(todos);
}

第五步:创建主页面

app/page.tsx 中创建 Todo 应用界面:

"use client";

import { useState, useEffect } from 'react';
import { Button } from "@/components/ui/button";
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { type Todo } from '@/app/types/todo';

export default function Home() {
  const [newTodo, setNewTodo] = useState("");
  const [todos, setTodos] = useState<Todo[]>([]);

  // 获取 Todos
  const fetchTodos = async () => {
    const response = await fetch('/api/todos');
    const data = await response.json();
    setTodos(data);
  };

  // 添加 Todo
  const addTodo = async () => {
    if (!newTodo.trim()) return;

    await fetch('/api/todos', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ text: newTodo })
    });

    setNewTodo("");
    fetchTodos();
  };

  // 切换 Todo 状态
  const toggleTodo = async (id: number, completed: boolean) => {
    await fetch('/api/todos', {
      method: 'PUT',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ id, completed })
    });
    
    fetchTodos();
  };

  // 删除 Todo
  const deleteTodo = async (id: number) => {
    await fetch('/api/todos', {
      method: 'DELETE',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ id })
    });
    
    fetchTodos();
  };

  // 组件挂载时获取 Todos
  useEffect(() => {
    fetchTodos();
  }, []);

  return (
    <main className="container mx-auto p-4 max-w-md">
      <Card>
        <CardHeader>
          <CardTitle>Todo List</CardTitle>
        </CardHeader>
        <CardContent>
          <div className="flex gap-2 mb-4">
            <Input
              value={newTodo}
              onChange={e => setNewTodo(e.target.value)}
              placeholder="添加新任务..."
              onKeyPress={e => e.key === 'Enter' && addTodo()}
            />
            <Button onClick={addTodo}>添加</Button>
          </div>

          <div className="space-y-2">
            {todos.map((todo) => (
              <div
                key={todo.id}
                className="flex items-center justify-between p-2 border rounded"
              >
                <div className="flex items-center gap-2">
                  <input
                    type="checkbox"
                    checked={todo.completed}
                    onChange={e => toggleTodo(todo.id, e.target.checked)}
                    className="w-4 h-4"
                  />
                  <span className={todo.completed ? 'line-through' : ''}>
                    {todo.text}
                  </span>
                </div>
                <Button
                  variant="destructive"
                  size="sm"
                  onClick={() => deleteTodo(todo.id)}
                >
                  删除
                </Button>
              </div>
            ))}
          </div>
        </CardContent>
      </Card>
    </main>
  );
}

第六步:运行项目

启动开发服务器:

npm run dev

访问 http://localhost:3000 即可看到你的 Todo 应用!

Todo应用界面

项目亮点与总结

通过这个简单的 Todo 项目,我们体验了 Next.js 的全栈开发能力:

  1. 前后端一体化:在同一个项目中完成 API 和前端界面的开发
  2. 类型安全:使用 TypeScript 确保代码质量
  3. 现代化 UI:使用 shadcn/ui 构建美观的界面
  4. RESTful API:遵循标准的接口设计规范

Next.js 的强大之处在于它提供了一套完整的解决方案,让开发者可以专注于业务逻辑,而不是繁琐的配置。无论是个人项目还是企业级应用,Next.js 都能提供出色的开发体验和性能表现。

下一步学习建议

如果你想进一步深入学习 Next.js,建议:

  1. 学习数据获取:了解 SWR、TanStack Query 等数据获取库
  2. 探索数据库集成:学习如何连接 PostgreSQL、MySQL 等数据库
  3. 掌握身份验证:实现用户登录注册功能
  4. 部署上线:学习如何将 Next.js 应用部署到 Vercel、Netlify 等平台

希望本文能帮助你入门 Next.js,开启全栈开发之旅!

提示:本文所有代码已上传至 GitHub 仓库,欢迎 Star 和 Fork!