javascript技巧

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript技巧 > Next.js从入门到精通

Next.js从入门到精通的万字长文教程(附详细代码)

作者:油墨香^_^

Next.js是一个基于React的框架,提供了服务器端渲染(SSR)和静态站点生成(SSG)功能,这篇文章主要介绍了Next.js从入门到精通的相关资料,文中通过代码介绍的非常详细,需要的朋友可以参考下

引言:为什么是 Next.js?

在 React 生态系统中,我们常常面临一些挑战:如何配置服务器端渲染(SSR)以提升首屏加载速度和 SEO?如何实现静态站点生成(SSG)以获得极致的性能?如何优雅地处理路由、API 接口、代码分割和打包优化?

Next.js 的出现,完美地回答了这些问题。它是由 Vercel 公司创建的基于 React 的全功能生产级框架。它提供了一系列开箱即用的特性,让你能专注于业务逻辑而非构建配置。从简单的个人博客到复杂的电子商务平台,Next.js 都能胜任。

Next.js 的核心优势:

  1. 零配置: 内置 webpack、Babel、代码分割、热更新等,开箱即用。

  2. 服务端渲染: 轻松实现 SSR 和 SSG,提升性能和 SEO。

  3. 文件系统路由: 在 pages 目录下创建文件即可自动生成路由。

  4. API 路由: 在同一个项目中编写前端和后端 API。

  5. 出色的开发者体验: 快速刷新、强大的错误提示等。

  6. 强大的社区和生态: 拥有庞大的社区支持和丰富的插件。

本教程将分为四个部分:

  1. 入门篇: 创建第一个 Next.js 应用,理解核心概念。

  2. 核心概念篇: 深入探讨路由、数据获取、样式和 API 路由。

  3. 进阶实战篇: 构建一个完整的全栈博客应用。

  4. 精通与优化篇: 探索高级特性、性能优化和部署。

第一部分:入门篇 - 创建你的第一个 Next.js 应用

1.1 环境准备与项目创建

首先,确保你的系统安装了 Node.js (版本 14.6.0 或更高)。

Next.js 提供了官方的创建工具 create-next-app,它能快速搭建一个预配置好的 Next.js 项目。

打开你的终端,执行以下命令:

bash

npx create-next-app@latest my-nextjs-app

你会被提示选择一些配置项:

我们选择使用 App Router,它是 Next.js 13 引入的基于 React Server Components 的新路由系统,功能更强大,是未来的方向。

等待依赖安装完成后,进入项目目录并启动开发服务器:

bash

cd my-nextjs-app
npm run dev

在浏览器中打开 http://localhost:3000,你将看到 Next.js 的欢迎页面。

1.2 项目结构初探

使用 create-next-app 并选择上述配置后,你的项目结构大致如下:

text

my-nextjs-app/
├── app/                 # App Router 的主目录
│   ├── favicon.ico
│   ├── globals.css      # 全局样式
│   ├── layout.tsx       # 根布局组件
│   ├── page.tsx         # 首页组件 (对应路由 `/`)
│   └── ...
├── public/              # 静态资源目录 (图片、字体等)
│   └── ...
├── next.config.js       # Next.js 配置文件
├── package.json
├── eslint.config.mjs    # ESLint 配置
└── tsconfig.json        # TypeScript 配置

关键文件解释:

1.3 理解 React Server Components vs Client Components

这是 App Router 的核心概念。

示例: 让我们修改 app/page.tsx,看看它们如何协作。

tsx

// app/page.tsx
// 这是一个 Server Component (默认)

// 一个在服务器端获取数据的异步函数 (模拟)
async function fetchServerSideData() {
  // 这里可以直接访问数据库或内部 API
  await new Promise(resolve => setTimeout(resolve, 1000)); // 模拟延迟
  return { message: "Hello from the Server!" };
}

// 一个普通的 Server Component
function ServerGreeting() {
  return <p>I was rendered on the server.</p>;
}

