第一个Next.js应用 #

一、创建首页 #

1.1 理解页面文件 #

在 Next.js 中,page.tsx 文件定义了一个路由页面:

text
src/app/page.tsx → /
src/app/about/page.tsx → /about
src/app/blog/page.tsx → /blog

1.2 修改首页 #

编辑 src/app/page.tsx

tsx
export default function Home() {
    return (
        <main className="min-h-screen bg-gradient-to-b from-gray-900 to-gray-800">
            <div className="container mx-auto px-4 py-16">
                <h1 className="text-4xl font-bold text-white mb-4">
                    欢迎来到我的第一个 Next.js 应用
                </h1>
                <p className="text-gray-300 text-lg">
                    这是一个使用 Next.js 15 构建的现代化 Web 应用
                </p>
            </div>
        </main>
    )
}

1.3 页面组件特点 #

  • 必须默认导出一个 React 组件
  • 组件名称可以任意命名
  • 返回 JSX 作为页面内容
  • 可以是服务端组件(默认)

二、创建布局 #

2.1 根布局 #

编辑 src/app/layout.tsx

tsx
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import './globals.css'
import Navigation from '@/components/Navigation'

const inter = Inter({ subsets: ['latin'] })

export const metadata: Metadata = {
    title: '我的 Next.js 应用',
    description: '使用 Next.js 15 构建的现代化应用',
}

export default function RootLayout({
    children,
}: {
    children: React.ReactNode
}) {
    return (
        <html lang="zh-CN">
            <body className={inter.className}>
                <Navigation />
                {children}
            </body>
        </html>
    )
}

2.2 创建导航组件 #

创建 src/components/Navigation.tsx

tsx
import Link from 'next/link'

export default function Navigation() {
    return (
        <nav className="bg-gray-800 text-white">
            <div className="container mx-auto px-4">
                <div className="flex items-center justify-between h-16">
                    <Link href="/" className="text-xl font-bold">
                        My App
                    </Link>
                    <div className="flex space-x-4">
                        <Link href="/" className="hover:text-gray-300">
                            首页
                        </Link>
                        <Link href="/about" className="hover:text-gray-300">
                            关于
                        </Link>
                        <Link href="/blog" className="hover:text-gray-300">
                            博客
                        </Link>
                    </div>
                </div>
            </div>
        </nav>
    )
}

三、创建多个页面 #

3.1 关于页面 #

创建 src/app/about/page.tsx

tsx
import type { Metadata } from 'next'

export const metadata: Metadata = {
    title: '关于我们',
    description: '了解更多关于我们的信息',
}

export default function AboutPage() {
    return (
        <main className="min-h-screen bg-gray-100 py-16">
            <div className="container mx-auto px-4">
                <h1 className="text-3xl font-bold mb-8">关于我们</h1>
                <div className="bg-white rounded-lg shadow p-6">
                    <p className="text-gray-600 mb-4">
                        这是一个使用 Next.js 构建的示例应用。
                    </p>
                    <p className="text-gray-600">
                        Next.js 是一个强大的 React 框架,提供了服务端渲染、
                        静态生成等多种渲染方式。
                    </p>
                </div>
            </div>
        </main>
    )
}

3.2 博客列表页 #

创建 src/app/blog/page.tsx

tsx
import Link from 'next/link'

const posts = [
    { id: 1, title: 'Next.js 入门指南', excerpt: '学习 Next.js 的基础知识' },
    { id: 2, title: 'React Server Components', excerpt: '深入理解服务端组件' },
    { id: 3, title: 'Next.js 路由系统', excerpt: '掌握 App Router 的使用' },
]

export default function BlogPage() {
    return (
        <main className="min-h-screen bg-gray-100 py-16">
            <div className="container mx-auto px-4">
                <h1 className="text-3xl font-bold mb-8">博客文章</h1>
                <div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
                    {posts.map((post) => (
                        <article
                            key={post.id}
                            className="bg-white rounded-lg shadow p-6 hover:shadow-lg transition-shadow"
                        >
                            <h2 className="text-xl font-semibold mb-2">
                                <Link href={`/blog/${post.id}`} className="hover:text-blue-600">
                                    {post.title}
                                </Link>
                            </h2>
                            <p className="text-gray-600">{post.excerpt}</p>
                        </article>
                    ))}
                </div>
            </div>
        </main>
    )
}

