Next.js 是 React 的全栈框架,核心优势是首屏秒开、SEO 友好。传统 React 在浏览器执行,用户要等 JS 下载和执行才看到内容。Next.js 把渲染搬到服务器,直接返回 HTML,打开就能看。

最近next越来越像 supabase 这些框架靠齐,想让前端把后端的事情做了:你可以在前端页面里面直接连接DB写SQL,个人用AI写代码创业的项目也可以这么玩,毕竟快是最重要的。公司的项目哪怕是node的小项目,还是建议还是要有专门的后端服务。

维度传统 ReactNext.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、直连数据库
    • ❌ 不能用 useStateuseEffectonClick
    • 代码不发到浏览器,安全
  • 客户端组件(文件头加 'use client'):
    • ✅ 能用 useStateuseEffect、事件处理
    • ❌ 不能直接 async(要在 useEffect 里)
    • 跟传统 React 一样

传统 React 整个页面的 JS 都发到浏览器,Next.js RSC 只发交互组件的 JS(比如一个按钮 3KB),其他都是 HTML。

SSR vs RSC 有啥区别?

维度SSRRSC
渲染位置服务器渲染,然后浏览器”水合”(重新绑定事件)服务端组件只在服务器,客户端组件按需加载
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.tsAPI 接口处理 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/123
  • app/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 数据,不用 useStateuseEffect。可以并行获取多个数据(不会互相等待):

// 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(localStoragenavigator)或实时数据(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 图)
  • 懒加载(图片快进视口才加载)
  • 防止布局抖动

普通 <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, 你可能需要这些:

  1. 删掉 react-router,按文件夹结构建 app 目录
  2. 默认写服务端组件,要 onClickuseState 才加 'use client'
  3. 删掉 useEffect 获取初始数据,改用 async/await
  4. 表单提交用 Server Actions,不用手写 /api 接口
  5. <img> 改成 <Image><a> 改成 <Link>
  6. generateMetadata 管理 SEO,不用 react-helmet
  7. 用 Middleware 做鉴权,不用手写高阶组件

微前端

如果你产品比较大,想用微前端来做产品隔离,可以看看 Next.js 的 Multi-Zones 方案:nextjs-microfrontend