export default async function HomePage() {
  // 在 Server Component 中可以直接使用 async/await 获取数据
  const data = await fetchServerSideData();

  return (
    <div>
      <h1>My First Next.js App</h1>
      <p>{data.message}</p>
      <ServerGreeting />
      {/* 我们引入一个 Client Component */}
      <ClientCounter />
    </div>
  );
}

// --- 新建一个 Client Component ---
// 在 app/components/ClientCounter.tsx
"use client"; // <-- 必须的指令,标记这是一个 Client Component

import { useState } from 'react';

export function ClientCounter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times (Client Component)</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  );
}

在这个例子中:

这种混合模式让你能够为页面的每个部分选择最合适的渲染环境,从而构建出高性能的应用。

第二部分:核心概念篇 - 深入 Next.js 的基石

2.1 基于文件系统的路由 (App Router)

在 App Router 中,路由由 app 目录下的文件夹结构定义。每个文件夹代表一个路由段(segment)。要创建一个页面,你需要使用 page.tsx 或 page.jsx 文件。

基础路由:

文件路径对应路由
app/page.tsx/
app/about/page.tsx/about
app/blog/page.tsx/blog

动态路由:
如果你需要捕获动态的路由段(如博客文章 ID),可以使用方括号 [folderName] 来创建动态路由。

文件路径对应路由示例 URL
app/blog/[id]/page.tsx/blog/:id/blog/123/blog/my-post

在 page.tsx 中,你可以通过 params 属性访问到这个动态参数。

tsx

// app/blog/[id]/page.tsx
interface BlogPostProps {
  params: {
    id: string;
  };
}

export default function BlogPost({ params }: BlogPostProps) {
  return (
    <div>
      <h1>Blog Post: {params.id}</h1>
      <p>This is the content of blog post with ID: {params.id}.</p>
    </div>
  );
}

布局 (Layouts):
布局是共享的 UI,它在多个页面之间保持状态,并且不会在路由切换时重新渲染。它由 layout.tsx 文件定义。

tsx

// app/layout.tsx
export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        {/* 一个所有页面共享的导航栏 */}
        <nav className="bg-blue-600 text-white p-4">
          <div className="container mx-auto">
            <a href="/" rel="external nofollow"  className="text-xl font-bold">My Site</a>
            <a href="/about" rel="external nofollow"  className="ml-4">About</a>
            <a href="/blog" rel="external nofollow"  className="ml-4">Blog</a>
          </div>
        </nav>
        {/* 子页面将在这里被渲染 */}
        <main className="container mx-auto p-4">{children}</main>
        {/* 一个所有页面共享的页脚 */}
        <footer className="bg-gray-200 p-4 text-center">
          <p>&copy; 2023 My Site</p>
        </footer>
      </body>
    </html>
  );
}

2.2 数据获取 (Data Fetching)

Next.js 在 Server Components 中扩展了 fetch API,提供了强大的数据获取能力,包括自动缓存和重复数据删除。

三种渲染模式:

  1. 静态站点生成: 页面在构建时生成。适用于内容不经常变化的页面(如博客、文档、营销页面)。性能最好。

  2. 服务端渲染: 页面在每次请求时生成。适用于高度动态、个性化的页面(如仪表盘、用户主页)。

  3. 客户端渲染: 页面在浏览器中生成。适用于具有大量交互、不关心 SEO 的页面部分。

在 App Router 中,你通过 fetch 的选项来控制渲染模式。

tsx

// app/blog/page.tsx
// 这是一个 Server Component

interface Post {
  id: number;
  title: string;
  body: string;
}

// SSG (Static Generation) - 默认行为
// 在构建时获取数据并生成静态页面
async function getStaticPosts(): Promise<Post[]> {
  const res = await fetch('https://jsonplaceholder.typicode.com/posts', {
    // cache: 'force-cache' 是默认值,等同于 SSG
  });
  if (!res.ok) {
    throw new Error('Failed to fetch posts');
  }
  return res.json();
}

