Next.js 是 React 的全栈框架,核心优势是首屏秒开、SEO 友好。传统 React 在浏览器执行,用户要等 JS 下载和执行才看到内容。Next.js 把渲染搬到服务器,直接返回 HTML,打开就能看。
最近next越来越像 supabase 这些框架靠齐,想让前端把后端的事情做了:你可以在前端页面里面直接连接DB写SQL,个人用AI写代码创业的项目也可以这么玩,毕竟快是最重要的。公司的项目哪怕是node的小项目,还是建议还是要有专门的后端服务。
| 维度 | 传统 React | Next.js |
|---|---|---|
| 首屏 | 白屏 → 下载 JS → 执行 → 请求数据 → 渲染 | 服务器直接返回完整 HTML |
| SEO | 爬虫抓到空 div | 完整 HTML,SEO 友好 |
| 路由 | 手写 react-router 配置 | 文件结构就是路由 |
| 数据 | useEffect + fetch (瀑布流) | 服务端 async/await (并行) 或者 直接在页面上写SQL |
三种渲染模式
CSR - 你熟悉的 React
就是平时写的那种,全在浏览器跑:
function NewsList() {
const [news, setNews] = useState([]);
useEffect(() => {
fetch('/api/news').then(res => res.json()).then(setNews);
}, []);
return <ul>{news.map(item => <li>{item.title}</li>)}</ul>;
}用户打开页面先看一片空白,等 JS 下载完、执行完、数据请求回来才看到内容。白屏久,SEO 也不行。
SSR - 服务器先渲染好
Next.js 早期版本用的方式。服务器把页面画好给你,浏览器拿到 HTML 就能看内容,但之后还会下载 JS 重新跑一遍 hydration 将所有的shift 到一起让按钮能点击。
// pages/news.tsx (Pages Router 写法)
export async function getServerSideProps() {
const news = await fetch('https://api.example.com/news').then(r => r.json());
return { props: { news } };
}
export default function NewsList({ news }) {
return <ul>{news.map(item => <li key={item.id}>{item.title}</li>)}</ul>;
}问题是整个页面的 JS 都得发给浏览器,哪怕很多地方根本不需要交互,也能需要。
RSC - 服务器组件
Next.js 13 之后的新玩法。默认组件都在服务器跑,只有要交互的才发 JS 到浏览器。
// app/page.tsx - 服务端组件(默认,注意没有 'use client')
async function NewsPage() {
// 可以直接查数据库,不走 API
const news = await db.query('SELECT * FROM news');
return (
<article>
...
<LikeButton postId={news.id} /> {/* 只有这个按钮会打包成 JS */}
</article>
);
}
// components/LikeButton.tsx - 客户端组件(需要加 'use client')
'use client';
export function LikeButton({ postId }: { postId: number }) {
const [liked, setLiked] = useState(false);
return <button onClick={() => setLiked(!liked)}>
👍 {liked ? '已赞' : '点赞'}
</button>;
}怎么用:
- 服务端组件(默认,文件头不写东西):
- ✅ 能用
async/await、直连数据库 - ❌ 不能用
useState、useEffect、onClick - 代码不发到浏览器,安全
- ✅ 能用
- 客户端组件(文件头加
'use client'):- ✅ 能用
useState、useEffect、事件处理 - ❌ 不能直接
async(要在useEffect里) - 跟传统 React 一样
- ✅ 能用
传统 React 整个页面的 JS 都发到浏览器,Next.js RSC 只发交互组件的 JS(比如一个按钮 3KB),其他都是 HTML。
SSR vs RSC 有啥区别?
| 维度 | SSR | RSC |
|---|---|---|
| 渲染位置 | 服务器渲染,然后浏览器”水合”(重新绑定事件) | 服务端组件只在服务器,客户端组件按需加载 |
| JS 体积 | 整个页面代码都发到浏览器 | 只发客户端组件代码 |
简单说:SSR 是”服务器先做一遍,浏览器再做一遍”,RSC 是”能在服务器做的就不发到浏览器”。
路由
最重要的:在 Next.js 里不用手写路由配置(像 react-router 那样),文件夹结构就是路由。
文件结构就是路由
app/
├── layout.tsx → 根布局(所有页面共享,比如导航栏)
├── page.tsx → 首页 "/"
├── about/
│ └── page.tsx → 关于页 "/about"
├── blog/
│ ├── layout.tsx ← 博客专用布局(比如侧边栏)
│ ├── page.tsx → 博客列表 "/blog"
│ ├── loading.tsx ← 加载骨架屏(自动显示)
│ └── [id]/
│ ├── page.tsx → 博客详情 "/blog/123"
│ └── error.tsx ← 错误页面(比如 404)
├── api/ → API 路由(后端接口)
│ └── users/
│ └── route.ts → 处理 POST/GET /api/users
└── (dashboard)/ → 路由组(括号表示不影响 URL)
├── layout.tsx ← 后台专用布局
├── users/
│ └── page.tsx → "/users"(注意不是 /dashboard/users)
└── settings/
└── page.tsx → "/settings"
几个关键文件:
| 文件名 | 作用 | 什么时候用 |
|---|---|---|
page.tsx | 页面内容 | 每个路由必须有 |
layout.tsx | 共享布局 | 多个页面要共用导航栏、侧边栏 |
loading.tsx | 加载状态 | 配合 async 组件自动展示骨架屏 |
error.tsx | 错误边界 | 捕获页面错误,显示友好提示 |
route.ts | API 接口 | 处理 HTTP 请求(GET/POST 等) |
动态路由
用 [参数名] 命名文件夹,能匹配任意值:
// app/blog/[id]/page.tsx
// 匹配 /blog/123、/blog/hello-world 等
export default async function BlogPost({ params }) {
// params 是异步的,要 await
const { id } = await params;
const post = await fetch(`https://api.com/posts/${id}`).then(r => r.json());
return <article>{post.content}</article>;
}实际场景:
app/products/[id]/page.tsx→ 商品详情页/products/123app/docs/[...slug]/page.tsx→ 捕获多段路径/docs/guide/getting-started
路由组
用 (名字) 命名文件夹,能组织代码,但不会出现在 URL 里。
为啥需要? 不同页面可能要完全不同的布局,比如营销页用简洁布局,管理后台带侧边栏布局。
// app/(marketing)/layout.tsx
export default function MarketingLayout({ children }) {
return (
<div>
<SimpleHeader /> {/* 简单顶部导航 */}
{children}
</div>
);
}
// app/(dashboard)/layout.tsx
export default function DashboardLayout({ children }) {
return (
<div className="flex">
<Sidebar /> {/* 管理后台侧边栏 */}
<main>{children}</main>
</div>
);
}这样 /pricing(营销页)和 /users(后台)能用不同布局,URL 保持简洁。
数据获取
服务端组件(推荐)
直接在组件里 await 数据,不用 useState 和 useEffect。可以并行获取多个数据(不会互相等待):
// app/dashboard/page.tsx
async function Dashboard() {
const [user, stats] = await Promise.all([
db.user.findUnique({ where: { id: 1 } }),
fetch('https://api.example.com/stats').then(r => r.json()),
]);
return <div>...</div>;
}为啥比传统 React 快?
传统 React(CSR)数据流:
1. 浏览器下载 HTML(空白页)
2. 下载 JS bundle
3. 执行 JS,React 挂载
4. useEffect 触发,发请求拿 user 数据
5. user 数据回来,渲染页面
6. 子组件 useEffect 触发,发请求拿 stats 数据(瀑布流!)
7. stats 数据回来,最终渲染完成
总耗时:网络延迟 + JS 下载 + 数据请求1 + 数据请求2
Next.js 服务端组件:
1. 服务器并行获取 user 和 stats(同时进行)
2. 服务器渲染成 HTML
3. 返回给浏览器,用户立即看到完整内容
总耗时:max(数据请求1, 数据请求2) + 渲染时间
客户端组件
只在这些情况用使用客户端组件:需要用户交互(按钮、表单)、浏览器 API(localStorage、navigator)或实时数据(WebSocket)时用 'use client'。
'use client';
export function RealtimeChart() {
const [data, setData] = useState([]);
useEffect(() => {
// 建立 WebSocket 连接
const ws = new WebSocket('wss://api.example.com');
ws.onmessage = (e) => setData(JSON.parse(e.data));
return () => ws.close();
}, []);
return <Chart data={data} />;
}Server Actions
表单提交的新方式,不用手写 API 接口:
// app/todo/page.tsx
async function addTodo(formData: FormData) {
'use server'; // 标记为服务端函数
const text = formData.get('text') as string;
await db.todos.create({ data: { text } });
revalidatePath('/todo'); // 刷新页面数据
}
export default function TodoPage() {
return (
<form action={addTodo}>
<input name="text" placeholder="输入待办事项" />
<button type="submit">添加</button>
</form>
);
}好处:
- 不用创建
/api/todo接口 - 即使用户禁用 JS,表单还能提交
- TypeScript 类型安全,函数和页面在同一文件
Form 组件
<Form> 组件用于提交后跳转的场景(比如搜索表单):
import Form from 'next/form';
export default function SearchPage() {
return (
<Form action="/search">
<input name="q" placeholder="搜索..." />
<button type="submit">搜索</button>
</Form>
);
}
// app/search/page.tsx
export default async function SearchResults({ searchParams }) {
const { q } = await searchParams;
const results = await searchAPI(q);
return <div>搜索 "{q}" 的结果:{results.length} 条</div>;
}好处是自动预加载目标页面、客户端导航、JS 没加载也能用。
| 场景 | 用什么 |
|---|---|
| 提交后留在当前页(添加待办) | <form> + Server Actions |
| 提交后跳转到新页面(搜索) | <Form> 组件 |
请求去重和缓存
如果多个组件都要拿同一个用户信息,Next.js 会自动去重。即使调用了两次 getUser('123'),实际只发一次网络请求:
// lib/api.ts
import { cache } from 'react';
export const getUser = cache(async (userId: string) => {
return fetch(`http://user-service/api/users/${userId}`, {
next: { revalidate: 60 } // 缓存 60 秒后重新验证
}).then(r => r.json());
});fetch 缓存策略:
// 不缓存(默认)
fetch('https://api.example.com/data')
// 永久缓存
fetch('https://api.example.com/data', {
cache: 'force-cache'
})
// 缓存并在 60 秒后重新验证
fetch('https://api.example.com/data', {
next: { revalidate: 60 }
})BFF 场景:微服务数据聚合
如果你公司使用了微服务,也可以直接让Next.js 做 BFF (Backend for Frontend), 一直觉得这个事儿就应该前端做,而不是让后端来做一个前端要什么数据,自己组装什么数据。
方式 1:API Routes
当你要给移动端 App 或第三方提供 API 时:
// app/api/dashboard/route.ts
export async function GET(request: Request) {
const token = request.headers.get('authorization');
// 并行调多个服务
const [user, orders] = await Promise.all([
fetch('http://user-service/api/user', { ... }).then(r => r.json()),
fetch('http://order-service/api/orders', { ... }}).then(r => r.json()),
]);
// 字段转换(后端是 user_name,前端想要 userName)
return Response.json({
userName: user.user_name,
totalOrders: orders.length,
recentOrders: orders.slice(0, 5),
});
}方式 2:Server Components
不用创建 API 接口,直接在页面组件里调微服务:
// app/dashboard/page.tsx
async function Dashboard() {
const [user, orders] = await Promise.all([
fetch('http://user-service/api/user').then(r => r.json()),
fetch('http://order-service/api/orders').then(r => r.json()),
]);
return <div>...</div>;
}这种适合只给网页用时,直接在页面组件里调微服务,无需创建 API 接口,类型安全。
性能优化
Streaming 与 Suspense
传统 SSR 必须等所有数据加载完才能返回页面。Next.js 支持 Streaming,能边获取数据边返回页面。
import { Suspense } from 'react';
export default function DashboardPage() {
return (
<div>
<h1>控制面板</h1>
{/* 快速数据:立即显示 */}
<UserProfile />
{/* 慢速数据:异步加载,先显示骨架屏 */}
<Suspense fallback={<StatsLoading />}>
<Stats />
</Suspense>
<Suspense fallback={<OrdersLoading />}>
<RecentOrders />
</Suspense>
</div>
);
}
// 这个组件数据慢,但不会阻塞整个页面
async function Stats() {
const stats = await fetch('https://slow-api.com/stats').then(r => r.json());
return <div>统计数据:{stats.total}</div>;
}好处: 用户立即看到页面框架,快的部分先显示,慢的部分后显示,改善首屏时间。
Loading UI
Next.js 自动识别 loading.tsx,在页面数据加载时显示:
// app/blog/loading.tsx
export default function Loading() {
return (
<div className="animate-pulse">
<div className="h-8 bg-gray-200 rounded w-3/4 mb-4"></div>
<div className="h-4 bg-gray-200 rounded w-full mb-2"></div>
...
</div>
);
}
// app/blog/page.tsx
export default async function BlogPage() {
// 这个页面加载数据时,自动显示 loading.tsx
const posts = await fetch('https://api.com/posts').then(r => r.json());
return <PostList posts={posts} />;
}Error Handling
用 error.tsx 捕获页面错误,提供友好提示:
// app/blog/error.tsx
'use client'; // Error 组件必须是客户端组件
export default function Error({ error, reset }) {
return (
<div className="error-container">
<h2>出错了!</h2>
<p>{error.message}</p>
<button onClick={reset}>重试</button>
</div>
);
}
// app/blog/page.tsx
export default async function BlogPage() {
// 如果这里抛错误,会被 error.tsx 捕获
const posts = await fetch('https://api.com/posts').then(r => {
if (!r.ok) throw new Error('获取文章失败');
return r.json();
});
return <PostList posts={posts} />;
}Metadata API(SEO 必备)
Next.js 提供了简单的方式管理页面 SEO 信息:
// app/blog/[id]/page.tsx
import { Metadata } from 'next';
// 静态 metadata
export const metadata: Metadata = {
title: '博客',
description: '我的技术博客',
};
// 动态 metadata(根据页面内容生成)
export async function generateMetadata({
params
}: {
params: Promise<{ id: string }>
}): Promise<Metadata> {
const { id } = await params;
const post = await fetch(`https://api.com/posts/${id}`).then(r => r.json());
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
images: [post.coverImage],
...
},
};
}
export default async function BlogPost({ params }) {
const { id } = await params;
const post = await fetch(`https://api.com/posts/${id}`).then(r => r.json());
return <article>{post.content}</article>;
}Image 组件
普通 <img> 标签的问题:不会压缩、不会懒加载、图片太大拖慢页面。
Next.js 的 <Image> 自动优化:
import Image from 'next/image';
// ❌ 别用原生 img
<img src="/hero.jpg" />
// ✅ 用 Next.js 的 Image(首屏图片加 priority 立即加载)
<Image src="/hero.jpg" width={800} height={600} alt="Hero" priority />自动做了什么:
- 转成 WebP/AVIF 格式(体积小)
- 按设备尺寸提供不同大小的图(手机不会加载 4K 图)
- 懒加载(图片快进视口才加载)
- 防止布局抖动
Link 组件
普通 <a> 标签点击后会整页刷新,Next.js 的 <Link> 是 SPA 式无刷新跳转:
import Link from 'next/link';
// ❌ 别用 <a>(除非是外部链接)
<a href="/about">关于我们</a>
// ✅ 用 Link
<Link href="/about">关于我们</Link>好处: 无刷新跳转、自动预加载(链接出现在视口时,后台加载目标页面)、速度快。
Middleware
Middleware 在请求到达页面之前执行,能做鉴权、重定向、改写请求等。
重点:不像 Express 可以链式调用多个 middleware。Next.js 只能有一个 middleware.ts 文件!全部放到里面,通过
// authMiddleware.ts(放在项目根目录)
export function authMiddleware(request: NextRequest) {
const token = request.cookies.get('token');
// 没 token,重定向到登录页
if (!token) {
return NextResponse.redirect(new URL('/login', request.url));
}
return NextResponse.next();
}
// 页面的权限检查,也比较适合用middleware 来做
export function roleMiddleware(request: NextRequest) {
const role = request.cookies.get('role')?.value;
// 检查是否有admin的权限
if (request.nextUrl.pathname.startsWith('/admin') && role !== 'admin') {
return NextResponse.redirect(new URL('/403', request.url));
}
}
// 配置哪些路径要执行 middleware
export const config = {
matcher: [
'/api/:path*', // 所有 /api 下的路由
'/admin/:path*', // 所有 /admin 下的路由
],
};环境变量
Next.js 区分服务端和客户端环境变量:
# .env.local
# 只在服务端能用(安全)
DATABASE_URL=postgres://...
API_SECRET=abc123
# 客户端也能用(暴露给浏览器,加 NEXT_PUBLIC_ 前缀)
NEXT_PUBLIC_API_URL=https://api.example.com
NEXT_PUBLIC_GA_ID=G-XXXXXXXXXX使用:
// 服务端组件 - 能访问所有环境变量
export default async function ServerComponent() {
const dbUrl = process.env.DATABASE_URL; // ✅ 能访问
const apiUrl = process.env.NEXT_PUBLIC_API_URL; // ✅ 也能访问
const data = await db.query('SELECT * FROM users');
return <div>{data.length} users</div>;
}
// 客户端组件 - 只能访问 NEXT_PUBLIC_ 开头的
'use client';
export default function ClientComponent() {
const dbUrl = process.env.DATABASE_URL; // ❌ undefined(安全)
const apiUrl = process.env.NEXT_PUBLIC_API_URL; // ✅ 能访问
useEffect(() => {
fetch(apiUrl + '/data').then(...);
}, []);
}重要: 别把敏感信息(数据库密码、API 密钥)加 NEXT_PUBLIC_ 前缀,否则会暴露给浏览器!
从 React 迁移到 Next.js
如果你正在使用react,想迁移到next.js, 你可能需要这些:
- 删掉
react-router,按文件夹结构建app目录 - 默认写服务端组件,要
onClick、useState才加'use client' - 删掉
useEffect获取初始数据,改用async/await - 表单提交用 Server Actions,不用手写
/api接口 <img>改成<Image>,<a>改成<Link>- 用
generateMetadata管理 SEO,不用react-helmet - 用 Middleware 做鉴权,不用手写高阶组件
微前端
如果你产品比较大,想用微前端来做产品隔离,可以看看 Next.js 的 Multi-Zones 方案:nextjs-microfrontend