Appearance
Next.js
Next.js 是一个基于 React 的全栈 Web 框架,由 Vercel 公司开发并维护。它提供了服务器端渲染、静态站点生成、增量静态再生等功能,帮助开发者构建高性能、可扩展的 Web 应用程序。Next.js 封装了 React 应用开发中的复杂配置,提供了开箱即用的开发体验,使开发者能够专注于业务逻辑的实现。
Next.js 的核心优势在于其灵活的渲染策略。开发者可以根据页面的具体需求,选择静态生成、服务器端渲染或客户端渲染,甚至可以在同一个项目中混合使用多种渲染方式。这种灵活性使得 Next.js 能够适应各种复杂的业务场景,无论是需要 SEO 优化的营销页面,还是需要实时数据更新的交互式应用,Next.js 都能提供最优的解决方案。此外,Next.js 内置了自动代码分割、图片优化、字体优化等性能优化功能,确保应用在生产环境中能够保持出色的性能表现。
核心概念
渲染模式概述
Next.js 支持三种主要的渲染模式,每种模式都有其适用场景和特点。静态站点生成在构建时生成 HTML,适用于内容不频繁变化的页面,如博客文章、产品文档等。服务器端渲染在每次请求时动态生成 HTML,适用于需要实时数据的页面,如个性化仪表板、搜索结果页等。客户端渲染在浏览器端使用 JavaScript 渲染内容,适用于高度交互的组件和需要即时用户反馈的功能。
增量静态再生结合了静态生成和服务器端渲染的优点。它在构建时预渲染页面,同时在后台按需更新已生成的页面。这种方式既享有静态页面的高性能,又保持了数据的实时性。当用户访问一个页面时,Next.js 首先返回已缓存的版本,同时在后台重新验证或重新生成该页面,确保用户始终能够获得相对新鲜的内容,同时享受静态页面带来的快速响应。
目录结构
Next.js 项目遵循约定俗成的目录结构,使代码组织更加清晰和可维护。app 目录是 App Router 的核心,存放页面组件、布局和路由相关文件,每个子目录代表一个路由段,通过文件夹结构自动生成路由路径。public 目录存放静态资源,如图片、字体、robots.txt 等,这些文件直接通过根路径访问。src 目录用于存放源代码,通常包含 app、components、lib、styles 等子目录,将源代码与配置文件分离。
next-app/
├── app/
│ ├── layout.tsx # 根布局文件
│ ├── page.tsx # 首页组件
│ ├── globals.css # 全局样式
│ ├── about/
│ │ └── page.tsx # /about 页面
│ └── blog/
│ ├── page.tsx # /blog 页面
│ └── [slug]/
│ └── page.tsx # /blog/:slug 动态路由
├── public/
│ ├── images/
│ │ └── logo.png
│ └── favicon.ico
├── src/
│ └── app/
│ └── page.tsx
├── next.config.js # Next.js 配置
├── package.json
├── tsconfig.json
└── tailwind.config.ts配置文件
next.config.js 是 Next.js 的核心配置文件,用于定制构建过程和运行时行为。在这个文件中,开发者可以配置 React 严格模式、图像优化策略、实验性功能、静态资源处理等选项。例如,启用 React StrictMode 有助于在开发阶段发现潜在的代码问题;配置 images 域允许从外部源加载优化后的图片;设置 experimental 属性可以提前体验尚在测试阶段的新功能。
javascript
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'example.com',
pathname: '/images/**',
},
],
formats: ['image/avif', 'image/webp'],
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
},
experimental: {
serverActions: true,
optimizePackageImports: ['lucide-react', 'shadcn-ui'],
},
async headers() {
return [
{
source: '/:path*',
headers: [
{
key: 'X-Frame-Options',
value: 'DENY',
},
{
key: 'X-Content-Type-Options',
value: 'nosniff',
},
],
},
];
},
};
module.exports = nextConfig;快速开始
安装与初始化
创建新的 Next.js 项目非常简单,只需要几个步骤即可完成。确保你的系统中已安装 Node.js 版本 18.17.0 或更高版本,然后使用 create-next-app 命令快速脚手架一个新项目。这个命令会安装所有必要的依赖,包括 React、TypeScript、ESLint、Tailwind CSS 等,并配置好开箱即用的开发环境。
bash
# 使用 npm 创建项目
npx create-next-app@latest my-next-app --typescript --tailwind --eslint
# 使用 yarn 创建项目
yarn create next-app my-next-app --typescript --tailwind --eslint
# 使用 pnpm 创建项目
pnpm create next-app my-next-app --typescript --tailwind --eslint
# 使用 bun 创建项目
bun create next-app my-next-app --typescript --tailwind --eslint在创建过程中,你可以选择是否使用 TypeScript、ESLint、Tailwind CSS、App Router 等特性。推荐使用 App Router,因为它提供了更现代的路由系统和服务器组件支持。完成安装后,进入项目目录并启动开发服务器,即可开始构建你的应用。
bash
cd my-next-app
npm run dev
# 在浏览器中访问 http://localhost:3000开发环境配置
TypeScript 配置是 Next.js 项目的重要组成部分。在项目根目录下的 tsconfig.json 文件中,Next.js 会自动配置适合 React 开发的 TypeScript 选项,包括模块解析路径、严格类型检查、JSX 编译选项等。默认配置已经足够大多数项目使用,但你可以根据团队规范进行定制。
json
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"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"
}
],
"paths": {
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}ESLint 配置帮助维护代码质量和一致性。Next.js 内置了 next/core-web-vitals 规则集,涵盖了 React 组件的最佳实践、钩子使用规范、性能优化建议等。在 .eslintrc.json 文件中,你可以扩展这个规则集并添加自定义规则。
json
{
"extends": ["next/core-web-vitals"],
"rules": {
"@typescript-eslint/no-unused-vars": "warn",
"react/no-unescaped-entities": "off"
}
}文件路由
基础路由
Next.js 的 App Router 使用文件系统作为路由的基础。每个 app 目录下的文件夹代表一个路由段,文件夹名称即为路由路径。page.tsx 文件定义了对应路由的 UI 内容,是路由的入口组件。通过嵌套文件夹,可以创建嵌套路由,每个嵌套层级都会在 URL 中体现。
app/
├── page.tsx # / (首页)
├── about/
│ └── page.tsx # /about
├── products/
│ ├── page.tsx # /products
│ └── [id]/
│ └── page.tsx # /products/:id
└── blog/
├── page.tsx # /blog
└── [slug]/
└── page.tsx # /blog/:slugtsx
// app/about/page.tsx
export default function AboutPage() {
return (
<main className="min-h-screen p-8">
<h1 className="text-4xl font-bold mb-4">关于我们</h1>
<p className="text-lg text-gray-600">
这是一个使用 Next.js 构建的关于页面。
</p>
</main>
);
}动态路由
动态路由允许你创建匹配任意模式的路由段,适用于博客文章、产品详情页等场景。使用方括号 [param] 语法定义动态段,可以是必需参数,也可以是可选参数。动态段的值会作为 params 属性传递给页面组件,在服务器组件中可以直接异步获取。
tsx
// app/products/[id]/page.tsx
import { notFound } from 'next/navigation';
interface ProductPageProps {
params: {
id: string;
};
}
async function getProduct(id: string) {
const res = await fetch(`https://api.example.com/products/${id}`, {
next: { revalidate: 3600 },
});
if (!res.ok) {
return undefined;
}
return res.json();
}
export default async function ProductPage({ params }: ProductPageProps) {
const product = await getProduct(params.id);
if (!product) {
notFound();
}
return (
<main className="container mx-auto p-8">
<h1 className="text-3xl font-bold">{product.name}</h1>
<p className="text-xl mt-4">${product.price}</p>
<p className="mt-4">{product.description}</p>
</main>
);
}可选参数使用双括号 [[...slug]] 定义,表示该参数可以不存在。这在需要处理多种 URL 模式时非常有用,例如 /blog 和 /blog/:slug 可以共存。
tsx
// app/blog/[[...slug]]/page.tsx
export default function BlogPage({
params,
}: {
params: {
slug?: string[];
};
}) {
const slug = params.slug;
if (!slug) {
return <h1>博客首页</h1>;
}
return <h1>文章分类: {slug.join(' / ')}</h1>;
}路由组
路由组使用圆括号 (group) 包裹文件夹名称,它不会影响 URL 路径,但允许你对相关路由进行逻辑分组。这在需要为不同部分的路由设置不同布局时特别有用,例如管理员界面和公开界面可以使用完全不同的布局结构。
app/
├── (marketing)/
│ ├── layout.tsx # 营销页面布局
│ ├── page.tsx # /
│ └── about/
│ └── page.tsx # /about
└── (dashboard)/
├── layout.tsx # 仪表板布局
├── dashboard/
│ └── page.tsx # /dashboard
└── settings/
└── page.tsx # /dashboard/settingstsx
// app/(marketing)/layout.tsx
export default function MarketingLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="min-h-screen">
<header className="bg-blue-600 text-white p-4">
<nav>
<a href="/" className="mr-4">首页</a>
<a href="/about">关于</a>
</nav>
</header>
<main>{children}</main>
</div>
);
}布局与页面
根布局
根布局文件 app/layout.tsx 定义了所有页面的共同 UI 结构。它必须包含 <html> 和 <body> 标签,是整个应用的顶层容器。在根布局中,你可以放置全局的导航栏、页脚、字体配置、全局样式导入等。每个页面都会包裹在这个布局中,因此放在这里的内容会在页面切换时保持不变。
tsx
// app/layout.tsx
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import './globals.css';
import Navbar from '@/components/Navbar';
import Footer from '@/components/Footer';
const inter = Inter({ subsets: ['latin'] });
export const metadata: Metadata = {
title: '我的 Next.js 应用',
description: '使用 Next.js 14 构建的现代化 Web 应用',
openGraph: {
title: '我的 Next.js 应用',
description: '使用 Next.js 14 构建的现代化 Web 应用',
type: 'website',
},
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="zh-CN">
<body className={inter.className}>
<Navbar />
<div className="min-h-screen">
{children}
</div>
<Footer />
</body>
</html>
);
}嵌套布局
除了根布局,你还可以在任何路由段创建嵌套布局。这些布局会包裹对应路由段的所有子页面,并在路由切换时保持状态。嵌套布局可以访问路由参数,用于获取当前路由的上下文信息。这在创建需要保持状态的页面区域时非常有用,例如侧边栏中的选中状态。
tsx
// app/dashboard/layout.tsx
import Sidebar from '@/components/Sidebar';
export default function DashboardLayout({
children,
params,
}: {
children: React.ReactNode;
params: { teamId: string };
}) {
return (
<div className="flex h-screen">
<Sidebar teamId={params.teamId} />
<main className="flex-1 overflow-auto p-8">
{children}
</main>
</div>
);
}模板布局
模板文件 app/template.tsx 类似于布局,但在每次路由切换时都会重新创建。这对于需要在页面切换时触发动画效果或重置状态的场景非常有用。与布局不同,模板中的状态不会在页面间保持,每次导航都会触发组件的重新挂载。
tsx
// app/template.tsx
'use client';
import { useEffect } from 'react';
export default function Template({ children }: { children: React.ReactNode }) {
useEffect(() => {
console.log('模板挂载 - 页面切换');
}, []);
return (
<div className="animate-fade-in">
{children}
</div>
);
}数据获取
服务器组件数据获取
在服务器组件中,你可以直接使用 async/await 进行数据获取,这是最简单和直观的方式。Next.js 会自动缓存 fetch 请求的结果,在同一请求中重复获取相同数据时不会产生额外网络请求。对于需要实时性的数据,可以使用 revalidate 选项控制缓存策略。
tsx
// app/posts/page.tsx
interface Post {
id: number;
title: string;
body: string;
}
async function getPosts() {
const res = await fetch('https://jsonplaceholder.typicode.com/posts', {
next: { revalidate: 60 },
});
if (!res.ok) {
throw new Error('获取文章失败');
}
return res.json();
}
export default async function PostsPage() {
const posts: Post[] = await getPosts();
return (
<main className="container mx-auto p-8">
<h1 className="text-3xl font-bold mb-8">文章列表</h1>
<div className="grid gap-6">
{posts.map((post) => (
<article key={post.id} className="p-6 border rounded-lg">
<h2 className="text-xl font-semibold">{post.title}</h2>
<p className="mt-2 text-gray-600">{post.body}</p>
</article>
))}
</div>
</main>
);
}静态站点生成
静态站点生成在构建时预先渲染所有页面,生成纯 HTML 文件。这种方式提供最快的页面加载速度,非常适合内容不频繁变化的页面,如文档站点、博客、营销页面等。在 generateStaticParams 函数中定义需要预生成的路径,Next.js 会在构建时为每个路径生成对应的 HTML。
tsx
// app/blog/[slug]/page.tsx
interface Post {
slug: string;
title: string;
content: string;
}
async function getPost(slug: string): Promise<Post | null> {
const posts: Record<string, Post> = {
'hello-world': {
slug: 'hello-world',
title: '你好,世界',
content: '这是我的第一篇 Next.js 博客文章。',
},
};
return posts[slug] || null;
}
export async function generateStaticParams() {
const posts = ['hello-world', 'nextjs-guide'];
return posts.map((slug) => ({
slug,
}));
}
export default async function BlogPostPage({
params,
}: {
params: { slug: string };
}) {
const post = await getPost(params.slug);
if (!post) {
return <div>文章不存在</div>;
}
return (
<article className="max-w-2xl mx-auto py-12">
<h1 className="text-4xl font-bold">{post.title}</h1>
<div className="mt-8 prose">{post.content}</div>
</article>
);
}增量静态再生
增量静态再生允许你在已部署的应用中更新静态页面,而无需重新构建整个站点。通过设置 revalidate 时间间隔,Next.js 会在后台按需重新验证和更新过期的页面。这种方式结合了静态生成的性能和动态内容的实时性。
tsx
// app/products/page.tsx
async function getProducts() {
const res = await fetch('https://api.example.com/products', {
next: { revalidate: 3600 }, // 1小时后重新验证
});
return res.json();
}
export default async function ProductsPage() {
const products = await getProducts();
return (
<div className="grid grid-cols-3 gap-6">
{products.map((product: any) => (
<ProductCard key={product.id} product={product} />
))}
</div>
);
}API 路由
基础 API 路由
Next.js 允许你在 app/api 目录下创建 API 端点,每个文件会对应一个 RESTful 风格的 API 路由。这些 API 路由本质上是 Next.js 的服务器less 函数,可以访问数据库、执行认证逻辑、处理表单数据等。App Router 中的 API 路由默认使用服务器组件的运行时环境,支持异步操作。
typescript
// app/api/users/route.ts
import { NextResponse } from 'next/server';
interface User {
id: string;
name: string;
email: string;
}
const users: User[] = [
{ id: '1', name: '张三', email: 'zhangsan@example.com' },
{ id: '2', name: '李四', email: 'lisi@example.com' },
];
export async function GET() {
return NextResponse.json(users);
}
export async function POST(request: Request) {
const body = await request.json();
const newUser: User = {
id: String(users.length + 1),
...body,
};
users.push(newUser);
return NextResponse.json(newUser, { status: 201 });
}动态 API 路由
与页面路由类似,API 路由也支持动态参数。你可以使用方括号语法创建动态 API 端点,处理不同的资源标识符。在 API 路由函数中,可以通过 params 对象访问动态参数的值。
typescript
// app/api/users/[id]/route.ts
import { NextResponse } from 'next/server';
export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
const userId = params.id;
const user = {
id: userId,
name: '张三',
email: 'zhangsan@example.com',
};
return NextResponse.json(user);
}
export async function PUT(
request: Request,
{ params }: { params: { id: string } }
) {
const body = await request.json();
return NextResponse.json({
id: params.id,
...body,
updatedAt: new Date().toISOString(),
});
}
export async function DELETE(
request: Request,
{ params }: { params: { id: string } }
) {
return NextResponse.json({ success: true, deletedId: params.id });
}请求处理
API 路由支持所有 HTTP 方法,包括 GET、POST、PUT、PATCH、DELETE、OPTIONS 等。通过检查 request.method 属性,可以对不同的请求方法执行不同的处理逻辑。对于复杂的请求处理,建议将业务逻辑分离到独立的模块中,保持路由处理程序的简洁。
typescript
// app/api/upload/route.ts
import { NextResponse } from 'next/server';
export async function POST(request: Request) {
try {
const formData = await request.formData();
const file = formData.get('file') as File;
const name = formData.get('name') as string;
if (!file) {
return NextResponse.json(
{ error: '缺少文件' },
{ status: 400 }
);
}
const bytes = await file.arrayBuffer();
const buffer = Buffer.from(bytes);
// 这里可以将文件保存到存储服务
console.log(`上传文件: ${name}, 大小: ${buffer.length} 字节`);
return NextResponse.json({
success: true,
filename: name,
size: buffer.length,
});
} catch (error) {
return NextResponse.json(
{ error: '上传失败' },
{ status: 500 }
);
}
}样式方案
Tailwind CSS
Next.js 与 Tailwind CSS 天然集成,提供了优秀的开发体验。通过 create-next-app 创建项目时,可以选择自动配置 Tailwind。Tailwind 的原子化 CSS 理念使得样式定义变得简洁而强大,同时通过 JIT 编译器确保生成的生产代码只包含实际使用的样式类。
tsx
// app/page.tsx
export default function HomePage() {
return (
<main className="min-h-screen bg-gradient-to-br from-blue-500 to-purple-600">
<div className="container mx-auto px-4 py-16">
<h1 className="text-5xl font-bold text-white text-center mb-8">
欢迎来到 Next.js
</h1>
<p className="text-xl text-white/90 text-center max-w-2xl mx-auto">
使用 Tailwind CSS 快速构建美观的用户界面
</p>
<div className="mt-12 flex justify-center gap-4">
<button className="px-8 py-3 bg-white text-blue-600 rounded-lg font-semibold hover:bg-blue-50 transition-colors">
开始使用
</button>
<button className="px-8 py-3 border-2 border-white text-white rounded-lg font-semibold hover:bg-white/10 transition-colors">
了解更多
</button>
</div>
</div>
</main>
);
}CSS 模块
对于需要更好样式隔离的场景,CSS 模块是理想的选择。CSS 模块会自动生成唯一的类名,避免样式冲突,同时保持代码的组织性。每个模块文件 .module.css 可以导入到组件中使用,生成的类名在生产构建中会被哈希处理。
css
/* app/components/Button.module.css */
.button {
padding: 0.75rem 1.5rem;
border-radius: 0.5rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
}
.primary {
background-color: #3b82f6;
color: white;
}
.primary:hover {
background-color: #2563eb;
}
.secondary {
background-color: #f3f4f6;
color: #374151;
}
.secondary:hover {
background-color: #e5e7eb;
}tsx
// app/components/Button.tsx
import styles from './Button.module.css';
interface ButtonProps {
children: React.ReactNode;
variant?: 'primary' | 'secondary';
onClick?: () => void;
}
export default function Button({
children,
variant = 'primary',
onClick
}: ButtonProps) {
return (
<button
className={`${styles.button} ${styles[variant]}`}
onClick={onClick}
>
{children}
</button>
);
}全局样式
全局样式文件 app/globals.css 用于定义整个应用的全局样式规则,包括 CSS 重置、自定义 CSS 变量、全局字体设置等。在这个文件中,你可以定义与应用主题相关的 CSS 变量,然后在组件中通过 Tailwind 或直接引用这些变量。
css
/* app/globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--primary-color: #3b82f6;
--secondary-color: #8b5cf6;
--background: #ffffff;
--foreground: #171717;
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}
body {
color: var(--foreground);
background: var(--background);
font-family: system-ui, -apple-system, sans-serif;
}
@layer utilities {
.text-balance {
text-wrap: balance;
}
}优化特性
图片优化
Next.js 的 Image 组件提供了强大的图片优化功能,包括自动调整尺寸、格式转换、懒加载等。使用 next/image 替代原生 <img> 标签,可以显著提升页面性能。组件会自动根据设备和浏览器支持情况,选择最优的图片格式和尺寸。
tsx
// app/components/ProductImage.tsx
import Image from 'next/image';
interface ProductImageProps {
src: string;
alt: string;
width?: number;
height?: number;
}
export default function ProductImage({
src,
alt,
width = 400,
height = 400
}: ProductImageProps) {
return (
<div className="relative aspect-square overflow-hidden rounded-lg">
<Image
src={src}
alt={alt}
fill
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
className="object-cover transition-transform hover:scale-105"
priority={false}
placeholder="blur"
blurDataURL="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="
/>
</div>
);
}字体优化
Next.js 的 next/font 模块提供了内置的字体优化功能。它会自动下载 Google Fonts 或本地字体,在构建时将其转换为优化的格式,并托管在本地服务器上。这样可以避免字体加载导致的布局偏移,同时减少外部网络请求。
tsx
// app/layout.tsx
import { Inter, JetBrains_Mono } from 'next/font/google';
const inter = Inter({
subsets: ['latin'],
display: 'swap',
variable: '--font-inter',
fallback: ['system-ui', 'sans-serif'],
});
const jetbrainsMono = JetBrains_Mono({
subsets: ['latin'],
display: 'swap',
variable: '--font-mono',
fallback: ['monospace'],
});
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="zh-CN" className={`${inter.variable} ${jetbrainsMono.variable}`}>
<body className="font-sans antialiased">
{children}
</body>
</html>
);
}脚本优化
next/script 组件提供了对第三方脚本加载的精细控制。通过设置 strategy 属性,可以选择在页面空闲时加载脚本、延迟加载、立即执行或使用 Worker 线程。这对于集成分析工具、广告脚本等第三方服务非常有用,同时不会阻塞主线程影响页面性能。
tsx
// app/layout.tsx
import Script from 'next/script';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="zh-CN">
<head>
<title>我的应用</title>
</head>
<body>
{children}
<Script
src="https://analytics.example.com/script.js"
strategy="afterInteractive"
onLoad={() => {
console.log('分析脚本已加载');
}}
/>
<Script
id="ads-pixel"
strategy="afterInteractive"
dangerouslySetInnerHTML={{
__html: `
!function(f,b,e,v,n,t,s)
{if(f.fbq)return;n=f.fbq=function(){n.callMethod?
n.callMethod.apply(n,arguments):n.queue.push(arguments)};
if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';
n.queue=[];t=b.createElement(e);t.async=!0;
t.src=v;s=b.getElementsByTagName(e)[0];
s.parentNode.insertBefore(t,s)}(window, document,'script',
'https://connect.facebook.net/en_US/fbevents.js');
`,
}}
/>
</body>
</html>
);
}部署选项
Vercel 部署
Vercel 是 Next.js 的创建者提供的托管平台,提供了最流畅的部署体验。只需将代码推送到 Git 仓库,Vercel 会自动检测 Next.js 项目并配置构建和部署流程。每次推送到主分支都会触发自动部署,同时提供预览部署功能,让你可以预览 pull request 的效果。Vercel 还提供全球 CDN 边缘网络、自动 HTTPS、自定义域名等特性。
bash
# 安装 Vercel CLI
npm i -g vercel
# 登录 Vercel
vercel login
# 部署项目
vercel自托管部署
Next.js 也支持在任何支持 Node.js 的环境中部署,包括传统服务器、Docker 容器、Kubernetes 集群等。使用 next start 命令启动生产服务器,或使用自定义服务器实现更复杂的部署需求。Docker 部署是自托管的常用方式,通过 Dockerfile 定义应用的构建和运行过程。
dockerfile
# Dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
EXPOSE 3000
CMD ["node", "server.js"]Edge 部署
Next.js 支持在边缘运行时部署函数,提供更低的延迟和更好的全球可用性。通过使用 Edge Runtime,可以将 API 路由和部分页面逻辑部署到全球分布的边缘节点。这种方式特别适合需要低延迟的实时应用、个性化内容和 A/B 测试场景。
typescript
// app/api/edge/route.ts
export const runtime = 'edge';
export default async function handler(request: Request) {
const { searchParams } = new URL(request.url);
const name = searchParams.get('name') || 'World';
return new Response(
JSON.stringify({
message: `你好,${name}!`,
timestamp: Date.now(),
}),
{
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'public, s-maxage=86400',
},
}
);
}最佳实践
项目结构组织
良好的项目结构组织是保持代码库可维护性的基础。建议将组件按功能分类存放,使用清晰的命名约定。共享组件放在 components 目录下,按原子级别或功能模块组织。工具函数和业务逻辑分离到 lib 目录。类型定义统一放在 types 目录。API 相关的代码可以放在 services 或 api 目录中。
src/
├── app/ # App Router 页面
│ ├── (auth)/ # 认证相关路由
│ ├── (dashboard)/ # 仪表板路由
│ ├── api/ # API 路由
│ └── globals.css
├── components/
│ ├── ui/ # 基础 UI 组件
│ ├── forms/ # 表单组件
│ ├── layout/ # 布局组件
│ └── features/ # 功能组件
├── lib/
│ ├── utils.ts # 工具函数
│ ├── api.ts # API 封装
│ └── constants.ts # 常量定义
├── hooks/ # 自定义 Hooks
├── services/ # 业务服务层
├── types/ # TypeScript 类型
└── styles/ # 全局样式性能优化策略
性能优化是 Next.js 应用开发的重要环节。首先,确保对静态资源进行适当压缩和缓存。其次,使用 next/image 和 next/font 自动优化媒体文件和字体。第三,合理使用动态导入减少初始加载体积。第四,对复杂计算使用 memo 和 useMemo 避免不必要的重复计算。第五,利用服务器组件减少客户端 JavaScript 体积。
tsx
// 使用动态导入优化性能
import dynamic from 'next/dynamic';
const HeavyChart = dynamic(() => import('@/components/HeavyChart'), {
loading: () => <div>加载中...</div>,
ssr: false, // 禁用服务端渲染
});
export default function Page() {
return (
<div>
<h1>仪表板</h1>
<HeavyChart data={[1, 2, 3, 4, 5]} />
</div>
);
}安全实践
确保 Next.js 应用的安全性需要关注多个方面。首先,使用 next/headers 和 next/cookies 时注意敏感信息的处理。其次,API 路由中始终进行输入验证和错误处理。第三,配置适当的 Content Security Policy 防止 XSS 攻击。第四,对用户生成的内容进行适当的转义和清理。第五,使用环境变量管理敏感配置,避免硬编码密钥。
typescript
// app/api/protected/route.ts
import { NextResponse } from 'next/server';
import { headers } from 'next/headers';
export async function GET() {
const headersList = headers();
const apiKey = headersList.get('x-api-key');
if (!apiKey || apiKey !== process.env.API_KEY) {
return NextResponse.json(
{ error: '未授权' },
{ status: 401 }
);
}
return NextResponse.json({
data: '受保护的内容',
timestamp: new Date().toISOString(),
});
}