// SSR (Server-Side Rendering)
// 在每次请求时获取数据
async function getServerSidePosts(): Promise<Post[]> {
  const res = await fetch('https://jsonplaceholder.typicode.com/posts', {
    cache: 'no-store', // <-- 这表示不缓存,每次请求都重新获取
  });
  if (!res.ok) {
    throw new Error('Failed to fetch posts');
  }
  return res.json();
}

// ISR (Incremental Static Regeneration)
// 静态生成,但可以定期在后台重新验证和更新
async function getIncrementalPosts(): Promise<Post[]> {
  const res = await fetch('https://jsonplaceholder.typicode.com/posts', {
    next: { revalidate: 60 }, // <-- 每60秒重新验证一次
  });
  if (!res.ok) {
    throw new Error('Failed to fetch posts');
  }
  return res.json();
}

export default async function BlogListPage() {
  // 选择一种数据获取方式
  const posts = await getStaticPosts();

  return (
    <div>
      <h1>Blog Posts</h1>
      <ul>
        {posts.map((post) => (
          <li key={post.id}>
            <a href={`/blog/${post.id}`}>{post.title}</a>
          </li>
        ))}
      </ul>
    </div>
  );
}

2.3 样式化 (Styling)

Next.js 不限定你使用何种 CSS 方案。你可以使用:

以 CSS Modules 为例:

css

/* app/components/Button.module.css */
.primary {
  background-color: blue;
  color: white;
  padding: 10px 20px;
  border: none;
  border-radius: 5px;
  cursor: pointer;
}

.primary:hover {
  background-color: darkblue;
}

tsx

// app/components/Button.tsx
"use client"; // 如果要用 onClick,就需要是 Client Component

import styles from './Button.module.css';

interface ButtonProps {
  children: React.ReactNode;
  onClick: () => void;
}

export function Button({ children, onClick }: ButtonProps) {
  return (
    <button className={styles.primary} onClick={onClick}>
      {children}
    </button>
  );
}

2.4 API 路由 (API Routes)

Next.js 允许你在 app 目录下创建 API 端点,这意味着你可以在同一个项目中编写全栈应用。

API 路由位于 app/api/ 目录下,使用 route.ts 文件定义。

创建一个简单的 GET API:

tsx

// app/api/hello/route.ts
export async function GET() {
  // 这里可以连接数据库,处理业务逻辑
  return Response.json({ message: 'Hello from the API!' });
}

现在,访问 http://localhost:3000/api/hello 将会返回 JSON 数据。

处理不同的 HTTP 方法:

tsx

// app/api/users/route.ts
import { NextRequest } from 'next/server';

// GET /api/users
export async function GET() {
  const users = [{ id: 1, name: 'John' }, { id: 2, name: 'Jane' }];
  return Response.json(users);
}

// POST /api/users
export async function POST(request: NextRequest) {
  const body = await request.json();
  // 在这里,你可以将 body 中的数据保存到数据库
  console.log('Creating user with data:', body);
  // 模拟创建用户
  const newUser = { id: 3, ...body };
  return Response.json(newUser, { status: 201 });
}

你可以在 Client Component 中使用 fetch 来调用这些 API。

tsx

"use client";
// ... 在某个 Client Component 中
const handleCreateUser = async () => {
  const response = await fetch('/api/users', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ name: 'Alice' }),
  });
  const newUser = await response.json();
  console.log('User created:', newUser);
};

第三部分:进阶实战篇 - 构建一个全栈博客应用

现在,让我们运用所学的知识,构建一个简单的全栈博客应用。它将包含:

3.1 项目结构与数据层

我们使用一个本地的 JSON 文件来模拟数据库。在实际项目中,你会使用 Prisma、Drizzle 等 ORM 连接 PostgreSQL、MySQL 等数据库。

创建 lib/posts.ts 来处理数据:

tsx

// lib/posts.ts
export interface Post {
  id: string;
  title: string;
  content: string;
  date: string;
}

