Next.js從入門到精通的萬字長(zhǎng)文教程(附詳細(xì)代碼)
引言:為什么是 Next.js?
在 React 生態(tài)系統(tǒng)中,我們常常面臨一些挑戰(zhàn):如何配置服務(wù)器端渲染(SSR)以提升首屏加載速度和 SEO?如何實(shí)現(xiàn)靜態(tài)站點(diǎn)生成(SSG)以獲得極致的性能?如何優(yōu)雅地處理路由、API 接口、代碼分割和打包優(yōu)化?
Next.js 的出現(xiàn),完美地回答了這些問題。它是由 Vercel 公司創(chuàng)建的基于 React 的全功能生產(chǎn)級(jí)框架。它提供了一系列開箱即用的特性,讓你能專注于業(yè)務(wù)邏輯而非構(gòu)建配置。從簡(jiǎn)單的個(gè)人博客到復(fù)雜的電子商務(wù)平臺(tái),Next.js 都能勝任。
Next.js 的核心優(yōu)勢(shì):
零配置: 內(nèi)置 webpack、Babel、代碼分割、熱更新等,開箱即用。
服務(wù)端渲染: 輕松實(shí)現(xiàn) SSR 和 SSG,提升性能和 SEO。
文件系統(tǒng)路由: 在
pages目錄下創(chuàng)建文件即可自動(dòng)生成路由。API 路由: 在同一個(gè)項(xiàng)目中編寫前端和后端 API。
出色的開發(fā)者體驗(yàn): 快速刷新、強(qiáng)大的錯(cuò)誤提示等。
強(qiáng)大的社區(qū)和生態(tài): 擁有龐大的社區(qū)支持和豐富的插件。
本教程將分為四個(gè)部分:
入門篇: 創(chuàng)建第一個(gè) Next.js 應(yīng)用,理解核心概念。
核心概念篇: 深入探討路由、數(shù)據(jù)獲取、樣式和 API 路由。
進(jìn)階實(shí)戰(zhàn)篇: 構(gòu)建一個(gè)完整的全棧博客應(yīng)用。
精通與優(yōu)化篇: 探索高級(jí)特性、性能優(yōu)化和部署。
第一部分:入門篇 - 創(chuàng)建你的第一個(gè) Next.js 應(yīng)用
1.1 環(huán)境準(zhǔn)備與項(xiàng)目創(chuàng)建
首先,確保你的系統(tǒng)安裝了 Node.js (版本 14.6.0 或更高)。
Next.js 提供了官方的創(chuàng)建工具 create-next-app,它能快速搭建一個(gè)預(yù)配置好的 Next.js 項(xiàng)目。
打開你的終端,執(zhí)行以下命令:
bash
npx create-next-app@latest my-nextjs-app
你會(huì)被提示選擇一些配置項(xiàng):
Would you like to use TypeScript?
No / Yes(推薦選擇 Yes)Would you like to use ESLint?
Yes(推薦)Would you like to use Tailwind CSS?
No / Yes(根據(jù)喜好,這里我們先選 No)Would you like to use src/ directory?
No / Yes(推薦 Yes,保持項(xiàng)目結(jié)構(gòu)清晰)Would you like to use App Router?
Yes(推薦,這是 Next.js 13+ 的現(xiàn)代路由)
我們選擇使用 App Router,它是 Next.js 13 引入的基于 React Server Components 的新路由系統(tǒng),功能更強(qiáng)大,是未來的方向。
等待依賴安裝完成后,進(jìn)入項(xiàng)目目錄并啟動(dòng)開發(fā)服務(wù)器:
bash
cd my-nextjs-app npm run dev
在瀏覽器中打開 http://localhost:3000,你將看到 Next.js 的歡迎頁(yè)面。
1.2 項(xiàng)目結(jié)構(gòu)初探
使用 create-next-app 并選擇上述配置后,你的項(xiàng)目結(jié)構(gòu)大致如下:
text
my-nextjs-app/ ├── app/ # App Router 的主目錄 │ ├── favicon.ico │ ├── globals.css # 全局樣式 │ ├── layout.tsx # 根布局組件 │ ├── page.tsx # 首頁(yè)組件 (對(duì)應(yīng)路由 `/`) │ └── ... ├── public/ # 靜態(tài)資源目錄 (圖片、字體等) │ └── ... ├── next.config.js # Next.js 配置文件 ├── package.json ├── eslint.config.mjs # ESLint 配置 └── tsconfig.json # TypeScript 配置
關(guān)鍵文件解釋:
app/layout.tsx: 這是根布局。所有頁(yè)面都會(huì)共享這個(gè)布局中的內(nèi)容(如<html>和<body>標(biāo)簽)。它是 Server Component。app/page.tsx: 這是你的首頁(yè)。當(dāng)你訪問/時(shí),渲染的就是這個(gè)組件。public/: 這個(gè)目錄下的文件可以被直接訪問。例如,public/logo.png可以通過/logo.png在瀏覽器中訪問。
1.3 理解 React Server Components vs Client Components
這是 App Router 的核心概念。
Server Components (默認(rèn)):
在服務(wù)器上運(yùn)行。 你的組件代碼不會(huì)被打包發(fā)送到客戶端。
可以直接訪問后端資源。 如數(shù)據(jù)庫(kù)、API 密鑰、文件系統(tǒng),無需通過 API 路由。
有助于減少客戶端包大小。 因?yàn)榇a在服務(wù)端執(zhí)行。
不能使用狀態(tài)和生命周期。 不能使用
useState,useEffect等 Hook。不能使用瀏覽器 API。 如
window,document。
Client Components:
在客戶端(瀏覽器)運(yùn)行。 和傳統(tǒng)的 React 組件一樣。
可以使用狀態(tài)和生命周期。 可以使用所有 React Hook。
可以處理交互性。 如點(diǎn)擊事件、表單提交。
需要使用 "use client" 指令。 在文件頂部聲明。
示例: 讓我們修改 app/page.tsx,看看它們?nèi)绾螀f(xié)作。
tsx
// app/page.tsx
// 這是一個(gè) Server Component (默認(rèn))
// 一個(gè)在服務(wù)器端獲取數(shù)據(jù)的異步函數(shù) (模擬)
async function fetchServerSideData() {
// 這里可以直接訪問數(shù)據(jù)庫(kù)或內(nèi)部 API
await new Promise(resolve => setTimeout(resolve, 1000)); // 模擬延遲
return { message: "Hello from the Server!" };
}
// 一個(gè)普通的 Server Component
function ServerGreeting() {
return <p>I was rendered on the server.</p>;
}
export default async function HomePage() {
// 在 Server Component 中可以直接使用 async/await 獲取數(shù)據(jù)
const data = await fetchServerSideData();
return (
<div>
<h1>My First Next.js App</h1>
<p>{data.message}</p>
<ServerGreeting />
{/* 我們引入一個(gè) Client Component */}
<ClientCounter />
</div>
);
}
// --- 新建一個(gè) Client Component ---
// 在 app/components/ClientCounter.tsx
"use client"; // <-- 必須的指令,標(biāo)記這是一個(gè) 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>
);
}在這個(gè)例子中:
HomePage,fetchServerSideData,ServerGreeting都在服務(wù)器端執(zhí)行。ClientCounter因?yàn)橛辛?nbsp;"use client",它是一個(gè) Client Component,其代碼會(huì)被打包發(fā)送到瀏覽器,處理點(diǎn)擊事件和狀態(tài)更新。
這種混合模式讓你能夠?yàn)轫?yè)面的每個(gè)部分選擇最合適的渲染環(huán)境,從而構(gòu)建出高性能的應(yīng)用。
第二部分:核心概念篇 - 深入 Next.js 的基石
2.1 基于文件系統(tǒng)的路由 (App Router)
在 App Router 中,路由由 app 目錄下的文件夾結(jié)構(gòu)定義。每個(gè)文件夾代表一個(gè)路由段(segment)。要?jiǎng)?chuàng)建一個(gè)頁(yè)面,你需要使用 page.tsx 或 page.jsx 文件。
基礎(chǔ)路由:
| 文件路徑 | 對(duì)應(yīng)路由 |
|---|---|
app/page.tsx | / |
app/about/page.tsx | /about |
app/blog/page.tsx | /blog |
動(dòng)態(tài)路由:
如果你需要捕獲動(dòng)態(tài)的路由段(如博客文章 ID),可以使用方括號(hào) [folderName] 來創(chuàng)建動(dòng)態(tài)路由。
| 文件路徑 | 對(duì)應(yīng)路由 | 示例 URL |
|---|---|---|
app/blog/[id]/page.tsx | /blog/:id | /blog/123, /blog/my-post |
在 page.tsx 中,你可以通過 params 屬性訪問到這個(gè)動(dòng)態(tài)參數(shù)。
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,它在多個(gè)頁(yè)面之間保持狀態(tài),并且不會(huì)在路由切換時(shí)重新渲染。它由 layout.tsx 文件定義。
根布局 (app/layout.tsx): 必須的,它包裹所有頁(yè)面。
路由組布局: 可以在任何子目錄下創(chuàng)建
layout.tsx,它只包裹該目錄下的頁(yè)面。
tsx
// app/layout.tsx
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
{/* 一個(gè)所有頁(yè)面共享的導(dǎo)航欄 */}
<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>
{/* 子頁(yè)面將在這里被渲染 */}
<main className="container mx-auto p-4">{children}</main>
{/* 一個(gè)所有頁(yè)面共享的頁(yè)腳 */}
<footer className="bg-gray-200 p-4 text-center">
<p>© 2023 My Site</p>
</footer>
</body>
</html>
);
}2.2 數(shù)據(jù)獲取 (Data Fetching)
Next.js 在 Server Components 中擴(kuò)展了 fetch API,提供了強(qiáng)大的數(shù)據(jù)獲取能力,包括自動(dòng)緩存和重復(fù)數(shù)據(jù)刪除。
三種渲染模式:
靜態(tài)站點(diǎn)生成: 頁(yè)面在構(gòu)建時(shí)生成。適用于內(nèi)容不經(jīng)常變化的頁(yè)面(如博客、文檔、營(yíng)銷頁(yè)面)。性能最好。
服務(wù)端渲染: 頁(yè)面在每次請(qǐng)求時(shí)生成。適用于高度動(dòng)態(tài)、個(gè)性化的頁(yè)面(如儀表盤、用戶主頁(yè))。
客戶端渲染: 頁(yè)面在瀏覽器中生成。適用于具有大量交互、不關(guān)心 SEO 的頁(yè)面部分。
在 App Router 中,你通過 fetch 的選項(xiàng)來控制渲染模式。
tsx
// app/blog/page.tsx
// 這是一個(gè) Server Component
interface Post {
id: number;
title: string;
body: string;
}
// SSG (Static Generation) - 默認(rèn)行為
// 在構(gòu)建時(shí)獲取數(shù)據(jù)并生成靜態(tài)頁(yè)面
async function getStaticPosts(): Promise<Post[]> {
const res = await fetch('https://jsonplaceholder.typicode.com/posts', {
// cache: 'force-cache' 是默認(rèn)值,等同于 SSG
});
if (!res.ok) {
throw new Error('Failed to fetch posts');
}
return res.json();
}
// SSR (Server-Side Rendering)
// 在每次請(qǐng)求時(shí)獲取數(shù)據(jù)
async function getServerSidePosts(): Promise<Post[]> {
const res = await fetch('https://jsonplaceholder.typicode.com/posts', {
cache: 'no-store', // <-- 這表示不緩存,每次請(qǐng)求都重新獲取
});
if (!res.ok) {
throw new Error('Failed to fetch posts');
}
return res.json();
}
// ISR (Incremental Static Regeneration)
// 靜態(tài)生成,但可以定期在后臺(tái)重新驗(yàn)證和更新
async function getIncrementalPosts(): Promise<Post[]> {
const res = await fetch('https://jsonplaceholder.typicode.com/posts', {
next: { revalidate: 60 }, // <-- 每60秒重新驗(yàn)證一次
});
if (!res.ok) {
throw new Error('Failed to fetch posts');
}
return res.json();
}
export default async function BlogListPage() {
// 選擇一種數(shù)據(jù)獲取方式
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: 在
app/globals.css中導(dǎo)入,會(huì)應(yīng)用到所有頁(yè)面。CSS Modules: 創(chuàng)建
[name].module.css文件,樣式局部作用于組件。Tailwind CSS: 功能優(yōu)先的 CSS 框架,與 Next.js 集成度極高。
Styled-components / Emotion: CSS-in-JS 庫(kù)(通常需要在 Client Components 中使用)。
Sass: 支持
.scss和.sass文件。
以 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 目錄下創(chuàng)建 API 端點(diǎn),這意味著你可以在同一個(gè)項(xiàng)目中編寫全棧應(yīng)用。
API 路由位于 app/api/ 目錄下,使用 route.ts 文件定義。
創(chuàng)建一個(gè)簡(jiǎn)單的 GET API:
tsx
// app/api/hello/route.ts
export async function GET() {
// 這里可以連接數(shù)據(jù)庫(kù),處理業(yè)務(wù)邏輯
return Response.json({ message: 'Hello from the API!' });
}現(xiàn)在,訪問 http://localhost:3000/api/hello 將會(huì)返回 JSON 數(shù)據(jù)。
處理不同的 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 中的數(shù)據(jù)保存到數(shù)據(jù)庫(kù)
console.log('Creating user with data:', body);
// 模擬創(chuàng)建用戶
const newUser = { id: 3, ...body };
return Response.json(newUser, { status: 201 });
}你可以在 Client Component 中使用 fetch 來調(diào)用這些 API。
tsx
"use client";
// ... 在某個(gè) 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);
};第三部分:進(jìn)階實(shí)戰(zhàn)篇 - 構(gòu)建一個(gè)全棧博客應(yīng)用
現(xiàn)在,讓我們運(yùn)用所學(xué)的知識(shí),構(gòu)建一個(gè)簡(jiǎn)單的全棧博客應(yīng)用。它將包含:
博客文章列表(SSG)
單篇博客文章頁(yè)面(動(dòng)態(tài)路由 + SSG)
一個(gè)用于創(chuàng)建新博客文章的 API 端點(diǎn)
3.1 項(xiàng)目結(jié)構(gòu)與數(shù)據(jù)層
我們使用一個(gè)本地的 JSON 文件來模擬數(shù)據(jù)庫(kù)。在實(shí)際項(xiàng)目中,你會(huì)使用 Prisma、Drizzle 等 ORM 連接 PostgreSQL、MySQL 等數(shù)據(jù)庫(kù)。
創(chuàng)建 lib/posts.ts 來處理數(shù)據(jù):
tsx
// lib/posts.ts
export interface Post {
id: string;
title: string;
content: string;
date: string;
}
// 模擬數(shù)據(jù)
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;
}
// 根據(jù) ID 獲取文章
export function getPostById(id: string): Post | undefined {
return posts.find(post => post.id === id);
}
// 創(chuàng)建新文章
export function createPost(post: Omit<Post, 'id'>): Post {
const newPost: Post = {
...post,
id: Date.now().toString(), // 簡(jiǎn)單的 ID 生成
};
posts.push(newPost);
return newPost;
}3.2 博客列表頁(yè)面 (SSG)
tsx
// app/blog/page.tsx
import Link from 'next/link';
import { getPosts } from '@/lib/posts';
// 這個(gè)頁(yè)面將在構(gòu)建時(shí)靜態(tài)生成
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 博客文章詳情頁(yè)面 (動(dòng)態(tài)路由 + SSG)
tsx
// app/blog/[id]/page.tsx
import { notFound } from 'next/navigation';
import { getPostById, getPosts } from '@/lib/posts';
interface BlogPostProps {
params: {
id: string;
};
}
// 生成靜態(tài)參數(shù),告訴 Next.js 哪些 [id] 需要在構(gòu)建時(shí)預(yù)渲染
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 頁(yè)面
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">
{/* 假設(shè)你使用 Tailwind @tailwindcss/typography 插件來美化內(nèi)容 */}
<p>{post.content}</p>
</div>
</article>
);
}3.4 創(chuàng)建文章的 API 端點(diǎn)
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;
// 簡(jiǎn)單的驗(yàn)證
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 創(chuàng)建一個(gè)表單來發(fā)布新文章 (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) {
// 創(chuàng)建成功,重置表單并刷新博客列表
setTitle('');
setContent('');
router.refresh(); // 刷新服務(wù)端組件的數(shù)據(jù)
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>
);
}然后,在博客列表頁(yè)面引入這個(gè)表單:
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>
);
}現(xiàn)在,你的全棧博客應(yīng)用就完成了!它具備了顯示文章、查看文章詳情和創(chuàng)建新文章的功能。
第四部分:精通與優(yōu)化篇 - 邁向生產(chǎn)環(huán)境
4.1 中間件 (Middleware)
中間件允許你在請(qǐng)求完成之前運(yùn)行代碼。它可用于身份驗(yàn)證、日志記錄、重寫 URL、修改響應(yīng)頭等。
創(chuàng)建一個(gè) middleware.ts 文件在項(xiàng)目根目錄(或 src 目錄下)。
tsx
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
// 檢查 cookie 或 header 來進(jìn)行簡(jiǎn)單的認(rèn)證
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));
}
// 可以修改響應(yīng)頭
const response = NextResponse.next();
response.headers.set('x-custom-header', 'my-value');
return response;
}
// 配置中間件匹配的路徑
export const config = {
matcher: '/admin/:path*', // 只為 /admin 下的路徑運(yùn)行中間件
};4.2 圖像優(yōu)化 (Next.js Image Component)
Next.js 提供了一個(gè)強(qiáng)大的 <Image> 組件,用于自動(dòng)優(yōu)化圖片。
優(yōu)勢(shì):
自動(dòng)優(yōu)化: 提供 WebP、AVIF 等現(xiàn)代格式。
懶加載: 圖片僅在進(jìn)入視口時(shí)加載。
防止布局偏移: 自動(dòng)設(shè)置寬度和高度。
響應(yīng)式: 配合
sizes屬性,提供響應(yīng)式圖片。
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} // 必須指定
/>
{/* 遠(yuǎn)程圖片 - 需要在 next.config.js 中配置 domains */}
<Image
src="https://example.com/photo.jpg"
alt="A remote image"
width={500}
height={300}
// 其他常用屬性
placeholder="blur" // 加載時(shí)顯示模糊占位圖
blurDataURL="data:image/jpeg;base64,..." // 小圖的 base64
priority // 優(yōu)先級(jí)加載 (用于 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 靜態(tài)資源與元數(shù)據(jù) (Metadata)
在 App Router 中,你可以通過導(dǎo)出 metadata 對(duì)象來定義頁(yè)面的 SEO 信息。
tsx
// app/blog/[id]/page.tsx (更新)
import { Metadata } from 'next';
// 動(dòng)態(tài)生成元數(shù)據(jù)
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), // 用內(nèi)容前160字符做描述
openGraph: {
title: post.title,
description: post.content.slice(0, 160),
type: 'article',
publishedTime: post.date,
},
};
}
// ... 原有的 page 組件代碼4.4 性能優(yōu)化
使用 next/dynamic 進(jìn)行動(dòng)態(tài)導(dǎo)入(懶加載):
將非關(guān)鍵的組件拆分成獨(dú)立的 chunk,在需要時(shí)再加載。tsx
// 動(dòng)態(tài)導(dǎo)入一個(gè)重量的組件,比如一個(gè)圖表庫(kù) import dynamic from 'next/dynamic'; const HeavyChart = dynamic(() => import('@/app/components/HeavyChart'), { loading: () => <p>Loading Chart...</p>, ssr: false, // 如果組件只在客戶端運(yùn)行,可以禁用 SSR }); export function Dashboard() { return ( <div> <h1>Dashboard</h1> {/* 這個(gè)組件只有在渲染時(shí)才會(huì)被加載 */} <HeavyChart /> </div> ); }分析包大小:
Next.js 內(nèi)置了分析工具。運(yùn)行npm run build后,你可以看到每個(gè)頁(yè)面和依賴的分解。bash
npm run build # 輸出中會(huì)包含頁(yè)面大小信息
你也可以使用
@next/bundle-analyzer來生成可視化的報(bào)告。優(yōu)化字體:
Next.js 對(duì) Google Fonts 和本地字體有很好的優(yōu)化支持,能自動(dòng)處理子集和緩存。tsx
// 在布局或頁(yè)面中 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 應(yīng)用可以部署到任何支持 Node.js 的服務(wù)器,但最絲滑的體驗(yàn)是部署到 Vercel(Next.js 的創(chuàng)建者)。
部署到 Vercel:
將你的代碼推送到 GitHub、GitLab 或 Bitbucket。
在 Vercel 上注冊(cè)并連接你的 Git 倉(cāng)庫(kù)。
Vercel 會(huì)自動(dòng)檢測(cè)到是 Next.js 項(xiàng)目,并配置好構(gòu)建和部署設(shè)置。
點(diǎn)擊部署。之后,每次向主分支推送代碼,都會(huì)觸發(fā)自動(dòng)部署。
其他部署平臺(tái):
Netlify: 同樣提供優(yōu)秀的體驗(yàn)。
AWS / GCP / Azure: 可以使用 Docker 或平臺(tái)特定的方式部署。
你自己的服務(wù)器: 運(yùn)行
npm run build然后npm start。
總結(jié)與展望
恭喜你!通過這篇萬字長(zhǎng)文,你已經(jīng)系統(tǒng)地學(xué)習(xí)了 Next.js 的核心概念和高級(jí)特性。
我們從這里出發(fā):
理解了 Next.js 的優(yōu)勢(shì)和項(xiàng)目創(chuàng)建。
掌握了 App Router、Server/Client Components、路由、布局和數(shù)據(jù)獲取。
實(shí)戰(zhàn)構(gòu)建了一個(gè)具備 CRUD 功能的全棧博客。
探索了中間件、圖像優(yōu)化、元數(shù)據(jù)和性能優(yōu)化等生產(chǎn)級(jí)特性。
下一步學(xué)習(xí)方向:
狀態(tài)管理: 在復(fù)雜的 Client Components 中,考慮使用 Zustand、Jotai 或 Redux Toolkit。
測(cè)試: 學(xué)習(xí)使用 Jest 和 React Testing Library 為你的組件和頁(yè)面編寫單元測(cè)試和集成測(cè)試。
數(shù)據(jù)庫(kù)集成: 深入學(xué)習(xí)如何使用 Prisma、Drizzle ORM 或直接使用數(shù)據(jù)庫(kù)驅(qū)動(dòng)(如
pgfor PostgreSQL)來持久化數(shù)據(jù)。認(rèn)證與授權(quán): 集成 NextAuth.js 或 Auth.js 來處理復(fù)雜的用戶登錄和權(quán)限。
持續(xù)學(xué)習(xí): Next.js 生態(tài)在快速發(fā)展,關(guān)注官方博客和文檔,了解最新的特性和最佳實(shí)踐。
到此這篇關(guān)于Next.js從入門到精通的文章就介紹到這了,更多相關(guān)Next.js從入門到精通內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
JavaScript中null和undefined區(qū)別小結(jié)
本文主要介紹了JavaScript中null和undefined區(qū)別小結(jié),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2026-03-03
微信小程序跨頁(yè)面?zhèn)鬟fdata數(shù)據(jù)方法解析
這篇文章主要介紹了微信小程序跨頁(yè)面?zhèn)鬟fdata數(shù)據(jù)方法解析,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-12-12
利用JS實(shí)現(xiàn)獲取當(dāng)前系統(tǒng)電量情況
在前端瀏覽器中我們可以通過使用JavaScript的navigator.getBattery()方法來獲取當(dāng)前系統(tǒng)的電池情況,本文將介紹如何使用這個(gè)API以及它在實(shí)際應(yīng)用中的使用,需要的可以參考下2023-12-12
javascript onkeydown,onkeyup,onkeypress,onclick,ondblclick
昨天群里面的朋友問了個(gè)比較有意思的問題,keydown,keyup,keypress事件的先后順序。2009-02-02
JavaScript實(shí)現(xiàn)音樂導(dǎo)航效果
這篇文章主要為大家詳細(xì)介紹了JavaScript實(shí)現(xiàn)音樂導(dǎo)航效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-11-11
微信小程序?qū)崿F(xiàn)獲取小程序碼和二維碼java接口開發(fā)
這篇文章主要介紹了微信小程序?qū)崿F(xiàn)獲取小程序碼和二維碼java接口開發(fā),小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2019-03-03

