从零开始用 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 添加了静态类型系统,带来三大优势:
- 类型约束:编译时发现错误,减少运行时bug
- 代码提示:IDE 智能补全,提高开发效率
- 可维护性:类型即文档,便于团队协作
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
项目结构说明:
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
第三步:定义数据类型
在 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 项目,我们体验了 Next.js 的全栈开发能力:
- 前后端一体化:在同一个项目中完成 API 和前端界面的开发
- 类型安全:使用 TypeScript 确保代码质量
- 现代化 UI:使用 shadcn/ui 构建美观的界面
- RESTful API:遵循标准的接口设计规范
Next.js 的强大之处在于它提供了一套完整的解决方案,让开发者可以专注于业务逻辑,而不是繁琐的配置。无论是个人项目还是企业级应用,Next.js 都能提供出色的开发体验和性能表现。
下一步学习建议
如果你想进一步深入学习 Next.js,建议:
- 学习数据获取:了解 SWR、TanStack Query 等数据获取库
- 探索数据库集成:学习如何连接 PostgreSQL、MySQL 等数据库
- 掌握身份验证:实现用户登录注册功能
- 部署上线:学习如何将 Next.js 应用部署到 Vercel、Netlify 等平台
希望本文能帮助你入门 Next.js,开启全栈开发之旅!
提示:本文所有代码已上传至 GitHub 仓库,欢迎 Star 和 Fork!