要点
App Router 用文件组织路由。page 定义页面入口,layout 提供共享外壳;组件函数返回的是界面描述。
跑通一次从创建到部署的闭环
- 用
create-next-app创建项目后,确认锁文件、目录结构和package.json里的 scripts 三者对得上 - 手工新增一个
app/about/page.tsx,刷新后在浏览器里看到新路由,而不是只在文件树里存在 - 把其中一个组件标记为客户端组件,观察浏览器控制台与构建产物的差别
- 跑通一次生产构建并部署,确认直接访问深层路由也能正常返回
容易卡住的地方
- 把
layout和page的职责混在一起,改一个页面时另一个页面跟着坏 - 开发服务器看不到预期内容,先怀疑缓存,实际是渲染位置选错了
- 部署后页面取不到数据,原因是环境变量没有在部署环境里配置
概述
本章从空目录开始,走完创建项目、加一个路由、写一个组件、跑一次生产构建这一整条路径。重点不是记住脚手架的每个交互提示,而是知道每一步在磁盘上留下了什么文件,以及这些文件在什么时候被读取。
项目创建流程
第一步:环境准备
# 检查Node.js版本 (需要Node.js 20.9+)
node --version
1. 检查npm版本
npm --version
1. 创建项目目录
mkdir my-first-nextjs-app
cd my-first-nextjs-app
第二步:项目初始化
# 使用create-next-app创建项目
npx create-next-app@latest . --typescript --tailwind --eslint --app --src-dir --import-alias "@/*"
1. 或者手动回答配置问题
npx create-next-app@latest
1. 项目配置选项:
1. Would you like to use TypeScript? … Yes
1. Would you like to use ESLint? … Yes
1. Would you like to use Tailwind CSS? … Yes
1. Would you like to use 'src/' directory? … Yes
1. Would you like to use App Router? … Yes
1. Would you like to customize the default import alias? … @/*
第三步:项目结构理解
my-first-nextjs-app/
├── src/
│ ├── app/
│ │ ├── layout.tsx # 根布局
│ │ ├── page.tsx # 首页
│ │ ├── globals.css # 全局样式
│ │ └── favicon.ico # 网站图标
│ ├── components/ # 组件目录
│ └── lib/ # 工具库
├── public/ # 静态资源
├── package.json # 依赖配置
├── next.config.js # Next.js配置
├── tsconfig.json # TypeScript配置
├── tailwind.config.ts # Tailwind配置
└── README.md # 项目说明
理解App Router架构
根布局 (layout.tsx)
// src/app/layout.tsx
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import './globals.css'
const inter = Inter({ subsets: ['latin'] })
export const metadata: Metadata = {
title: '我的第一个Next.js应用',
description: '学习Next.js 16开发',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="zh-CN">
<body className={inter.className}>
<div className="min-h-screen bg-gray-50">
<header className="bg-blue-600 text-white p-4">
<h1 className="text-2xl font-bold">我的应用</h1>
</header>
<main className="container mx-auto p-4">
{children}
</main>
<footer className="bg-gray-800 text-white p-4 text-center">
<p>© 2025 我的Next.js应用</p>
</footer>
</div>
</body>
</html>
)
}
首页 (page.tsx)
// src/app/page.tsx
import Image from 'next/image'
export default function HomePage() {
return (
<div className="space-y-8">
<section className="text-center py-12">
<h1 className="text-4xl font-bold text-gray-900 mb-4">
欢迎来到Next.js 16
</h1>
<p className="text-xl text-gray-600 mb-8">
这是你的第一个Next.js应用
</p>
<div className="space-x-4">
<button className="bg-blue-600 text-white px-6 py-3 rounded-lg hover:bg-blue-700 transition">
开始学习
</button>
<button className="border border-blue-600 text-blue-600 px-6 py-3 rounded-lg hover:bg-blue-50 transition">
查看示例
</button>
</div>
</section>
<section className="grid grid-cols-1 md:grid-cols-3 gap-8">
<div className="bg-white p-6 rounded-lg shadow-md">
<h3 className="text-xl font-semibold mb-2">🚀 快速开发</h3>
<p className="text-gray-600">
Next.js提供强大的开发工具和零配置体验
</p>
</div>
<div className="bg-white p-6 rounded-lg shadow-md">
<h3 className="text-xl font-semibold mb-2">⚡ 高性能</h3>
<p className="text-gray-600">
自动优化和现代化的渲染策略
</p>
</div>
<div className="bg-white p-6 rounded-lg shadow-md">
<h3 className="text-xl font-semibold mb-2">🎯 SEO友好</h3>
<p className="text-gray-600">
内置SEO优化和搜索引擎友好特性
</p>
</div>
</section>
</div>
)
}
创建路由页面
关于页面
// src/app/about/page.tsx
export default function AboutPage() {
return (
<div className="max-w-4xl mx-auto">
<h1 className="text-3xl font-bold text-gray-900 mb-6">
关于我们
</h1>
<div className="prose prose-lg">
<p className="text-gray-600 mb-4">
这是使用Next.js 16构建的第一个应用。在这个项目中,你将学习:
</p>
<ul className="list-disc pl-6 space-y-2">
<li>App Router的基本概念和使用</li>
<li>页面和布局的创建方法</li>
<li>组件开发和状态管理</li>
<li>样式系统和UI设计</li>
<li>数据获取和API集成</li>
</ul>
</div>
<div className="mt-8 p-6 bg-blue-50 rounded-lg">
<h2 className="text-xl font-semibold text-blue-900 mb-4">
技术栈
</h2>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="text-center">
<div className="text-2xl mb-2">⚛️</div>
<div className="text-sm">React 19</div>
</div>
<div className="text-center">
<div className="text-2xl mb-2">🚀</div>
<div className="text-sm">Next.js 16</div>
</div>
<div className="text-center">
<div className="text-2xl mb-2">📘</div>
<div className="text-sm">TypeScript</div>
</div>
<div className="text-center">
<div className="text-2xl mb-2">🎨</div>
<div className="text-sm">Tailwind CSS</div>
</div>
</div>
</div>
</div>
)
}
联系页面
// src/app/contact/page.tsx
'use client' // 使用客户端组件
import { useState } from 'react'
export default function ContactPage() {
const [formData, setFormData] = useState({
name: '',
email: '',
message: ''
})
const [isSubmitting, setIsSubmitting] = useState(false)
const [submitted, setSubmitted] = useState(false)
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
setFormData({
...formData,
[e.target.name]: e.target.value
})
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setIsSubmitting(true)
// 模拟表单提交
await new Promise(resolve => setTimeout(resolve, 1000))
setSubmitted(true)
setIsSubmitting(false)
setFormData({ name: '', email: '', message: '' })
}
if (submitted) {
return (
<div className="max-w-2xl mx-auto text-center">
<div className="bg-green-50 border border-green-200 rounded-lg p-8">
<h2 className="text-2xl font-bold text-green-800 mb-4">
✅ 提交成功!
</h2>
<p className="text-green-700">
感谢你的留言,我们会尽快回复你。
</p>
<button
onClick={() => setSubmitted(false)}
className="mt-4 bg-green-600 text-white px-6 py-2 rounded-lg hover:bg-green-700"
>
发送新消息
</button>
</div>
</div>
)
}
return (
<div className="max-w-2xl mx-auto">
<h1 className="text-3xl font-bold text-gray-900 mb-6">
联系我们
</h1>
<form onSubmit={handleSubmit} className="space-y-6">
<div>
<label htmlFor="name" className="block text-sm font-medium text-gray-700 mb-2">
姓名
</label>
<input
type="text"
id="name"
name="name"
value={formData.name}
onChange={handleChange}
required
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-2">
邮箱
</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleChange}
required
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
<div>
<label htmlFor="message" className="block text-sm font-medium text-gray-700 mb-2">
留言
</label>
<textarea
id="message"
name="message"
value={formData.message}
onChange={handleChange}
required
rows={4}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
<button
type="submit"
disabled={isSubmitting}
className="w-full bg-blue-600 text-white py-3 rounded-lg hover:bg-blue-700 disabled:bg-gray-400 transition"
>
{isSubmitting ? '发送中...' : '发送消息'}
</button>
</form>
</div>
)
}
创建可复用组件
导航组件
// src/components/Navigation.tsx
import Link from 'next/link'
export default function Navigation() {
return (
<nav className="bg-white shadow-md">
<div className="container mx-auto px-4">
<div className="flex justify-between items-center h-16">
<Link href="/" className="text-xl font-bold text-blue-600">
MyApp
</Link>
<div className="flex space-x-8">
<Link
href="/"
className="text-gray-700 hover:text-blue-600 transition"
>
首页
</Link>
<Link
href="/about"
className="text-gray-700 hover:text-blue-600 transition"
>
关于
</Link>
<Link
href="/contact"
className="text-gray-700 hover:text-blue-600 transition"
>
联系
</Link>
</div>
</div>
</div>
</nav>
)
}
卡片组件
// src/components/Card.tsx
import { ReactNode } from 'react'
interface CardProps {
children: ReactNode
className?: string
}
export default function Card({ children, className = '' }: CardProps) {
return (
<div className={`bg-white rounded-lg shadow-md p-6 ${className}`}>
{children}
</div>
)
}
interface CardHeaderProps {
children: ReactNode
className?: string
}
export function CardHeader({ children, className = '' }: CardHeaderProps) {
return (
<div className={`mb-4 ${className}`}>
{children}
</div>
)
}
interface CardContentProps {
children: ReactNode
className?: string
}
export function CardContent({ children, className = '' }: CardContentProps) {
return (
<div className={className}>
{children}
</div>
)
}
开发和调试
启动开发服务器
# 启动开发服务器
npm run dev
1. 或者使用yarn
yarn dev
1. 或者使用pnpm
pnpm dev
开发服务器将在 http://localhost:3000 启动,支持:
- 热重载 – 代码修改后自动刷新
- 错误提示 – 详细的错误信息和修复建议
- Fast Refresh – 保持组件状态的快速刷新
常用开发命令
# 开发模式
npm run dev
1. 构建生产版本
npm run build
1. 启动生产服务器
npm start
1. 代码检查
npm run lint
1. 类型检查
npm run type-check
项目配置优化
Next.js配置
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
// Turbopack 配置(Next.js 16 为顶层键,不再是 experimental.turbo)
turbopack: {
rules: {
'*.svg': {
loaders: ['@svgr/webpack'],
as: '*.js',
},
},
},
// 图片优化
images: {
formats: ['image/webp', 'image/avif'],
remotePatterns: [{ protocol: 'https', hostname: 'example.com' }], // 允许的图片域名
},
// 重定向配置
async redirects() {
return [
{
source: '/home',
destination: '/',
permanent: true,
},
]
},
// 头部配置
async headers() {
return [
{
source: '/(.*)',
headers: [
{
key: 'X-Content-Type-Options',
value: 'nosniff',
},
],
},
]
},
}
module.exports = nextConfig
TypeScript配置
// tsconfig.json
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "es6"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
部署第一个应用
Vercel部署(推荐)
# 安装Vercel CLI
npm i -g vercel
1. 登录Vercel
vercel login
1. 部署项目
vercel
1. 部署生产环境
vercel --prod
构建和部署
# 构建项目
npm run build
1. 检查构建结果
npm start
1. 部署到其他平台(自托管 Node 服务器):
1. 1. 在 next.config.js 中设置 output: 'standalone'
1. 2. 构建后把 .next/standalone、.next/static、public 三者上传到服务器
1. 3. 服务器上安装依赖并运行 node .next/standalone/server.js
1. 若不使用 standalone,则需上传整个项目并在服务器执行 npm ci && npm run build && npm run start
实践练习
练习1:创建个人介绍页面
- 创建
/profile路由 - 展示个人信息、技能和经历
- 使用组件化思想组织页面
练习2:作品集页面
- 创建
/portfolio路由 - 展示项目作品
- 实现筛选和搜索功能
练习3:博客页面
- 创建
/blog路由 - 展示文章列表
- 实现文章详情页面
总结
第一个应用的目标是建立可观察的开发闭环:能启动、能找到页面入口、能区分服务端和客户端交互、能从报错定位到文件,而不是只看到默认欢迎页。