// 模拟数据
let posts: Post[] = [
  {
    id: '1',
    title: 'First Post',
    content: 'This is the content of the first post.',
    date: '2023-10-25',
  },
  {
    id: '2',
    title: 'Second Post',
    content: 'This is the content of the second post.',
    date: '2023-10-26',
  },
];

// 获取所有文章
export function getPosts(): Post[] {
  return posts;
}

// 根据 ID 获取文章
export function getPostById(id: string): Post | undefined {
  return posts.find(post => post.id === id);
}

// 创建新文章
export function createPost(post: Omit<Post, 'id'>): Post {
  const newPost: Post = {
    ...post,
    id: Date.now().toString(), // 简单的 ID 生成
  };
  posts.push(newPost);
  return newPost;
}

3.2 博客列表页面 (SSG)

tsx

// app/blog/page.tsx
import Link from 'next/link';
import { getPosts } from '@/lib/posts';

// 这个页面将在构建时静态生成
export default function BlogListPage() {
  const posts = getPosts();

  return (
    <div>
      <h1 className="text-3xl font-bold mb-8">My Blog</h1>
      <ul className="space-y-4">
        {posts.map((post) => (
          <li key={post.id} className="border-b pb-4">
            <Link href={`/blog/${post.id}`} className="text-xl font-semibold text-blue-600 hover:underline">
              {post.title}
            </Link>
            <p className="text-gray-500 text-sm">{post.date}</p>
            <p className="mt-2">{post.content.slice(0, 100)}...</p>
          </li>
        ))}
      </ul>
    </div>
  );
}

3.3 博客文章详情页面 (动态路由 + SSG)

tsx

// app/blog/[id]/page.tsx
import { notFound } from 'next/navigation';
import { getPostById, getPosts } from '@/lib/posts';

interface BlogPostProps {
  params: {
    id: string;
  };
}

// 生成静态参数,告诉 Next.js 哪些 [id] 需要在构建时预渲染
export async function generateStaticParams() {
  const posts = getPosts();
  // 为每篇文章生成 { id: post.id }
  return posts.map((post) => ({
    id: post.id,
  }));
}

export default function BlogPostPage({ params }: BlogPostProps) {
  const post = getPostById(params.id);

  // 如果没找到文章,返回 404 页面
  if (!post) {
    notFound();
  }

  return (
    <article>
      <h1 className="text-4xl font-bold mb-4">{post.title}</h1>
      <p className="text-gray-500 mb-8">{post.date}</p>
      <div className="prose max-w-none">
        {/* 假设你使用 Tailwind @tailwindcss/typography 插件来美化内容 */}
        <p>{post.content}</p>
      </div>
    </article>
  );
}

3.4 创建文章的 API 端点

tsx

// app/api/posts/route.ts
import { NextRequest } from 'next/server';
import { createPost } from '@/lib/posts';

export async function POST(request: NextRequest) {
  try {
    const body = await request.json();
    const { title, content } = body;

    // 简单的验证
    if (!title || !content) {
      return Response.json({ error: 'Title and content are required' }, { status: 400 });
    }

    const newPost = createPost({
      title,
      content,
      date: new Date().toISOString().split('T')[0], // YYYY-MM-DD
    });

    return Response.json(newPost, { status: 201 });
  } catch (error) {
    return Response.json({ error: 'Internal Server Error' }, { status: 500 });
  }
}

3.5 创建一个表单来发布新文章 (Client Component)

tsx

// app/components/CreatePostForm.tsx
"use client";

import { useState } from 'react';
import { useRouter } from 'next/navigation';

