外观
Next.js 笔记
约 2619 字大约 9 分钟
2025-08-22
一、Next.js 基础
1.1 什么是 Next.js?
Next.js 是一个基于 React 的开源框架,用于构建服务端渲染(SSR)、静态生成(SSG)和混合渲染的 Web 应用程序。它由 Vercel 公司开发和维护,提供了开箱即用的优化功能,使开发者能够专注于业务逻辑而非配置。
Next.js 的核心优势:
- 自动代码分割:按页面分割代码,只加载必要的资源
- 服务端渲染(SSR):提升 SEO 和首屏加载速度
- 静态站点生成(SSG):预渲染页面,实现即时加载
- 增量静态再生(ISR):无需重新构建即可更新静态内容
- 内置 CSS 和 Sass 支持:无需额外配置
- API 路由:在同一个项目中编写前端和后端代码
- TypeScript 支持:开箱即用的 TypeScript 集成
1.2 为什么选择 Next.js?
- 更好的 SEO:服务端渲染使搜索引擎更容易抓取内容
- 更快的首屏加载:预渲染的 HTML 立即可用
- 简化复杂性:内置路由、代码分割等,无需手动配置
- 灵活的渲染策略:根据页面需求选择 SSG、SSR 或客户端渲染
- 强大的开发者体验:热重载、错误提示、TypeScript 支持等
1.3 创建 Next.js 项目
使用 Create Next App(推荐)
npx create-next-app@latest my-app
# 或
yarn create next-app my-app选择配置选项:
- TypeScript:是/否
- ESLint:是/否
- Tailwind CSS:是/否
src/目录:是/否- App Router:是/否(推荐使用)
- 导入别名:是/否
项目结构概览
my-app/
├── app/ # 基于 App Router 的页面和布局
│ ├── layout.js # 全局布局
│ ├── page.js # 首页
│ ├── dashboard/
│ │ ├── page.js # /dashboard 页面
│ │ └── layout.js # /dashboard 布局
│ └── ...
├── public/ # 静态资源
├── styles/ # 样式文件
├── .env.local # 环境变量
├── next.config.js # Next.js 配置
└── package.json启动开发服务器
npm run dev
# 或
yarn dev二、路由系统
2.1 基于文件系统的路由
App Router(推荐)
- 文件系统即路由:
app/about/page.js会创建/about路由 page.js文件定义页面内容layout.js文件定义布局
// app/about/page.js
export default function AboutPage() {
return <h1>About Page</h1>;
}页面嵌套
app/
├── dashboard/
│ ├── settings/
│ │ └── page.js -> /dashboard/settings
│ └── page.js -> /dashboard
└── page.js -> /2.2 动态路由
单级动态路由
app/
└── users/
└── [id]/
└── page.js -> /users/123// app/users/[id]/page.js
export default function UserPage({ params }) {
return <h1>User ID: {params.id}</h1>;
}多级动态路由
app/
└── articles/
└── [category]/
└── [id]/
└── page.js -> /articles/tech/42// app/articles/[category]/[id]/page.js
export default function ArticlePage({ params }) {
return (
<div>
<h1>Category: {params.category}</h1>
<p>ID: {params.id}</p>
</div>
);
}2.3 路由跳转
Link 组件(客户端导航)
import Link from 'next/link';
export default function Navigation() {
return (
<nav>
<Link href="/">Home</Link>
<Link href="/about">About</Link>
<Link href="/users/123">User 123</Link>
</nav>
);
}useRouter Hook(编程式导航)
'use client'; // 标记为客户端组件
import { useRouter } from 'next/navigation';
export default function Button() {
const router = useRouter();
const goToAbout = () => {
router.push('/about');
};
const goToUser = (id) => {
router.push(`/users/${id}`);
};
const goBack = () => {
router.back();
};
return (
<div>
<button onClick={goToAbout}>Go to About</button>
<button onClick={() => goToUser(123)}>Go to User 123</button>
<button onClick={goBack}>Go Back</button>
</div>
);
}三、数据获取
3.1 服务端数据获取
getStaticProps(静态生成)
// app/page.js
export default function HomePage({ posts }) {
return (
<div>
{posts.map(post => (
<div key={post.id}>
<h2>{post.title}</h2>
<p>{post.body}</p>
</div>
))}
</div>
);
}
// 只在构建时运行
export async function getStaticProps() {
const res = await fetch('https://api.example.com/posts');
const posts = await res.json();
return {
props: {
posts
},
revalidate: 60 // 每 60 秒重新生成(ISR)
};
}getServerSideProps(服务端渲染)
// app/users/page.js
export default function UsersPage({ users }) {
return (
<div>
{users.map(user => (
<div key={user.id}>{user.name}</div>
))}
</div>
);
}
// 每次请求都运行
export async function getServerSideProps() {
const res = await fetch('https://api.example.com/users');
const users = await res.json();
return {
props: {
users
}
};
}3.2 动态路由数据获取
getStaticPaths + getStaticProps
// app/users/[id]/page.js
export default function UserPage({ user }) {
return (
<div>
<h1>{user.name}</h1>
<p>Email: {user.email}</p>
</div>
);
}
// 生成所有可能的路径
export async function getStaticPaths() {
const res = await fetch('https://api.example.com/users');
const users = await res.json();
const paths = users.map(user => ({
params: { id: user.id.toString() }
}));
return { paths, fallback: 'blocking' };
}
// 获取特定用户数据
export async function getStaticProps({ params }) {
const res = await fetch(`https://api.example.com/users/${params.id}`);
const user = await res.json();
return { props: { user } };
}fallback 选项:
false:只生成 getStaticPaths 返回的路径true:生成 getStaticPaths 返回的路径,其他路径在首次请求时生成'blocking':同 true,但不会显示加载状态
3.3 客户端数据获取
使用 SWR(推荐)
'use client'; // 标记为客户端组件
import useSWR from 'swr';
const fetcher = (url) => fetch(url).then(res => res.json());
export default function Profile() {
const { data, error, isLoading } = useSWR('/api/user', fetcher);
if (error) return <div>Failed to load</div>;
if (isLoading) return <div>Loading...</div>;
if (!data) return null;
return <div>Hello, {data.name}!</div>;
}使用 useEffect
'use client';
import { useState, useEffect } from 'react';
export default function UserList() {
const [users, setUsers] = useState([]);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
async function fetchData() {
const res = await fetch('https://api.example.com/users');
const data = await res.json();
setUsers(data);
setIsLoading(false);
}
fetchData();
}, []);
if (isLoading) return <div>Loading...</div>;
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}四、页面与布局
4.1 页面组件
基本页面结构
// app/page.js
export default function HomePage() {
return (
<main>
<h1>Welcome to Next.js</h1>
<p>This is the home page.</p>
</main>
);
}元数据设置
// app/page.js
export const metadata = {
title: 'Home Page',
description: 'Welcome to our website',
};4.2 布局组件
全局布局
// app/layout.js
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
<header>
<nav>
<Link href="/">Home</Link>
<Link href="/about">About</Link>
</nav>
</header>
<main>{children}</main>
<footer>© 2023 My Website</footer>
</body>
</html>
);
}嵌套路由布局
app/
├── layout.js # 全局布局
├── page.js # 首页
└── dashboard/
├── layout.js # 仪表盘布局
└── page.js # 仪表盘页面// app/dashboard/layout.js
export default function DashboardLayout({ children }) {
return (
<div className="dashboard-layout">
<aside>
<ul>
<li><Link href="/dashboard">Overview</Link></li>
<li><Link href="/dashboard/settings">Settings</Link></li>
</ul>
</aside>
<div className="dashboard-content">
{children}
</div>
</div>
);
}布局嵌套执行顺序:
- 全局布局 (
app/layout.js) - 仪表盘布局 (
app/dashboard/layout.js) - 仪表盘页面 (
app/dashboard/page.js)
4.3 模板与片段
模板 (Template)
- 类似布局,但每次导航时会重新渲染
- 适用于需要在导航时重置状态的场景
// app/dashboard/template.js
export default function DashboardTemplate({ children }) {
return (
<div>
<h1>Dashboard</h1>
{children}
</div>
);
}片段 (Segment)
- 用于创建可复用的 UI 片段
- 通常用于模态框、侧边栏等
// app/dashboard/modal/[[...slug]]/page.js
export default function Modal({ params }) {
return (
<div className="modal-overlay">
<div className="modal-content">
{/* 模态框内容 */}
</div>
</div>
);
}五、样式处理
5.1 CSS Modules
创建 CSS Module
/* components/Button.module.css */
.primary {
background-color: #0070f3;
color: white;
padding: 8px 16px;
border-radius: 4px;
}
.secondary {
background-color: #f4f4f4;
color: #333;
padding: 8px 16px;
border-radius: 4px;
}使用 CSS Module
// components/Button.js
import styles from './Button.module.css';
export default function Button({ variant = 'primary', children }) {
return (
<button className={styles[variant]}>
{children}
</button>
);
}5.2 全局 CSS
创建全局 CSS
/* styles/globals.css */
:root {
--primary-color: #0070f3;
--secondary-color: #1a1a1a;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: system-ui, sans-serif;
line-height: 1.5;
color: var(--secondary-color);
}导入全局 CSS
// app/layout.js
import './globals.css';
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}5.3 Sass 支持
安装 Sass
npm install sass使用 Sass
/* styles/globals.scss */
$primary-color: #0070f3;
$secondary-color: #1a1a1a;
:root {
--primary-color: #{$primary-color};
--secondary-color: #{$secondary-color};
}
body {
font-family: 'Helvetica Neue', Arial, sans-serif;
color: $secondary-color;
a {
color: $primary-color;
&:hover {
text-decoration: underline;
}
}
}5.4 CSS-in-JS(可选)
使用 styled-components
npm install styled-components配置 styled-components
// next.config.js
const nextConfig = {
experimental: {
styledComponents: true,
},
};
module.exports = nextConfig;使用 styled-components
// components/Button.js
import styled from 'styled-components';
const Button = styled.button`
background-color: ${props => props.primary ? '#0070f3' : '#f4f4f4'};
color: ${props => props.primary ? 'white' : '#333'};
padding: 8px 16px;
border-radius: 4px;
border: none;
cursor: pointer;
&:hover {
opacity: 0.9;
}
`;
export default Button;六、API 路由
6.1 创建 API 端点
基本 API 路由
app/
└── api/
└── hello/
└── route.js// app/api/hello/route.js
export async function GET(request) {
return new Response(JSON.stringify({ message: 'Hello World' }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
}访问 URL: http://localhost:3000/api/hello
6.2 处理不同 HTTP 方法
// app/api/users/route.js
export async function GET(request) {
// 获取用户列表
return new Response(JSON.stringify(users), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
}
export async function POST(request) {
// 创建新用户
const body = await request.json();
// 保存用户数据
return new Response(JSON.stringify(newUser), {
status: 201,
headers: { 'Content-Type': 'application/json' },
});
}6.3 动态 API 路由
app/
└── api/
└── users/
└── [id]/
└── route.js// app/api/users/[id]/route.js
export async function GET(request, { params }) {
const { id } = params;
// 获取特定用户
return new Response(JSON.stringify(user), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
}
export async function PUT(request, { params }) {
const { id } = params;
const body = await request.json();
// 更新用户
return new Response(JSON.stringify(updatedUser), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
}
export async function DELETE(request, { params }) {
const { id } = params;
// 删除用户
return new Response(null, { status: 204 });
}6.4 API 路由中间件
// app/api/middleware.js
export function middleware(request) {
const { pathname } = request.nextUrl;
if (pathname.startsWith('/api/protected')) {
const apiKey = request.headers.get('x-api-key');
if (apiKey !== process.env.API_KEY) {
return new Response(JSON.stringify({ error: 'Unauthorized' }), {
status: 401,
headers: { 'Content-Type': 'application/json' },
});
}
}
return NextResponse.next();
}
export const config = {
matcher: '/api/:path*',
};七、图像优化
7.1 Image 组件
基本使用
import Image from 'next/image';
export default function Home() {
return (
<main>
<Image
src="/logo.png"
alt="Logo"
width={500}
height={500}
// 优先加载关键图像
priority
/>
</main>
);
}本地图片
import Image from 'next/image';
import logo from '../public/logo.png';
export default function Home() {
return (
<Image
src={logo}
alt="Logo"
width={500}
height={500}
/>
);
}7.2 优化配置
next.config.js 配置
// next.config.js
module.exports = {
images: {
domains: ['example.com', 'images.unsplash.com'],
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
loader: 'default',
path: '/_next/image',
disableStaticImages: false,
minimumCacheTTL: 60,
formats: ['image/webp'],
dangerouslyAllowSVG: false,
contentSecurityPolicy: "default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline';"
},
};7.3 响应式图像
使用 fill 属性
<div style={{ position: 'relative', width: '100%', height: '500px' }}>
<Image
src="/background.jpg"
alt="Background"
fill
style={{ objectFit: 'cover' }}
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>
</div>使用 srcSet
<Image
src="/logo.png"
alt="Logo"
width={500}
height={500}
srcSet="
/logo-320w.png 320w,
/logo-480w.png 480w,
/logo-800w.png 800w
"
sizes="(max-width: 600px) 100vw,
(max-width: 1200px) 50vw,
33vw"
/>八、环境变量
8.1 配置环境变量
创建 .env.local 文件
# .env.local
NEXT_PUBLIC_API_URL=https://api.example.com
DB_HOST=localhost
DB_USER=root
DB_PASS=secret访问环境变量
// 客户端可访问的变量(以 NEXT_PUBLIC_ 开头)
console.log(process.env.NEXT_PUBLIC_API_URL);
// 服务端可访问的变量
console.log(process.env.DB_HOST);8.2 环境变量验证
安装验证包
npm install joi创建验证文件
// lib/env.js
import Joi from 'joi';
const envSchema = Joi.object({
NODE_ENV: Joi.string().valid('development', 'production', 'test').default('development'),
NEXT_PUBLIC_API_URL: Joi.string().uri().required(),
DB_HOST: Joi.string().default('localhost'),
DB_USER: Joi.string().default('root'),
DB_PASS: Joi.string().required(),
}).unknown();
const { value: env, error } = envSchema.validate(process.env);
if (error) {
throw new Error(`Config validation error: ${error.message}`);
}
export default env;在应用中使用
// app/layout.js
import env from '@/lib/env';
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
{children}
</body>
</html>
);
}九、实用技巧与最佳实践
9.1 代码分割与懒加载
动态导入组件
'use client';
import dynamic from 'next/dynamic';
const HeavyComponent = dynamic(() => import('../components/HeavyComponent'), {
ssr: false, // 禁用服务端渲染
loading: () => <div>Loading...</div>,
});
export default function Page() {
return (
<div>
<h1>Light Page</h1>
<HeavyComponent />
</div>
);
}条件加载组件
'use client';
import { useState, useEffect } from 'react';
export default function Page() {
const [showHeavyComponent, setShowHeavyComponent] = useState(false);
useEffect(() => {
// 只在客户端加载
setShowHeavyComponent(true);
}, []);
return (
<div>
<h1>Light Page</h1>
{showHeavyComponent && (
<HeavyComponent />
)}
</div>
);
}9.2 性能优化
字体优化
// app/layout.js
import { Inter } from 'next/font/google';
const inter = Inter({ subsets: ['latin'] });
export default function RootLayout({ children }) {
return (
<html lang="en" className={inter.className}>
<body>{children}</body>
</html>
);
}预加载资源
// app/layout.js
export default function RootLayout({ children }) {
return (
<html lang="en">
<head>
<link rel="preload" href="/hero-image.jpg" as="image" />
</head>
<body>{children}</body>
</html>
);
}优化图片加载
import Image from 'next/image';
export default function Page() {
return (
<Image
src="/hero.jpg"
alt="Hero"
width={1200}
height={630}
priority // 优先加载关键图像
placeholder="blur" // 模糊占位符
blurDataURL="/hero-blur.jpg" // 模糊图片数据URL
/>
);
}9.3 错误处理
全局错误边界
// app/error.js
'use client';
export default function Error({ error, reset }) {
return (
<div>
<h2>Something went wrong!</h2>
<p>{error.message}</p>
<button onClick={() => reset()}>Try again</button>
</div>
);
}API 错误处理
// app/api/users/route.js
export async function GET() {
try {
const users = await fetchUsers();
return new Response(JSON.stringify(users), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
} catch (error) {
return new Response(JSON.stringify({
error: 'Failed to fetch users',
message: error.message
}), {
status: 500,
headers: { 'Content-Type': 'application/json' },
});
}
}