第一个 Next.js 应用

要点

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>&copy; 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 路由
  • 展示文章列表
  • 实现文章详情页面

总结

第一个应用的目标是建立可观察的开发闭环:能启动、能找到页面入口、能区分服务端和客户端交互、能从报错定位到文件,而不是只看到默认欢迎页。

相关阅读

暂无评论

发送评论 编辑评论

|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