export function CreatePostForm() {
  const [title, setTitle] = useState('');
  const [content, setContent] = useState('');
  const [isLoading, setIsLoading] = useState(false);
  const router = useRouter();

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setIsLoading(true);

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

      if (response.ok) {
        // 创建成功,重置表单并刷新博客列表
        setTitle('');
        setContent('');
        router.refresh(); // 刷新服务端组件的数据
        alert('Post created successfully!');
      } else {
        const error = await response.json();
        alert(`Error: ${error.error}`);
      }
    } catch (error) {
      alert('An error occurred while creating the post.');
    } finally {
      setIsLoading(false);
    }
  };

  return (
    <form onSubmit={handleSubmit} className="space-y-4 p-4 border rounded-lg">
      <h2 className="text-2xl font-bold">Create New Post</h2>
      <div>
        <label htmlFor="title" className="block text-sm font-medium text-gray-700">Title</label>
        <input
          type="text"
          id="title"
          value={title}
          onChange={(e) => setTitle(e.target.value)}
          required
          className="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2"
        />
      </div>
      <div>
        <label htmlFor="content" className="block text-sm font-medium text-gray-700">Content</label>
        <textarea
          id="content"
          value={content}
          onChange={(e) => setContent(e.target.value)}
          required
          rows={5}
          className="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2"
        />
      </div>
      <button
        type="submit"
        disabled={isLoading}
        className="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 disabled:opacity-50"
      >
        {isLoading ? 'Creating...' : 'Create Post'}
      </button>
    </form>
  );
}

然后,在博客列表页面引入这个表单:

tsx

// app/blog/page.tsx (更新)
import Link from 'next/link';
import { getPosts } from '@/lib/posts';
import { CreatePostForm } from '@/app/components/CreatePostForm';

export default function BlogListPage() {
  const posts = getPosts();

  return (
    <div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
      <div className="lg:col-span-2">
        <h1 className="text-3xl font-bold mb-8">My Blog</h1>
        <ul className="space-y-4">
          {posts.map((post) => (
            <li key={post.id} className="border-b pb-4">
              <Link href={`/blog/${post.id}`} className="text-xl font-semibold text-blue-600 hover:underline">
                {post.title}
              </Link>
              <p className="text-gray-500 text-sm">{post.date}</p>
              <p className="mt-2">{post.content.slice(0, 100)}...</p>
            </li>
          ))}
        </ul>
      </div>
      <div>
        <CreatePostForm />
      </div>
    </div>
  );
}

现在,你的全栈博客应用就完成了!它具备了显示文章、查看文章详情和创建新文章的功能。

第四部分:精通与优化篇 - 迈向生产环境

4.1 中间件 (Middleware)

中间件允许你在请求完成之前运行代码。它可用于身份验证、日志记录、重写 URL、修改响应头等。

创建一个 middleware.ts 文件在项目根目录(或 src 目录下)。

tsx

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  // 检查 cookie 或 header 来进行简单的认证
  const isLoggedIn = request.cookies.get('auth-token')?.value === 'secret-token';

  // 如果用户未登录且试图访问 /admin,重定向到 /login
  if (request.nextUrl.pathname.startsWith('/admin') && !isLoggedIn) {
    return NextResponse.redirect(new URL('/login', request.url));
  }

  // 可以修改响应头
  const response = NextResponse.next();
  response.headers.set('x-custom-header', 'my-value');
  return response;
}

// 配置中间件匹配的路径
export const config = {
  matcher: '/admin/:path*', // 只为 /admin 下的路径运行中间件
};

4.2 图像优化 (Next.js Image Component)

Next.js 提供了一个强大的 <Image> 组件,用于自动优化图片。

优势:

tsx

import Image from 'next/image';

export function MyComponent() {
  return (
    <div>
      {/* 本地图片 */}
      <Image
        src="/me.png" // 位于 public/ 目录
        alt="Picture of the author"
        width={500} // 必须指定
        height={500} // 必须指定
      />
      {/* 远程图片 - 需要在 next.config.js 中配置 domains */}
      <Image
        src="https://example.com/photo.jpg"
        alt="A remote image"
        width={500}
        height={300}
        // 其他常用属性
        placeholder="blur" // 加载时显示模糊占位图
        blurDataURL="data:image/jpeg;base64,..." // 小图的 base64
        priority // 优先级加载 (用于 LCP 图片)
      />
    </div>
  );
}

