Lesson 17:Next.js 15 项目搭建 — 进入全栈的大门
🧩 本节信息卡(学习前先看)
- 阶段定位:Phase 3(实战篇)
- 推荐时长:90~150 分钟(首次学习)
- 先修要求:完成 Phase 1~2,理解路由、状态管理与异步请求
- 学习产出:创建 Next.js 15 项目,完成首页、商品列表和错误兜底
✅ 本节完成标准(自检清单)
- [ ] 我可以独立复现文中的核心代码片段
- [ ] 我能解释“为什么这样实现”,而不只是“照着写”
- [ ] 我记录了至少 1 个踩坑点和修复方法
🧭 本节统一学习流程
- 学习目标:先明确本节要解决的业务问题与核心 API。
- 主线实战:跟随课程实现可运行功能(先跑通,再优化)。
- 原理深挖:理解为什么这样设计,以及常见误区。
- 练习挑战:完成 L1/L2(阶段收官课建议加 L3)巩固迁移能力。
- 本节小结:回顾“做了什么 / 学到了什么 / 下节前检查项”。
建议节奏:阅读 20% + 编码 60% + 复盘 20%。
🎯 本节目标:理解 Next.js 的定位和四种渲染模式,搭建全栈电商项目骨架。
📦 本节产出:一个基于 Next.js 15 App Router 的全栈项目,含首页和商品列表页面。
一、为什么需要 Next.js?
在 Phase 2,我们用 Vite + React Router 搭建了一个 纯客户端渲染 (CSR) 的 SPA。 纯 CSR 在内容展示、首屏加载和服务端能力上有一些取舍:
| 痛点 | CSR (Vite) | SSR (Next.js) |
|---|---|---|
| SEO | 初始 HTML 通常没有页面内容,索引依赖爬虫执行 JS | 预渲染 HTML 包含内容,更便于抓取 |
| 首屏速度 | 内容展示依赖 JS 下载和执行 | 可先显示 HTML;实际速度仍受数据查询、网络和缓存影响 |
| 后端 API | 需接入已有 API、后端服务或 BaaS | App Router 内置 Route Handlers,可与页面放在同一项目 |
| 数据库 | 前端不能直接访问数据库 | Server Components 可直接查数据库! |
二、四种渲染模式速览
同一个 Next.js 项目里,不同路由可以采用不同的渲染和缓存策略。下面四类是入门分类;实际页面也能组合服务端内容与客户端交互。
| 模式 | 渲染时机 | 优点 | 缺点 |
|---|---|---|---|
| CSR | 浏览器端 | 适合依赖浏览器状态的交互 | 初始内容依赖 JS,需评估索引与加载体验 |
| SSR | 每次请求时 | 可按请求生成 HTML | 有服务端计算成本;数据是否新鲜还取决于数据缓存 |
| SSG | 构建时(npm run build) | 速度极快、可 CDN 缓存 | 数据不会自动更新 |
| ISR | 预生成后按时间或事件重新验证 | 复用静态结果并更新内容 | 时间到期通常由后续请求触发更新,可能短暂看到旧内容 |
三、初始化项目
npx create-next-app@15 phase3-ecommerce --typescript --tailwind --eslint --app --src-dir --import-alias "@/*"
cd phase3-ecommerce
npm pkg set overrides.next.postcss=8.5.23
npm installNOTE
我们选择了 --app 来启用 App Router(而非旧版的 Pages Router)。 App Router 是 Next.js 13+ 的架构革新,也是 React Server Components 的官方落地载体。
本阶段使用 Node.js 24 LTS、Next.js 15.5 维护版本和 React 19,请提交 package-lock.json。@15 防止脚手架跨到 Next.js 16;截至本次校对,15 系列最新维护版本为 15.5.25。交互提示可选择默认 Turbopack 开发服务器。安装后用 npm ls next react react-dom 核对依赖,后续遵循 Next.js 支持政策 安装同系列安全更新。安装选项见 Next.js 15 安装文档。
Next.js 15.5.25 的传递依赖包含旧 PostCSS;上面的定向 override 使用已修复版本 8.5.23,本次已验证与课程项目构建兼容。原因见 PostCSS 安全公告。安装新的 Next.js 15 维护版后,可重新检查 npm audit 和构建结果,确认是否仍需此覆盖。
3.1 项目结构
phase3-ecommerce/
├── src/
│ └── app/ ← App Router 的核心目录
│ ├── layout.tsx ← 根布局(等同 Phase 2 的 RootLayout)
│ ├── page.tsx ← 首页 (/)
│ ├── globals.css ← 全局样式
│ └── products/
│ └── page.tsx ← 商品列表 (/products)
├── public/ ← 静态资源
├── next.config.ts ← Next.js 配置
├── postcss.config.mjs ← Tailwind 4 的 PostCSS 配置
└── package.json保留脚手架的 Tailwind 4 配置,globals.css 中应有 @import "tailwindcss";。Tailwind 4 默认使用 CSS 配置,不会生成 tailwind.config.ts。
3.2 核心区别:文件即路由
在 Phase 2 里,路由需要手动在 main.tsx 中写 createBrowserRouter([...]) 配置路由数组。
Next.js App Router 使用文件系统路由——文件夹结构就是 URL 路径!
规则很简单:
- 每个文件夹代表一段 URL
page.tsx是该路径的页面组件layout.tsx是该路径及其子路径的共享布局[id]方括号 = 动态路由参数(等同 React Router 中的:id)
四、编写首页和商品列表
4.1 根布局 app/layout.tsx
// src/app/layout.tsx
import type { Metadata } from 'next'
import Link from 'next/link'
import './globals.css'
export const metadata: Metadata = {
title: 'ShopNext — 全栈电商',
description: '用 Next.js 15 构建的全栈电商平台',
}
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="zh-CN">
<body className="min-h-screen bg-gray-50 text-gray-900 antialiased">
<header className="bg-white border-b border-gray-200 shadow-sm">
<nav className="max-w-7xl mx-auto px-4 h-16 flex items-center justify-between">
<Link href="/" className="text-xl font-bold text-indigo-600">🛒 ShopNext</Link>
<div className="flex gap-6 text-sm font-medium text-gray-600">
<Link href="/products" className="hover:text-indigo-600 transition-colors">商品</Link>
{/* 登录和购物车路由在后续课程创建后再添加导航链接。 */}
</div>
</nav>
</header>
<main>{children}</main>
</body>
</html>
)
}IMPORTANT
站内导航使用 Next.js 的 <Link>,支持客户端导航和预取;外部站点链接仍可使用 <a>。
4.2 首页 app/page.tsx
// src/app/page.tsx
import Link from 'next/link'
export default function Home() {
return (
<div className="max-w-7xl mx-auto px-4 py-20 text-center">
<h1 className="text-5xl font-extrabold bg-linear-to-r from-indigo-600 to-purple-600 bg-clip-text text-transparent">
欢迎来到 ShopNext
</h1>
<p className="mt-6 text-xl text-gray-500 max-w-2xl mx-auto">
一个用 Next.js 15 + React Server Components 构建的全栈电商平台。
</p>
<Link
href="/products"
className="mt-8 inline-block bg-indigo-600 text-white px-8 py-3 rounded-xl font-semibold hover:bg-indigo-700 transition-colors"
>
浏览商品 →
</Link>
</div>
)
}4.3 商品列表假数据 app/products/page.tsx
// src/app/products/page.tsx
// 模拟数据(后面会替换成 Prisma 数据库查询)
const products = [
{ id: '1', name: 'React 19 实战手册', price: 99, image: '📘' },
{ id: '2', name: 'TypeScript 进阶指南', price: 129, image: '📗' },
{ id: '3', name: 'Next.js 全栈开发', price: 159, image: '📕' },
]
export default function ProductsPage() {
return (
<div className="max-w-7xl mx-auto px-4 py-12">
<h1 className="text-3xl font-bold mb-8">全部商品</h1>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
{products.map(product => (
<article
key={product.id}
className="group bg-white rounded-2xl border border-gray-200 overflow-hidden hover:shadow-lg transition-shadow"
>
<div className="h-48 bg-gray-100 flex items-center justify-center text-6xl">
{product.image}
</div>
<div className="p-5">
<h2 className="font-semibold text-lg group-hover:text-indigo-600 transition-colors">
{product.name}
</h2>
<p className="mt-2 text-2xl font-bold text-indigo-600">¥{product.price}</p>
</div>
</article>
))}
</div>
</div>
)
}启动项目:
npm run dev访问 http://localhost:3000,你会看到一个有渐变标题的首页。点击"浏览商品"就能看到三本书的卡片。
右键→查看页面源代码:商品列表的初始 HTML 已包含商品内容。这说明页面经过了服务端预渲染,但不代表每次请求都执行 SSR:本节的静态数据页面在生产构建时可以预渲染为静态页面。Phase 2 的纯 CSR 配置需要 JS 执行后才生成内容;Vite 本身也能用于 SSR。商品详情页将在 L18 实现,L22 再补搜索和 SEO,本节卡片先只展示内容。
五、🧠 深度专题:Next.js 请求生命周期
Hydration(水合) 是 React 在浏览器中接管预渲染 HTML、关联客户端组件逻辑和事件处理器的过程,SSR 和静态预渲染都可能用到。HTML 本身仍有链接、表单等原生能力;Server Components 不在浏览器中水合,需交互的 Client Components 才参与水合。参见 服务端与客户端组件。
六、约定式文件:error.tsx 与 not-found.tsx
App Router 有一套约定式特殊文件,自动处理加载、错误和 404 等状态:
src/app/products/
├── page.tsx ← 页面组件
├── loading.tsx ← 加载状态(见 L27)
├── error.tsx ← 运行时错误兜底(必须是 Client Component!)
└── not-found.tsx ← 404 页面6.1 error.tsx — 路由级 ErrorBoundary
还记得 Phase 2 的 L16 中我们手写了 ErrorBoundary 类组件吗?Next.js 把这个能力内置到了文件约定中:
// src/app/products/error.tsx
'use client' // error.tsx 必须是 Client Component!
export default function ProductsError({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return (
<div className="max-w-lg mx-auto px-4 py-20 text-center">
<p className="text-6xl mb-4">😵</p>
<h2 className="text-xl font-bold mb-2">商品页面出错了</h2>
<p className="text-gray-500 mb-6 text-sm">{error.message}</p>
<button onClick={reset}
className="bg-indigo-600 text-white px-6 py-2 rounded-xl hover:bg-indigo-700">
🔄 重试
</button>
</div>
)
}reset() 会尝试重新渲染错误边界内的内容;它不会自动保证服务端数据已重新获取,持续的错误仍会显示兜底。它也不能捕获同一层布局自身的错误,详见 error.tsx 约定。
6.2 global-error.tsx — 根级兜底
根布局 (app/layout.tsx) 的错误不会被 app/error.tsx 捕获(因为 error.tsx 被嵌套在 layout.tsx 内部)。需要 global-error.tsx:
// src/app/global-error.tsx
'use client'
export default function GlobalError({
reset
}: {
error: Error; reset: () => void
}) {
return (
<html lang="zh-CN">
<body className="flex items-center justify-center min-h-screen">
<div className="text-center">
<h1 className="text-2xl font-bold mb-4">系统错误</h1>
<button onClick={reset} className="bg-indigo-600 text-white px-6 py-2 rounded-xl">
重试
</button>
</div>
</body>
</html>
)
}NOTE
global-error.tsx 必须自己渲染 <html> 和 <body> 标签,因为它替换了整个根布局。
七、练习
- 创建
app/about/page.tsx,写一个"关于我们"页面,注意观察 URL 是否自动映射。 - 打开浏览器的"查看源代码",对比 Phase 2 Vite 项目的 HTML 和 Next.js 的 HTML,理解 SSR 的核心价值。
📌 本节小结
| 你做了什么 | 你学到了什么 |
|---|---|
| 初始化了 Next.js 15 全栈电商项目 | Next.js 提供预渲染、路由和服务端能力 |
| 创建了首页和商品列表页 | App Router 文件系统路由 |
| 查看了预渲染返回的页面 HTML | CSR / SSR / SSG / ISR 四种渲染模式 |
| — | Hydration 水合的概念 |