如果 React 是一辆跑车,Next.js 就是给它装上导航、油门、空调和智能驾驶系统。本篇将带你循序渐进了解 Next.js 的核心概念与实战技巧。
一、Next.js 是什么?为什么要学它?
Next.js 是一个基于 React 的框架,它为前端开发者提供了构建生产级 Web 应用所需的一切:
- 页面路由自动生成
- 支持服务端渲染、静态生成、增量静态生成
- 自带 API 路由,前后端一体化开发
- 更好的 SEO、性能优化能力
🔍 一句话总结:React 负责 UI,Next.js 帮你搞定剩下的一切。
二、环境搭建:只需 60 秒就能起飞
-
安装 Node.js(推荐使用 LTS 版本)
-
初始化项目:
npx create-next-app@latest shop
cd shop
npm run dev
- 打开浏览器访问 http://localhost:3000,项目已运行!
三、项目目录结构详解
使用 create-next-app 初始化项目后,你会看到如下目录结构(默认启用 App Router):
shop/
├── app/ # 新的 App Router 路由目录
│ ├── globals.css # 全局样式
│ ├── layout.tsx # 根布局组件
│ ├── page.tsx # 首页 (/)
│ ├── loading.tsx # 全局加载组件
│ ├── error.tsx # 全局错误组件
│ ├── not-found.tsx # 404 页面
│ ├── about/ # /about 路由
│ │ ├── page.tsx # /about 页面
│ │ └── layout.tsx # about 区域布局
│ ├── products/ # /products 路由组
│ │ ├── [slug]/ # 动态路由 /products/[slug]
│ │ │ └── page.tsx
│ │ ├── (categories)/ # 路由组(不影响URL)
│ │ │ ├── electronics/
│ │ │ │ └── page.tsx # /products/electronics
│ │ │ └── clothing/
│ │ │ └── page.tsx # /products/clothing
│ │ └── _components/ # 私有文件夹
│ │ └── ProductCard.tsx
│ └── api/ # API 路由
│ └── products/
│ └── route.ts
├── public/ # 静态资源目录
│ ├── images/
│ └── icons/
├── lib/ # 工具函数库
├── components/ # 可复用组件
├── styles/ # 样式文件
├── next.config.js # Next.js 配置
└── package.json # 项目依赖
App Router 架构从 Next.js 13 起成为官方推荐,带来了革命性的文件组织方式:
- 📁 基于文件夹的路由:每个文件夹代表一个路由段
- 🎨 特殊文件约定:
page.tsx、layout.tsx、loading.tsx等有特定作用 - 🔄 React Server Components:默认支持服务端组件,性能更优
- 🎯 布局嵌套:支持多层布局复用,告别重复代码
四、App Router 路由系统全解析
Next.js 的路由系统是其核心优势之一。App Router 架构下,路由完全基于文件系统,让项目结构一目了然。
4.1 基础路由约定
在 app/ 目录中,文件夹即路由段,page.tsx 文件定义页面内容:
app/
├── page.tsx # 根路由 '/'
├── about/
│ └── page.tsx # '/about'
└── contact/
└── page.tsx # '/contact'
// app/about/page.tsx
export default function AboutPage() {
return (
<div>
<h1>关于我们</h1>
<p>这里是关于页面的内容</p>
</div>
);
}
4.2 嵌套路由(Nested Routes)
App Router 天然支持嵌套路由,通过文件夹层级体现URL结构:
app/
└── dashboard/
├── page.tsx # '/dashboard'
├── layout.tsx # dashboard 布局
├── analytics/
│ └── page.tsx # '/dashboard/analytics'
└── settings/
├── page.tsx # '/dashboard/settings'
└── profile/
└── page.tsx # '/dashboard/settings/profile'
每层文件夹都可以有自己的 layout.tsx,实现布局嵌套:
// app/dashboard/layout.tsx
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="dashboard-container">
<aside className="sidebar">
{/* 侧边栏导航 */}
<nav>...</nav>
</aside>
<main className="content">
{children}
</main>
</div>
);
}
4.3 动态路由(Dynamic Routes)
用方括号 [] 创建动态路由段,捕获URL参数:
app/
└── products/
├── page.tsx # '/products'
├── [slug]/
│ └── page.tsx # '/products/laptop-pro'
└── [category]/
└── [id]/
└── page.tsx # '/products/electronics/123'
在页面组件中通过 params 获取动态参数:
// app/products/[slug]/page.tsx
export default function ProductPage({
params
}: {
params: { slug: string }
}) {
return (
<article className="max-w-4xl mx-auto p-6">
<h1 className="text-3xl font-bold text-gray-900 mb-4">
商品:{params.slug}
</h1>
{/* 根据 slug 获取商品内容 */}
</article>
);
}
捕获所有路由段:使用 [...slug] 语法
app/
└── shop/
└── [...slug]/
└── page.tsx # 匹配 '/shop/a', '/shop/a/b', '/shop/a/b/c'
// app/shop/[...slug]/page.tsx
export default function ShopPage({
params
}: {
params: { slug: string[] }
}) {
const path = params.slug.join('/');
return (
<div className="container mx-auto px-4 py-8">
<h1 className="text-2xl font-semibold text-gray-800">
购物路径:{path}
</h1>
</div>
);
}
4.4 路由组(Route Groups)
使用圆括号 () 创建路由组,组织文件但不影响URL路径:
app/
├── (marketing)/ # 路由组,不影响URL
│ ├── about/
│ │ └── page.tsx # '/about' (不是 '/marketing/about')
│ └── contact/
│ └── page.tsx # '/contact'
├── (shop)/ # 另一个路由组
│ ├── products/
│ │ └── page.tsx # '/products'
│ └── cart/
│ └── page.tsx # '/cart'
└── layout.tsx # 根布局
路由组的作用:
- 逻辑分组:按功能模块组织代码
- 多重布局:不同组可以有不同的布局
- 团队协作:不同团队维护不同的路由组
// app/(marketing)/layout.tsx - 营销页面布局
export default function MarketingLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="min-h-screen bg-gray-50">
<header className="bg-white shadow-sm border-b">
<div className="max-w-7xl mx-auto px-4 py-6">
<h1 className="text-2xl font-bold text-gray-900">营销页面</h1>
</div>
</header>
<main className="max-w-7xl mx-auto px-4 py-8">
{children}
</main>
<footer className="bg-gray-800 text-white py-8 mt-auto">
<div className="max-w-7xl mx-auto px-4 text-center">
<p>© 2025 我的商城</p>
</div>
</footer>
</div>
);
}
// app/(shop)/layout.tsx - 商城页面布局
export default function ShopLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="min-h-screen bg-white">
<header className="bg-blue-600 text-white shadow-lg">
<div className="max-w-7xl mx-auto px-4 py-4">
<h1 className="text-xl font-semibold">我的商城</h1>
</div>
</header>
<nav className="bg-blue-500 text-white">
<div className="max-w-7xl mx-auto px-4 py-2">
<ul className="flex space-x-6">
<li><a href="/products" className="hover:text-blue-200">商品</a></li>
<li><a href="/cart" className="hover:text-blue-200">购物车</a></li>
</ul>
</div>
</nav>
<main className="max-w-7xl mx-auto px-4 py-6">
{children}
</main>
</div>
);
}
4.5 私有文件夹(Private Folders)
使用下划线 _ 前缀创建私有文件夹,不会被路由系统处理:
app/
├── products/
│ ├── page.tsx
│ ├── _components/ # 私有文件夹
│ │ ├── ProductCard.tsx # 仅供 products 模块使用的组件
│ │ └── ReviewList.tsx
│ ├── _lib/ # 私有工具库
│ │ └── api.ts
│ └── [slug]/
│ └── page.tsx
└── _shared/ # 全局私有文件夹
├── components/
└── utils/
私有文件夹的优势:
- 避免路由冲突:组件和工具文件不会意外成为路由
- 代码组织:相关文件就近存放,便于维护
- 作用域明确:私有文件夹内的内容仅供特定模块使用
4.6 特殊文件约定
App Router 定义了一系列特殊文件,各司其职:
| 文件名 | 作用 | 示例 |
|---|---|---|
layout.tsx | 布局组件,包裹子路由 | 侧边栏、头部导航 |
page.tsx | 页面组件,定义路由内容 | 具体页面内容 |
loading.tsx | 加载状态组件 | 骨架屏、加载动画 |
error.tsx | 错误边界组件 | 错误页面、重试按钮 |
not-found.tsx | 404页面组件 | 自定义404页面 |
global-error.tsx | 全局错误处理 | 捕获根布局错误 |
route.tsx | API路由处理器 | RESTful API端点 |
default.tsx | 并行路由默认页面 | 复杂路由场景 |
template.tsx | 模板组件 | 每次导航重新挂载 |
// app/dashboard/loading.tsx
export default function Loading() {
return (
<div className="animate-pulse space-y-4 p-6">
<div className="h-6 bg-gray-200 rounded-md w-3/4"></div>
<div className="h-4 bg-gray-200 rounded-md w-1/2"></div>
<div className="h-4 bg-gray-200 rounded-md w-2/3"></div>
</div>
);
}
// app/dashboard/error.tsx
'use client';
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="max-w-md w-full bg-white rounded-lg shadow-lg p-6">
<h2 className="text-xl font-semibold text-red-600 mb-4">
出现了一些问题!
</h2>
<p className="text-gray-600 mb-6">{error.message}</p>
<button
onClick={() => reset()}
className="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 transition-colors"
>
重试
</button>
</div>
</div>
);
}
补充:pages 目录(经典路由模式)
虽然 App Router 是新的推荐架构,但了解传统的 pages/ 目录仍然重要,特别是维护旧项目时:
基本路由规则:
pages/
├── index.js # '/' 首页
├── about.js # '/about'
├── products/
│ ├── index.js # '/products'
│ └── [slug].js # '/products/laptop-pro'
├── _app.js # 全局App组件
├── _document.js # HTML文档结构
└── 404.js # 404页面
主要特点:
- 文件即路由:每个
.js/.tsx文件自动成为路由 - 支持动态路由:
[param].js语法 - 全局配置:
_app.js包裹所有页面,_document.js自定义HTML结构 - API路由:
pages/api/目录下的文件自动成为API端点
迁移建议:
新项目推荐使用 App Router,但以下情况可考虑继续使用 pages:
- 维护现有项目,迁移成本较高
- 团队对 pages 架构更熟悉
- 项目规模较小,不需要复杂的嵌套布局
📌 两种架构可以在同一项目中共存,但建议统一使用一种以避免混乱。
五、布局系统(Layouts):告别重复代码
布局系统是 App Router 的一大亮点,让你能够优雅地复用UI结构,避免在每个页面重复写导航栏、侧边栏等公共组件。
5.1 根布局(Root Layout)
每个 Next.js 应用都必须有一个根布局 app/layout.tsx,它是最顶层的布局:
// app/layout.tsx
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="zh-CN">
<head>
<title>我的商城</title>
<meta name="description" content="Next.js 电商学习项目" />
</head>
<body className="min-h-screen bg-gray-50">
<header className="bg-white shadow-sm border-b">
<div className="max-w-7xl mx-auto px-4 py-4">
<nav className="flex items-center justify-between">
<h1 className="text-2xl font-bold text-gray-900">我的商城</h1>
<div className="space-x-4">
<a href="/" className="text-gray-600 hover:text-gray-900">首页</a>
<a href="/products" className="text-gray-600 hover:text-gray-900">商品</a>
<a href="/cart" className="text-gray-600 hover:text-gray-900">购物车</a>
</div>
</nav>
</div>
</header>
<main className="flex-1">{children}</main>
<footer className="bg-gray-800 text-white py-8 mt-auto">
<div className="max-w-7xl mx-auto px-4 text-center">
<p>© 2025 我的商城</p>
</div>
</footer>
</body>
</html>
);
}
根布局的作用相当于传统 pages 架构中的 _app.js + _document.js 的组合。
5.2 嵌套布局(Nested Layouts)
任何路由段都可以定义自己的布局,实现多层嵌套:
// app/dashboard/layout.tsx
import Link from 'next/link';
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="min-h-screen bg-gray-100">
<div className="flex">
<aside className="w-64 bg-white shadow-lg">
<div className="p-6">
<h2 className="text-lg font-semibold text-gray-800 mb-4">仪表盘</h2>
<nav className="space-y-2">
<Link
href="/dashboard"
className="block px-4 py-2 text-gray-600 hover:bg-blue-50 hover:text-blue-600 rounded-md transition-colors"
>
总览
</Link>
<Link
href="/dashboard/analytics"
className="block px-4 py-2 text-gray-600 hover:bg-blue-50 hover:text-blue-600 rounded-md transition-colors"
>
数据分析
</Link>
<Link
href="/dashboard/settings"
className="block px-4 py-2 text-gray-600 hover:bg-blue-50 hover:text-blue-600 rounded-md transition-colors"
>
设置
</Link>
</nav>
</div>
</aside>
<main className="flex-1 p-6">
<div className="bg-white rounded-lg shadow p-6">
{children}
</div>
</main>
</div>
</div>
);
}
这样,/dashboard 下的所有页面都会被这个布局包裹,同时还会被根布局包裹:
RootLayout
└─ DashboardLayout
└─ Page Content
5.3 布局状态保持
布局组件在路由切换时保持挂载状态,这意味着:
- 布局内的状态不会丢失
- 订阅、计时器等不会被重置
- 布局内的组件不会重新渲染(性能优化)
// app/dashboard/layout.tsx
'use client';
import { useState } from 'react';
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
const [sidebarOpen, setSidebarOpen] = useState(true);
// 这个状态在页面切换时会保持
return (
<div className="dashboard">
<button onClick={() => setSidebarOpen(!sidebarOpen)}>
切换侧边栏
</button>
<aside className={sidebarOpen ? 'open' : 'closed'}>
{/* 侧边栏内容 */}
</aside>
<main>{children}</main>
</div>
);
}
5.4 模板(Templates)
如果你希望在每次路由切换时都重新挂载组件,可以使用 template.tsx 而不是 layout.tsx:
// app/dashboard/template.tsx
'use client';
import { useEffect } from 'react';
export default function DashboardTemplate({
children,
}: {
children: React.ReactNode;
}) {
useEffect(() => {
// 每次路由切换都会执行
console.log('页面已挂载');
}, []);
return <div className="page-wrapper">{children}</div>;
}
Layout vs Template:
| 特性 | Layout | Template |
|---|---|---|
| 重新挂载 | ❌ 保持状态 | ✅ 每次重新挂载 |
| 性能 | 更好 | 稍差 |
| 使用场景 | 导航栏、侧边栏 | 页面级动画、统计 |
六、组件与样式管理:让应用更美观
Next.js 内置支持多种样式方案,你可以根据项目需求选择最适合的方式。
6.1 Tailwind CSS(现代化首选)
Tailwind 是当前最流行的原子化CSS框架,开发效率极高,也是我们推荐的主要样式方案:
# 安装 Tailwind CSS
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
// components/Button.tsx
interface ButtonProps {
variant?: 'primary' | 'secondary' | 'danger';
size?: 'sm' | 'md' | 'lg';
children: React.ReactNode;
onClick?: () => void;
disabled?: boolean;
}
export default function Button({
variant = 'primary',
size = 'md',
children,
onClick,
disabled = false
}: ButtonProps) {
const baseClasses = "font-medium rounded-md transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2";
const variantClasses = {
primary: "bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500",
secondary: "bg-gray-200 text-gray-900 hover:bg-gray-300 focus:ring-gray-500",
danger: "bg-red-600 text-white hover:bg-red-700 focus:ring-red-500"
};
const sizeClasses = {
sm: "px-3 py-1.5 text-sm",
md: "px-4 py-2 text-sm",
lg: "px-6 py-3 text-base"
};
const disabledClasses = disabled ? "opacity-50 cursor-not-allowed" : "";
const className = `${baseClasses} ${variantClasses[variant]} ${sizeClasses[size]} ${disabledClasses}`.trim();
return (
<button
className={className}
onClick={onClick}
disabled={disabled}
>
{children}
</button>
);
}
// 使用 Tailwind 构建商品卡片
export default function ProductCard({ product }: { product: Product }) {
return (
<div className="bg-white rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow duration-300">
<img
src={product.image}
alt={product.name}
className="w-full h-48 object-cover"
/>
<div className="p-4">
<h3 className="text-lg font-semibold text-gray-800 mb-2">{product.name}</h3>
<p className="text-gray-600 text-sm mb-3 line-clamp-2">{product.description}</p>
<div className="flex items-center justify-between">
<span className="text-xl font-bold text-blue-600">¥{product.price}</span>
<Button size="sm">加入购物车</Button>
</div>
</div>
</div>
);
}
6.2 CSS Modules(传统方案)
如果你更偏爱传统的 CSS 文件组织方式,CSS Modules 仍然是很好的选择:
/* components/ProductCard.module.css */
.card {
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
overflow: hidden;
transition: box-shadow 0.3s ease;
}
.card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.image {
width: 100%;
height: 192px;
object-fit: cover;
}
.content {
padding: 16px;
}
.title {
font-size: 1.125rem;
font-weight: 600;
color: #1f2937;
margin-bottom: 8px;
}
// components/ProductCard.tsx
import styles from './ProductCard.module.css';
export default function ProductCard({ product }: { product: Product }) {
return (
<div className={styles.card}>
<img src={product.image} alt={product.name} className={styles.image} />
<div className={styles.content}>
<h3 className={styles.title}>{product.name}</h3>
<p className={styles.description}>{product.description}</p>
</div>
</div>
);
}
6.3 Sass/SCSS 支持
Next.js 原生支持 Sass,适合喜欢嵌套语法的开发者:
npm install sass
// styles/components.scss
$primary-color: #3b82f6;
$success-color: #10b981;
$border-radius: 8px;
.product-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 24px;
padding: 24px;
.product-card {
background: white;
border-radius: $border-radius;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: transform 0.2s ease;
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
}
.product-image {
width: 100%;
height: 200px;
object-fit: cover;
}
.product-info {
padding: 16px;
.product-title {
font-size: 1.125rem;
font-weight: 600;
color: #1f2937;
margin-bottom: 8px;
}
.product-price {
font-size: 1.25rem;
font-weight: 700;
color: $primary-color;
}
}
}
}
}
6.4 CSS-in-JS 方案
支持 styled-components、emotion 等库,适合组件级样式管理:
// 使用 styled-components
import styled from 'styled-components';
const StyledProductCard = styled.div<{ featured?: boolean }>`
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: transform 0.2s ease;
border: ${props => props.featured ? '2px solid #3b82f6' : '1px solid #e5e7eb'};
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
}
.product-image {
width: 100%;
height: 200px;
object-fit: cover;
border-radius: 8px 8px 0 0;
}
.product-info {
padding: 16px;
.title {
font-size: 1.125rem;
font-weight: 600;
color: #1f2937;
margin-bottom: 8px;
}
.price {
font-size: 1.25rem;
font-weight: 700;
color: #3b82f6;
}
}
`;
6.5 全局样式配置
在根布局中导入全局样式,使用 Tailwind 的基础配置:
// app/layout.tsx
import './globals.css';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="zh-CN">
<body className="min-h-screen bg-gray-50 font-sans antialiased">
{children}
</body>
</html>
);
}
/* app/globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
html {
font-family: 'Inter', system-ui, sans-serif;
}
}
@layer components {
.btn {
@apply px-4 py-2 rounded-md font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2;
}
.btn-primary {
@apply bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500;
}
.btn-secondary {
@apply bg-gray-200 text-gray-900 hover:bg-gray-300 focus:ring-gray-500;
}
.product-grid {
@apply grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6;
}
}
6.6 组件组织最佳实践
按功能模块分组:
components/
├── ui/ # 基础UI组件
│ ├── Button/
│ │ ├── index.tsx
│ │ └── Button.test.tsx
│ ├── Card/
│ └── Input/
├── layout/ # 布局组件
│ ├── Header/
│ ├── Footer/
│ └── Sidebar/
└── shop/ # 商城业务组件
├── ProductCard/
├── CartItem/
├── CheckoutForm/
└── OrderSummary/
组件文件结构示例:
// components/ui/Button/index.tsx
import { ButtonHTMLAttributes } from 'react';
import { cn } from '@/lib/utils'; // 用于合并className的工具函数
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'danger';
size?: 'sm' | 'md' | 'lg';
loading?: boolean;
fullWidth?: boolean;
}
export default function Button({
variant = 'primary',
size = 'md',
loading = false,
fullWidth = false,
children,
className,
disabled,
...props
}: ButtonProps) {
const baseClasses = "inline-flex items-center justify-center font-medium rounded-md transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed";
const variantClasses = {
primary: "bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500",
secondary: "bg-gray-200 text-gray-900 hover:bg-gray-300 focus:ring-gray-500",
danger: "bg-red-600 text-white hover:bg-red-700 focus:ring-red-500"
};
const sizeClasses = {
sm: "px-3 py-1.5 text-sm",
md: "px-4 py-2 text-sm",
lg: "px-6 py-3 text-base"
};
const widthClasses = fullWidth ? "w-full" : "";
const buttonClasses = cn(
baseClasses,
variantClasses[variant],
sizeClasses[size],
widthClasses,
className
);
return (
<button
className={buttonClasses}
disabled={disabled || loading}
{...props}
>
{loading && (
<svg className="animate-spin -ml-1 mr-2 h-4 w-4" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
</svg>
)}
{children}
</button>
);
}
// 导出类型供其他组件使用
export type { ButtonProps };
// components/shop/ProductCard/index.tsx
import Image from 'next/image';
import Button from '@/components/ui/Button';
interface Product {
id: string;
name: string;
price: number;
image: string;
description: string;
inStock: boolean;
}
interface ProductCardProps {
product: Product;
onAddToCart: (productId: string) => void;
}
export default function ProductCard({ product, onAddToCart }: ProductCardProps) {
return (
<div className="group bg-white rounded-lg shadow-md overflow-hidden hover:shadow-xl transition-all duration-300 transform hover:-translate-y-1">
<div className="relative aspect-square overflow-hidden">
<Image
src={product.image}
alt={product.name}
fill
className="object-cover group-hover:scale-105 transition-transform duration-300"
/>
{!product.inStock && (
<div className="absolute inset-0 bg-gray-900 bg-opacity-50 flex items-center justify-center">
<span className="text-white font-semibold bg-red-600 px-3 py-1 rounded-full text-sm">
售罄
</span>
</div>
)}
</div>
<div className="p-4">
<h3 className="text-lg font-semibold text-gray-800 mb-2 line-clamp-2">
{product.name}
</h3>
<p className="text-gray-600 text-sm mb-3 line-clamp-2">
{product.description}
</p>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-1">
<span className="text-2xl font-bold text-blue-600">
¥{product.price.toLocaleString()}
</span>
</div>
<Button
size="sm"
disabled={!product.inStock}
onClick={() => onAddToCart(product.id)}
className="shrink-0"
>
{product.inStock ? '加入购物车' : '缺货'}
</Button>
</div>
</div>
</div>
);
}
// lib/utils.ts - className 合并工具函数
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
七、部署上线:让全世界看到你的作品
7.1 Vercel 部署(一键部署)
Vercel 是 Next.js 官方推荐的部署平台,配置简单,性能卓越:
步骤:
- 将代码推送到 GitHub/GitLab/Bitbucket
- 访问 vercel.com,使用 Git 账号登录
- 点击 "New Project",选择你的仓库
- Vercel 会自动识别 Next.js 项目并部署
- 几分钟后,你就有一个
your-app.vercel.app的在线地址
优势:
- 🚀 零配置部署:自动识别 Next.js 项目
- 🌍 全球 CDN:Edge Network 保证访问速度
- 🔧 自动构建:每次 Git push 都会自动重新部署
- 📊 性能监控:内置 Web Vitals 监控
- 🆓 免费额度:个人项目完全够用
7.2 其他部署选择
Netlify:
# 构建命令
npm run build
# 发布目录
out/ # 需要在 next.config.js 中配置 output: 'export'
自建服务器:
# 生产环境构建
npm run build
# 启动生产服务器
npm start
Docker 部署:
# Dockerfile
FROM node:18-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
FROM node:18-alpine AS builder
WORKDIR /app
COPY . .
COPY --from=deps /app/node_modules ./node_modules
RUN npm run build
FROM node:18-alpine AS runner
WORKDIR /app
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next ./.next
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./package.json
USER nextjs
EXPOSE 3000
CMD ["npm", "start"]
7.3 部署前优化检查
性能优化:
// 图片优化
import Image from 'next/image';
export default function Profile() {
return (
<Image
src="/profile.jpg"
alt="Profile Picture"
width={200}
height={200}
priority // 首屏图片
/>
);
}
代码分割:
// 动态导入减少首屏包大小
import dynamic from 'next/dynamic';
const HeavyComponent = dynamic(() => import('../components/Heavy'), {
loading: () => <p>Loading...</p>,
ssr: false // 客户端渲染
});
环境变量配置:
# .env.local
NEXT_PUBLIC_API_URL=https://api.yourdomain.com
DATABASE_URL=your_production_database_url
八、总结与期望
Next.js 为 React 项目注入了“全栈血统”,具备更强的页面组织能力、布局机制和开发体验。
本篇重点回顾:
- App Router 架构下的项目目录与布局方式
- 基于文件的嵌套路由设计
- 与经典 pages 路由的对比和过渡策略
📘 下一篇我们将重点介绍 Next.js 的数据获取方式以及API路由的使用方法,敬请期待!