在 next.config.js 中配置允许的图片域名:

javascript

// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  images: {
    domains: ['example.com', 'images.unsplash.com'],
  },
};

module.exports = nextConfig;

4.3 静态资源与元数据 (Metadata)

在 App Router 中,你可以通过导出 metadata 对象来定义页面的 SEO 信息。

tsx

// app/blog/[id]/page.tsx (更新)
import { Metadata } from 'next';

// 动态生成元数据
export async function generateMetadata({ params }: BlogPostProps): Promise<Metadata> {
  const post = getPostById(params.id);

  if (!post) {
    return {
      title: 'Post Not Found',
    };
  }

  return {
    title: post.title,
    description: post.content.slice(0, 160), // 用内容前160字符做描述
    openGraph: {
      title: post.title,
      description: post.content.slice(0, 160),
      type: 'article',
      publishedTime: post.date,
    },
  };
}

// ... 原有的 page 组件代码

4.4 性能优化

  1. 使用 next/dynamic 进行动态导入(懒加载):
    将非关键的组件拆分成独立的 chunk,在需要时再加载。

    tsx

    // 动态导入一个重量的组件,比如一个图表库
    import dynamic from 'next/dynamic';
    
    const HeavyChart = dynamic(() => import('@/app/components/HeavyChart'), {
      loading: () => <p>Loading Chart...</p>,
      ssr: false, // 如果组件只在客户端运行,可以禁用 SSR
    });
    
    export function Dashboard() {
      return (
        <div>
          <h1>Dashboard</h1>
          {/* 这个组件只有在渲染时才会被加载 */}
          <HeavyChart />
        </div>
      );
    }
  2. 分析包大小:
    Next.js 内置了分析工具。运行 npm run build 后,你可以看到每个页面和依赖的分解。

    bash

    npm run build
    # 输出中会包含页面大小信息

    你也可以使用 @next/bundle-analyzer 来生成可视化的报告。

  3. 优化字体:
    Next.js 对 Google Fonts 和本地字体有很好的优化支持,能自动处理子集和缓存。

    tsx

    // 在布局或页面中
    import { Inter } from 'next/font/google';
    
    const inter = Inter({ subsets: ['latin'] });
    
    export default function RootLayout({ children }) {
      return (
        <html lang="en" className={inter.className}>
          <body>{children}</body>
        </html>
      );
    }

4.5 部署

Next.js 应用可以部署到任何支持 Node.js 的服务器,但最丝滑的体验是部署到 Vercel(Next.js 的创建者)。

部署到 Vercel:

  1. 将你的代码推送到 GitHub、GitLab 或 Bitbucket。

  2. 在 Vercel 上注册并连接你的 Git 仓库。

  3. Vercel 会自动检测到是 Next.js 项目,并配置好构建和部署设置。

  4. 点击部署。之后,每次向主分支推送代码,都会触发自动部署。

其他部署平台:

总结与展望

恭喜你!通过这篇万字长文,你已经系统地学习了 Next.js 的核心概念和高级特性。

我们从这里出发:

下一步学习方向:

  1. 状态管理: 在复杂的 Client Components 中,考虑使用 Zustand、Jotai 或 Redux Toolkit。

  2. 测试: 学习使用 Jest 和 React Testing Library 为你的组件和页面编写单元测试和集成测试。

  3. 数据库集成: 深入学习如何使用 Prisma、Drizzle ORM 或直接使用数据库驱动(如 pg for PostgreSQL)来持久化数据。

  4. 认证与授权: 集成 NextAuth.js 或 Auth.js 来处理复杂的用户登录和权限。

  5. 持续学习: Next.js 生态在快速发展,关注官方博客和文档,了解最新的特性和最佳实践。

到此这篇关于Next.js从入门到精通的文章就介绍到这了,更多相关Next.js从入门到精通内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文