Next.js 学习指南
Next.js 是一个基于 React 的全栈 Web 开发框架,提供了服务端渲染、静态站点生成、API 路由等功能,让 React 应用开发更加高效和强大。
🚀 快速开始
创建 Next.js 项目
# 使用 create-next-app
npx create-next-app@latest my-next-app
# 使用 TypeScript
npx create-next-app@latest my-next-app --typescript
# 使用 Tailwind CSS
npx create-next-app@latest my-next-app --tailwind
# 手动安装
npm install next react react-dom基本项目结构
my-next-app/
├── pages/ # 页面目录
│ ├── api/ # API 路由
│ ├── _app.js # 应用入口
│ ├── _document.js # 文档配置
│ └── index.js # 首页
├── components/ # 组件目录
├── public/ # 静态资源
├── styles/ # 样式文件
├── next.config.js # Next.js 配置
└── package.json📚 核心概念
页面路由
Next.js 使用基于文件系统的路由:
// pages/index.js - 首页 (/)
export default function Home() {
return <h1>Welcome to Next.js!</h1>;
}
// pages/about.js - 关于页面 (/about)
export default function About() {
return <h1>About Us</h1>;
}
// pages/blog/[id].js - 动态路由 (/blog/1, /blog/2)
export default function BlogPost({ post }) {
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
);
}
// pages/blog/[category]/[id].js - 嵌套动态路由
export default function CategoryBlogPost({ post }) {
return (
<article>
<h1>{post.title}</h1>
<p>Category: {post.category}</p>
</article>
);
}数据获取
getStaticProps (SSG)
// pages/posts/[id].js
export default function Post({ post }) {
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
);
}
export async function getStaticProps({ params }) {
const post = await fetchPost(params.id);
return {
props: {
post
},
revalidate: 60 // ISR: 60秒后重新生成
};
}
export async function getStaticPaths() {
const posts = await fetchPosts();
const paths = posts.map((post) => ({
params: { id: post.id.toString() }
}));
return {
paths,
fallback: 'blocking' // 或 false, true
};
}getServerSideProps (SSR)
// pages/profile.js
export default function Profile({ user }) {
return (
<div>
<h1>Profile</h1>
<p>Name: {user.name}</p>
<p>Email: {user.email}</p>
</div>
);
}
export async function getServerSideProps(context) {
const { req, res, query } = context;
// 检查认证
const token = req.cookies.token;
if (!token) {
return {
redirect: {
destination: '/login',
permanent: false
}
};
}
// 获取用户数据
const user = await fetchUser(token);
return {
props: {
user
}
};
}客户端数据获取
import { useState, useEffect } from 'react';
export default function Posts() {
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function fetchPosts() {
try {
const response = await fetch('/api/posts');
const data = await response.json();
setPosts(data);
} catch (error) {
console.error('Error fetching posts:', error);
} finally {
setLoading(false);
}
}
fetchPosts();
}, []);
if (loading) return <div>Loading...</div>;
return (
<div>
{posts.map(post => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</article>
))}
</div>
);
}⚡ API 路由
基本 API 路由
// pages/api/posts.js
export default function handler(req, res) {
const { method } = req;
switch (method) {
case 'GET':
return getPosts(req, res);
case 'POST':
return createPost(req, res);
default:
res.setHeader('Allow', ['GET', 'POST']);
res.status(405).end(`Method ${method} Not Allowed`);
}
}
async function getPosts(req, res) {
try {
const posts = await fetchPostsFromDB();
res.status(200).json(posts);
} catch (error) {
res.status(500).json({ error: 'Failed to fetch posts' });
}
}
async function createPost(req, res) {
try {
const { title, content } = req.body;
const post = await createPostInDB({ title, content });
res.status(201).json(post);
} catch (error) {
res.status(500).json({ error: 'Failed to create post' });
}
}动态 API 路由
// pages/api/posts/[id].js
export default function handler(req, res) {
const { method } = req;
const { id } = req.query;
switch (method) {
case 'GET':
return getPost(req, res, id);
case 'PUT':
return updatePost(req, res, id);
case 'DELETE':
return deletePost(req, res, id);
default:
res.setHeader('Allow', ['GET', 'PUT', 'DELETE']);
res.status(405).end(`Method ${method} Not Allowed`);
}
}
async function getPost(req, res, id) {
try {
const post = await fetchPostFromDB(id);
if (!post) {
return res.status(404).json({ error: 'Post not found' });
}
res.status(200).json(post);
} catch (error) {
res.status(500).json({ error: 'Failed to fetch post' });
}
}中间件
// middleware.js
import { NextResponse } from 'next/server';
export function middleware(request) {
// 检查认证
const token = request.cookies.get('token');
if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url));
}
// 添加自定义头部
const response = NextResponse.next();
response.headers.set('x-custom-header', 'my-value');
return response;
}
export const config = {
matcher: [
'/dashboard/:path*',
'/api/:path*'
]
};🎯 组件和布局
自定义 App 组件
// pages/_app.js
import Layout from '../components/Layout';
import '../styles/globals.css';
export default function MyApp({ Component, pageProps }) {
return (
<Layout>
<Component {...pageProps} />
</Layout>
);
}自定义 Document
// pages/_document.js
import { Html, Head, Main, NextScript } from 'next/document';
export default function Document() {
return (
<Html lang="zh-CN">
<Head>
<link rel="icon" href="/favicon.ico" />
<meta name="description" content="My Next.js app" />
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
);
}布局组件
// components/Layout.js
import Header from './Header';
import Footer from './Footer';
export default function Layout({ children }) {
return (
<div className="min-h-screen flex flex-col">
<Header />
<main className="flex-grow">
{children}
</main>
<Footer />
</div>
);
}🔧 样式解决方案
CSS Modules
/* styles/Button.module.css */
.button {
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
}
.primary {
background-color: #0070f3;
color: white;
}
.secondary {
background-color: #f3f4f6;
color: #374151;
}// components/Button.js
import styles from '../styles/Button.module.css';
export default function Button({ children, variant = 'primary', ...props }) {
return (
<button
className={`${styles.button} ${styles[variant]}`}
{...props}
>
{children}
</button>
);
}Tailwind CSS
// components/Card.js
export default function Card({ title, content, image }) {
return (
<div className="max-w-sm rounded-lg shadow-lg bg-white overflow-hidden">
{image && (
<img
className="w-full h-48 object-cover"
src={image}
alt={title}
/>
)}
<div className="p-6">
<h3 className="text-xl font-semibold text-gray-900 mb-2">
{title}
</h3>
<p className="text-gray-600">
{content}
</p>
</div>
</div>
);
}Styled Components
// components/StyledButton.js
import styled from 'styled-components';
const StyledButton = styled.button`
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
background-color: ${props => props.variant === 'primary' ? '#0070f3' : '#f3f4f6'};
color: ${props => props.variant === 'primary' ? 'white' : '#374151'};
&:hover {
opacity: 0.8;
}
`;
export default function StyledButton({ children, variant = 'primary', ...props }) {
return (
<StyledButton variant={variant} {...props}>
{children}
</StyledButton>
);
}📦 状态管理
Context API
// contexts/AuthContext.js
import { createContext, useContext, useState } from 'react';
const AuthContext = createContext();
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(false);
const login = async (credentials) => {
setLoading(true);
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credentials)
});
const data = await response.json();
setUser(data.user);
} catch (error) {
console.error('Login failed:', error);
} finally {
setLoading(false);
}
};
const logout = () => {
setUser(null);
};
return (
<AuthContext.Provider value={{ user, loading, login, logout }}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within AuthProvider');
}
return context;
}SWR 数据获取
// hooks/usePosts.js
import useSWR from 'swr';
const fetcher = url => fetch(url).then(res => res.json());
export function usePosts() {
const { data, error, mutate } = useSWR('/api/posts', fetcher);
return {
posts: data,
isLoading: !error && !data,
isError: error,
mutate
};
}
// 在组件中使用
export default function Posts() {
const { posts, isLoading, isError } = usePosts();
if (isLoading) return <div>Loading...</div>;
if (isError) return <div>Error loading posts</div>;
return (
<div>
{posts.map(post => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</article>
))}
</div>
);
}🛠️ 配置和优化
Next.js 配置
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
// 图片优化
images: {
domains: ['example.com', 'cdn.example.com'],
formats: ['image/webp', 'image/avif']
},
// 环境变量
env: {
CUSTOM_KEY: process.env.CUSTOM_KEY
},
// 重定向
async redirects() {
return [
{
source: '/old-blog/:slug',
destination: '/blog/:slug',
permanent: true
}
];
},
// 重写
async rewrites() {
return [
{
source: '/api/:path*',
destination: 'https://api.example.com/:path*'
}
];
},
// 国际化
i18n: {
locales: ['en', 'zh'],
defaultLocale: 'en'
}
};
module.exports = nextConfig;性能优化
// 动态导入
import dynamic from 'next/dynamic';
const DynamicComponent = dynamic(() => import('../components/HeavyComponent'), {
loading: () => <p>Loading...</p>,
ssr: false // 禁用服务端渲染
});
// 图片优化
import Image from 'next/image';
export default function OptimizedImage() {
return (
<Image
src="/hero.jpg"
alt="Hero image"
width={1200}
height={600}
priority
placeholder="blur"
blurDataURL="data:image/jpeg;base64,..."
/>
);
}
// 字体优化
import { Inter } from 'next/font/google';
const inter = Inter({ subsets: ['latin'] });
export default function Layout({ children }) {
return (
<div className={inter.className}>
{children}
</div>
);
}📖 学习资源
官方文档
实用工具和库
- SWR: React Hooks 数据获取库
- React Query: 强大的数据同步库
- NextAuth.js: 身份验证解决方案
- Prisma: 数据库 ORM
- Vercel: 部署平台
🔍 调试工具
- Next.js DevTools: 内置开发工具
- React DevTools: React 组件调试
- Vercel Analytics: 性能监控
最后更新: 2024年
最近更新:12/9/2025, 2:17:55 AM