3.3 博客详情页 #

创建 src/app/blog/[id]/page.tsx

tsx
interface PageProps {
    params: Promise<{ id: string }>
}

const posts: Record<string, { title: string; content: string }> = {
    '1': {
        title: 'Next.js 入门指南',
        content: 'Next.js 是一个基于 React 的全栈框架...',
    },
    '2': {
        title: 'React Server Components',
        content: '服务端组件是 React 18 引入的新特性...',
    },
    '3': {
        title: 'Next.js 路由系统',
        content: 'App Router 是 Next.js 13 引入的新路由系统...',
    },
}

export async function generateMetadata({ params }: PageProps) {
    const { id } = await params
    const post = posts[id]
    
    return {
        title: post?.title || '文章不存在',
        description: post?.content?.slice(0, 100),
    }
}

export default async function BlogPostPage({ params }: PageProps) {
    const { id } = await params
    const post = posts[id]

    if (!post) {
        return (
            <main className="min-h-screen bg-gray-100 py-16">
                <div className="container mx-auto px-4">
                    <h1 className="text-3xl font-bold">文章不存在</h1>
                </div>
            </main>
        )
    }

    return (
        <main className="min-h-screen bg-gray-100 py-16">
            <article className="container mx-auto px-4">
                <h1 className="text-3xl font-bold mb-8">{post.title}</h1>
                <div className="bg-white rounded-lg shadow p-6">
                    <p className="text-gray-600 leading-relaxed">{post.content}</p>
                </div>
            </article>
        </main>
    )
}

四、添加样式 #

4.1 使用Tailwind CSS #

Tailwind CSS 是 Next.js 默认集成的样式方案:

tsx
export default function StyledComponent() {
    return (
        <div className="flex flex-col items-center justify-center min-h-screen bg-gradient-to-r from-purple-500 to-pink-500">
            <h1 className="text-4xl font-bold text-white mb-4">
                渐变背景标题
            </h1>
            <button className="px-6 py-3 bg-white text-purple-600 rounded-full font-semibold hover:bg-gray-100 transition-colors">
                点击按钮
            </button>
        </div>
    )
}

4.2 使用CSS模块 #

创建 src/app/about/page.module.css

css
.container {
    min-height: 100vh;
    background-color: #f3f4f6;
    padding: 4rem 0;
}

.card {
    background-color: white;
    border-radius: 0.5rem;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
    padding: 1.5rem;
}

.title {
    font-size: 1.875rem;
    font-weight: 700;
    margin-bottom: 1rem;
}

在组件中使用:

tsx
import styles from './page.module.css'

export default function AboutPage() {
    return (
        <main className={styles.container}>
            <div className={styles.card}>
                <h1 className={styles.title}>关于我们</h1>
                <p>这是使用 CSS 模块的样式示例。</p>
            </div>
        </main>
    )
}

4.3 全局样式 #

编辑 src/app/globals.css

css
@tailwind base;
@tailwind components;
@tailwind utilities;

:root {
    --primary-color: #3b82f6;
    --secondary-color: #64748b;
}

body {
    font-family: system-ui, -apple-system, sans-serif;
    line-height: 1.6;
}

a {
    color: var(--primary-color);
    text-decoration: none;
}

a:hover {
    text-decoration: underline;
}

五、添加交互 #

5.1 客户端组件 #

创建 src/components/Counter.tsx

tsx
'use client'

import { useState } from 'react'

export default function Counter() {
    const [count, setCount] = useState(0)

    return (
        <div className="flex items-center gap-4">
            <button
                onClick={() => setCount(count - 1)}
                className="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600"
            >
                -
            </button>
            <span className="text-2xl font-bold">{count}</span>
            <button
                onClick={() => setCount(count + 1)}
                className="px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600"
            >
                +
            </button>
        </div>
    )
}

在页面中使用:

tsx
import Counter from '@/components/Counter'

export default function InteractivePage() {
    return (
        <main className="min-h-screen bg-gray-100 py-16">
            <div className="container mx-auto px-4">
                <h1 className="text-3xl font-bold mb-8">交互示例</h1>
                <Counter />
            </div>
        </main>
    )
}

