今天的学习让我对Next.js和typescript有了更深入的理解,尤其是客户端渲染(CSR)和服务端渲染(SSR)的区别,以及如何在实际项目中灵活运用它们。作为一个前端初学者,这种"恍然大悟"的感觉真的很棒!
初识CSR与SSR的奥秘
在传统的React应用中,我们通常使用客户端渲染(CSR)。这意味着浏览器先下载一个几乎空的HTML页面,然后JavaScript再运行,从API获取数据并渲染内容。就像我写的Todo List应用那样:
"use client";
// 事件监听、生命周期等
import { useState, useEffect } from 'react';
// ... 其他导入
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: Todo[] = await response.json();
setTodos(data);
}
// 组件挂载时获取数据
useEffect(() => {
fetchTodos();
}, [])
// ... 其他代码
}
这里的关键是useEffect钩子和useState。当组件首次渲染时,todos数组是空的,然后useEffect执行,从API获取数据并更新状态,触发重新渲染。这就是典型的CSR模式 - 数据获取和渲染都在浏览器中完成。
CSR的好处是交互体验流畅,一旦页面加载完成,后续操作几乎无需整页刷新。但缺点也很明显:初始加载慢,对SEO不友好,因为搜索引擎爬虫可能无法等待JavaScript执行完成。
而服务端渲染(SSR)则不同,就像我的Repos页面:
export default async function ReposPage() {
// 在服务器端获取数据
const response = await fetch('https://api.github.com/users/dwk-lzd/repos')
const repos: Repo[] = await response.json()
return (
<main className="container mx-auto p-4">
<h1 className="text-2xl font-bold mb-4">GitHub Repositories</h1>
{/* 渲染仓库列表 */}
</main>
)
}
注意这个组件是async的,并且直接在组件体内获取数据!这意味着数据获取发生在服务器上,完整的HTML已经包含了所有内容,直接发送给浏览器。这对SEO非常友好,因为搜索引擎爬虫看到的是完整的内容。
探索shadcn/ui的魅力
今天我第一次使用了shadcn/ui这个组件库,真的很酷!它与众不同之处在于"按需安装"的理念。传统UI库需要一次性安装整个包,即使用不到所有组件,也会增加打包体积。
而shadcn/ui让我可以这样使用:
npx shadcn@latest add button input card
只添加我需要的组件!这太符合实际开发需求了,因为我们往往只需要少数几个组件,而不是整个庞大的库。
看看我是如何使用这些组件的:
import { Button } from "@/components/ui/button";
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
// 在组件中使用
<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="Add new todo..."
onKeyPress={(e) => e.key === 'Enter' && addTodo()}
/>
<Button onClick={addTodo}>Add</Button>
</div>
{/* ... 其他内容 */}
</CardContent>
</Card>
这些组件不仅外观现代,而且API设计也很合理。比如Button组件有不同的variant属性值,如destructive表示破坏性操作:
<Button
variant="destructive"
size="sm"
onClick={() => deleteTodo(todo.id)}
>Delete</Button>
这种设计让代码更加语义化,一看就知道这个按钮是用于删除操作的。
Next.js的路由魔法
Next.js的路由系统真的很智能!它采用了"文件系统即路由"的概念,这意味着我不需要手动配置路由表,只需要按照特定规则创建文件和文件夹即可。而在传统的react开发中我们需要安装react-router-dom来搭建路由
在我的项目中,app目录下的结构决定了路由:
page.tsx→ 对应的页面组件- 文件夹名称 → 路由路径
例如,我的Todo应用在根路径/,而Repos页面在/repos路径。Next.js自动处理了这一切,太方便了!
布局方面,Next.js提供了layout.tsx文件来定义共享的布局结构。虽然今天没有直接使用,但我知道它可以用来包装页面,提供一致的导航、页脚等元素。
Approuter自动配置路由,文件夹就是路由
RESTful API设计实践
今天最大的收获之一是理解了RESTful API的设计理念。RESTful是一种架构风格,核心思想是"一切皆资源",通过HTTP方法对资源进行操作。
在我的Todo应用中,API设计遵循了REST原则:
// 获取所有todos - GET /api/todos
const response = await fetch('/api/todos');
// 创建新todo - POST /api/todos
await fetch('/api/todos', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: newTodo })
})
// 更新todo - PUT /api/todos
await fetch('api/todos', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id, completed })
})
// 删除todo - DELETE /api/todos
await fetch('api/todos', {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id })
})
注意这些操作都针对同一个URL端点/api/todos,但使用不同的HTTP方法区分操作类型。这就是RESTful的精髓!
在服务器端,我实现了相应的处理逻辑:
import { NextResponse } from 'next/server'
import { type Todo } from '@/app/types/todo'
// 模拟数据
let todos: Todo[] = [
{ id: 1, title: '去广州', completed: false },
{ id: 2, title: '小蛮腰', completed: false }
]
// GET - 获取所有todos
export async function GET() {
return NextResponse.json(todos)
}
// POST - 创建新todo
export async function POST(request: Request) {
const data = await request.json()
const newTodo = {
id: +Date.now(), // 使用时间戳作为ID
title: data.title,
completed: false
}
todos.push(newTodo)
return NextResponse.json(newTodo)
}
// PUT - 更新todo
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 - 删除todo
export async function DELETE(request: Request) {
const data = await request.json()
todos = todos.filter(todo => todo.id !== data.id)
return NextResponse.json(todos)
}
这种设计让API变得直观且易于理解。每个端点对应一种资源(这里是todos),通过HTTP方法表达操作意图。
TypeScript的类型安全
TypeScript在今天的学习中发挥了重要作用。它通过类型注解让代码更加可靠和自文档化。
我定义了两个接口来描述数据结构:
// todo.ts
export interface Todo {
id: number
title: string
completed: boolean
}
// repos.ts
export interface Repo {
id: number;
name: string;
description: string | null; // 联合类型
html_url: string;
stargazers_count: number;
language: string | null
}
这些接口不仅提供了类型检查,还作为代码文档,清楚地说明了数据的结构。比如Repo接口中的description和language字段被定义为string | null,表示这些字段可能是字符串,也可能是null值。
在组件中使用这些类型时,TypeScript提供了智能提示和错误检查:
const [todos, setTodos] = useState<Todo[]>([]); // 指定状态类型为Todo数组
// TypeScript会检查传入的数据是否符合Todo接口
todos.map((todo: Todo) => ( /* ... */ ))
这种类型安全大大减少了运行时错误,提高了开发效率。
UI与交互设计
在构建用户界面时,我注重了以下几点:
- 清晰的视觉层次:使用Card组件包装内容,形成明确的视觉分组
- 直观的操作反馈:复选框直观显示完成状态,删除按钮使用醒目的destructive变体
- 便捷的交互方式:输入框支持回车键提交,减少鼠标操作
<div className="space-y-2">
{todos.map((todo: 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) => tooggleTodo(todo.id, e.target.checked)}
className="w-4 h-4"
/>
<span className={todo.completed ? 'line-through' : ''}>
{todo.title}
</span>
</div>
<Button
variant="destructive"
size="sm"
onClick={() => deleteTodo(todo.id)}
>Delete</Button>
</div>
))}
</div>
这里使用了Tailwind CSS的实用类来设置样式,如space-y-2为子元素添加垂直间距,flex创建弹性布局,items-center实现垂直居中等。
总结与展望
通过今天的学习,我掌握了Next.js的核心概念,包括CSR与SSR的区别、App Router的路由系统、API路由的设计,以及如何结合shadcn/ui构建现代Web界面。
这些知识为我打下了坚实的基础。Next.js的全栈能力让我可以在一个项目中同时处理前端UI和后端API,大大提高了开发效率。TypeScript的类型系统让代码更加可靠,减少了潜在的错误。
虽然今天的项目比较简单,但涵盖了许多现代Web开发的核心概念。接下来,我可以进一步探索更复杂的主题,如状态管理、身份认证、性能优化等。