5.2 表单处理 #

创建 src/components/ContactForm.tsx

tsx
'use client'

import { useState } from 'react'

export default function ContactForm() {
    const [formData, setFormData] = useState({
        name: '',
        email: '',
        message: '',
    })
    const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle')

    const handleSubmit = async (e: React.FormEvent) => {
        e.preventDefault()
        setStatus('loading')

        try {
            const response = await fetch('/api/contact', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify(formData),
            })

            if (response.ok) {
                setStatus('success')
                setFormData({ name: '', email: '', message: '' })
            } else {
                setStatus('error')
            }
        } catch {
            setStatus('error')
        }
    }

    return (
        <form onSubmit={handleSubmit} className="space-y-4 max-w-md">
            <div>
                <label className="block text-sm font-medium mb-1">姓名</label>
                <input
                    type="text"
                    value={formData.name}
                    onChange={(e) => setFormData({ ...formData, name: e.target.value })}
                    className="w-full px-3 py-2 border rounded"
                    required
                />
            </div>
            <div>
                <label className="block text-sm font-medium mb-1">邮箱</label>
                <input
                    type="email"
                    value={formData.email}
                    onChange={(e) => setFormData({ ...formData, email: e.target.value })}
                    className="w-full px-3 py-2 border rounded"
                    required
                />
            </div>
            <div>
                <label className="block text-sm font-medium mb-1">消息</label>
                <textarea
                    value={formData.message}
                    onChange={(e) => setFormData({ ...formData, message: e.target.value })}
                    className="w-full px-3 py-2 border rounded"
                    rows={4}
                    required
                />
            </div>
            <button
                type="submit"
                disabled={status === 'loading'}
                className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 disabled:opacity-50"
            >
                {status === 'loading' ? '发送中...' : '发送'}
            </button>
            {status === 'success' && (
                <p className="text-green-600">发送成功!</p>
            )}
            {status === 'error' && (
                <p className="text-red-600">发送失败,请重试。</p>
            )}
        </form>
    )
}

六、数据获取 #

6.1 服务端数据获取 #

tsx
interface User {
    id: number
    name: string
    email: string
}

async function getUsers(): Promise<User[]> {
    const res = await fetch('https://jsonplaceholder.typicode.com/users')
    if (!res.ok) throw new Error('Failed to fetch users')
    return res.json()
}

export default async function UsersPage() {
    const users = await getUsers()

    return (
        <main className="min-h-screen bg-gray-100 py-16">
            <div className="container mx-auto px-4">
                <h1 className="text-3xl font-bold mb-8">用户列表</h1>
                <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
                    {users.map((user) => (
                        <div key={user.id} className="bg-white rounded-lg shadow p-4">
                            <h2 className="font-semibold">{user.name}</h2>
                            <p className="text-gray-600 text-sm">{user.email}</p>
                        </div>
                    ))}
                </div>
            </div>
        </main>
    )
}

6.2 客户端数据获取 #

tsx
'use client'

import useSWR from 'swr'

const fetcher = (url: string) => fetch(url).then((res) => res.json())

export default function ClientUsersPage() {
    const { data, error, isLoading } = useSWR(
        'https://jsonplaceholder.typicode.com/users',
        fetcher
    )

    if (isLoading) return <div>加载中...</div>
    if (error) return <div>加载失败</div>

    return (
        <div className="grid gap-4">
            {data.map((user: any) => (
                <div key={user.id} className="bg-white p-4 rounded shadow">
                    {user.name}
                </div>
            ))}
        </div>
    )
}

七、项目运行 #

7.1 开发模式 #

bash
npm run dev

7.2 构建生产版本 #

bash
npm run build

7.3 运行生产版本 #

bash
npm run start

八、总结 #

第一个应用要点:

知识点 说明
页面创建 page.tsx 文件
布局定义 layout.tsx 文件
路由导航 Link 组件
样式处理 Tailwind CSS / CSS模块
客户端交互 ‘use client’ 指令
数据获取 async/await

下一步,让我们深入了解 Next.js 的项目结构!

最后更新:2026-03-28