现代全栈框架概览

前端与后端的边界正在消失

🎯 学习目标

学完本章,你将能够:

  • 理解 SSR、SSG、ISR 等渲染策略的差异和适用场景
  • 掌握 Next.js 的 Server Components 和数据获取模式
  • 了解 Nuxt 3 的自动导入和组合式 API
  • 了解 Remix 和 SvelteKit 的核心设计理念
  • 根据项目需求选择合适的全栈框架

你有没有想过

为什么 Next.js 能在服务端查数据库,还能在客户端做交互?前后端代码为什么要分两个项目?

传统开发中,前端和后端是两个独立的项目、两套部署流程、两种类型定义。而现代全栈框架让它们融合在同一个项目里,共享类型、共享逻辑、一次部署。

这个比喻只是为了帮你建立直观感受:以前前后端像两家公司做生意,现在像一家公司的两个部门。当然,框架内部仍然有明确的服务端和客户端边界。

全栈演进历程

从模板渲染到全栈融合

阶段代表技术前后端关系
传统 MVCExpress + EJS后端渲染模板
SPA 时代React + REST API前后端完全分离
SSR 混合Next.js SSR服务端渲染 + 客户端水合
全栈融合Next.js App Router同一项目,共享类型

现代全栈框架的核心特征:同构渲染、文件系统路由、内置 API 支持、混合渲染策略。

渲染策略对比

选对策略,性能翻倍

策略何时渲染适用场景
SSR每次请求时服务端渲染动态内容、SEO 要求高
SSG构建时生成静态 HTML博客、文档
ISR构建时 + 后台按需更新内容不频繁更新
CSR浏览器端 JS 渲染仪表盘、管理后台
Streaming SSR分块流式传输 HTML大型页面、慢数据源

核心原则:尽量静态,按需动态。优先 SSG/ISR,只在需要实时数据时用 SSR。

渲染策略详解

五种策略的关键指标与取舍

策略TTFBFCPSEO服务器负载典型场景
SSG极低(CDN)极快优无博客、营销页、文档站
ISR低(CDN)快优低商品列表、新闻资讯
SSR较高中等优高个性化首页、搜索结果
CSR高(等 JS)慢差无管理后台、内部工具
Streaming SSR低(分块)较快优中电商首页、复杂仪表盘

TTFB(Time to First Byte):从请求发出到收到第一个字节的时间。FCP(First Contentful Paint):用户首次看到内容的时间。SSG 直接从 CDN 返回静态文件,所以两者都极低。CSR 必须等 JS 下载执行后才能渲染,FCP 最慢。Streaming SSR 把页面拆成多个 chunk 逐步发送,让用户更快看到内容骨架。

渲染时序

四种策略在时间轴上的区别

图:SSR / SSG / ISR / CSR 四种渲染策略的时序对比

Part 1: Next.js

目前最流行的 React 全栈框架,由 Vercel 维护,App Router 基于 React Server Components 架构。我们将学习以下核心能力:

  • Server / Client Components——服务端组件直接查数据库,客户端组件处理交互
  • 数据获取策略——fetch cache / revalidate / ISR / SSG / SSR 灵活切换
  • 布局系统——layout.tsx / template.tsx 构建嵌套布局
  • 中间件——在请求到达页面之前执行鉴权、重定向等逻辑
  • API Routes——前后端一体,API 就在项目里
  • 部署方式——Vercel 一键部署或 Self-host 自托管

Server Components

在服务端渲染,不发送 JS 到客户端

定义:Server Component(服务端组件)在服务器上执行,渲染结果以 HTML 形式发送到浏览器,不会打包任何 JS 到客户端。

白话:就像餐厅后厨直接把做好的菜端上桌,顾客不需要知道菜怎么做。浏览器收到的是纯 HTML,不需要下载额外的 JavaScript。

// app/users/page.tsx(默认就是 Server Component)
import { db } from '@/lib/db';

export default async function UsersPage() {
  // 直接查数据库,不需要 API!
  const users = await db.user.findMany({
    select: { id: true, name: true, email: true },
  });

  return (
    <div>
      <h1>用户列表</h1>
      <UserTable users={users} />
    </div>
  );
}

Server Component 可以直接访问数据库、文件系统和环境变量,但不能使用 useState、useEffect 等客户端 API。

Client Component

需要交互时加上 'use client'

定义:Client Component(客户端组件)通过 'use client' 声明,会在浏览器中运行,支持状态管理和事件处理。

白话:如果页面需要"响应用户操作"(比如搜索、点赞、弹窗),就得用客户端组件,因为浏览器才能监听鼠标和键盘。

// components/UserTable.tsx
'use client'; // 必须声明
import { useState } from 'react';

export function UserTable({ users }) {
  const [search, setSearch] = useState('');
  const filtered = users.filter(u =>
    u.name.toLowerCase().includes(search.toLowerCase())
  );

  return (
    <>
      <input placeholder="搜索用户..."
        value={search}
        onChange={e => setSearch(e.target.value)} />
      <table>
        {filtered.map(user => (
          <tr key={user.id}>
            <td>{user.name}</td>
            <td>{user.email}</td>
          </tr>
        ))}
      </table>
    </>
  );
}

Server vs Client

一张表搞清楚两者的区别

特性Server ComponentClient Component
获取数据直接查数据库需要通过 API
使用 useState不可以可以
使用 onClick不可以可以
打包体积不发送 JS发送 JS bundle
访问后端资源数据库、文件系统不可以

尽量让组件保持为 Server Component,只在需要交互时才加 'use client'。

数据获取策略

fetch 扩展让缓存控制声明化

定义:Next.js 扩展了原生 fetch,添加了 cache 和 next.revalidate 选项,让你在数据请求层面直接控制渲染策略。

白话:以前缓存靠 Nginx 和 CDN 配置,现在直接写在代码里。告诉 Next.js "这个数据多久刷新一次"就够了。

// SSG:构建时获取,永不刷新(适合静态内容)
const res = await fetch('https://api.example.com/about', {
  cache: 'force-cache',
});

// SSR:每次请求都重新获取(适合实时数据)
const res = await fetch('https://api.example.com/realtime', {
  cache: 'no-store',
});

// ISR:每 60 秒后台重新验证(适合semi-static内容)
const res = await fetch('https://api.example.com/posts', {
  next: { revalidate: 60 },
});

这三种模式覆盖了绝大多数场景。默认是 force-cache(SSG),如果你不指定缓存策略,Next.js 会在构建时获取数据并缓存。加 no-store 切换到 SSR,加 revalidate 切换到 ISR。

ISR 与按需重验证

静态页面的"自动刷新"机制

定义:ISR(Incremental Static Regeneration)在构建时生成静态页面,之后按设定间隔在后台重新生成。还可以用 revalidatePath 手动触发更新。

白话:像新闻 APP 的缓存策略——先给你看缓存内容,后台悄悄拉最新的,下次打开就是新的。

// app/posts/[id]/page.tsx
export const revalidate = 3600; // 每小时重新验证

export default async function PostPage({ params }) {
  const post = await fetch(
    `https://api.example.com/posts/${params.id}`,
    { next: { revalidate: 3600 } }
  ).then(r => r.json());

  return <article><h1>{post.title}</h1><p>{post.content}</p></article>;
}

// 手动触发重验证(例如:后台编辑文章后调用)
// app/api/revalidate/route.ts
import { revalidatePath } from 'next/cache';
export async function POST(request) {
  const { path } = await request.json();
  revalidatePath(path);
  return Response.json({ revalidated: true });
}

布局系统

layout 与 template 的区别

定义:layout.tsx 定义共享布局,切换子路由时不会重新渲染。template.tsx 也是共享布局,但每次导航都会重新挂载。

白话:layout 像网站的外框(导航栏+侧边栏),切页面时外框不变;template 像模板纸,每次翻页换一张新的。

// app/layout.tsx — 根布局(整个应用共享,不会重新渲染)
export default function RootLayout({ children }) {
  return (
    <html lang="zh">
      <body>
        <nav>网站导航</nav>
        <main>{children}</main>
        <footer>版权信息</footer>
      </body>
    </html>
  );
}

// app/dashboard/layout.tsx — 嵌套布局
export default function DashboardLayout({ children }) {
  return (
    <div className="dashboard">
      <Sidebar />  {/* 切换子路由时 Sidebar 保持不变 */}
      <section>{children}</section>
    </div>
  );
}

布局可以嵌套:根布局包裹仪表盘布局,仪表盘布局再包裹具体页面。每层 layout 都有自己的 children 插槽。template.tsx 适合需要每次导航都触发 useEffect 或动画重置的场景。

中间件 Middleware

请求到达页面之前的第一道关卡

定义:Middleware 是运行在 Edge Runtime 中的函数,在每个请求到达路由处理程序之前执行。常用于鉴权、重定向、日志、A/B 测试。

白话:像大厦门口的保安——你要先进保安那关(鉴权),才能进电梯(路由)。保安不关心你进了哪个房间(页面逻辑),只管"让不让进"。

// middleware.ts(放在项目根目录或 src/ 下)
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  // 检查登录状态
  const token = request.cookies.get('auth-token')?.value;

  if (!token) {
    // 未登录:重定向到登录页
    const loginUrl = new URL('/login', request.url);
    loginUrl.searchParams.set('from', request.nextUrl.pathname);
    return NextResponse.redirect(loginUrl);
  }

  // 已登录:继续请求(可以在这里注入用户信息到 headers)
  const response = NextResponse.next();
  response.headers.set('x-user-id', decodeToken(token).userId);
  return response;
}

// 配置:只对 /dashboard 下的路由生效
export const config = {
  matcher: '/dashboard/:path*',
};

Middleware 运行在 Edge Runtime(轻量级 V8 环境),不是 Node.js。所以不能使用 fs、path 等 Node.js API,也不能直连数据库。它只适合做轻量级的请求拦截和重定向。

API Routes

前后端一体,API 就在项目里

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

const CreateUserSchema = z.object({
  name: z.string().min(1).max(50),
  email: z.string().email(),
  password: z.string().min(8),
});

export async function GET(request: NextRequest) {
  const users = await db.user.findMany({
    select: { id: true, name: true, email: true },
  });
  return NextResponse.json({ users });
}

export async function POST(request: NextRequest) {
  const body = await request.json();
  const data = CreateUserSchema.parse(body);
  const user = await db.user.create({ data });
  return NextResponse.json(user, { status: 201 });
}

部署方式

Vercel 一键部署或 Self-host

方式适用场景优势劣势
Vercel个人项目、初创团队零配置、自动 CI/CD、Edge Functions免费额度有限、供应商绑定
Docker 自托管企业项目、合规要求完全控制、无供应商绑定需自己运维、配置复杂
Node.js Server需要 SSR 的传统服务器灵活、可接入现有基础设施需要手动处理负载均衡
Static Export纯静态站点部署到任意静态托管不支持 SSR/API Routes
# Vercel 部署(推荐新手)
$ npx vercel

# Docker 自托管
$ docker build -t my-nextjs-app .
$ docker run -p 3000:3000 my-nextjs-app

# 静态导出(next.config.js 中设置 output: 'export')
$ npm run build   # 输出到 out/ 目录

Part 2: Nuxt 3

Vue 生态的全栈框架,由 Nuxt 团队维护。它的核心卖点是自动导入和 Nitro 服务引擎。我们将学习:

  • Auto-imports——组件、组合式函数自动导入,无需手写 import
  • 文件路由——目录结构即路由,零配置
  • useFetch / useAsyncData——声明式数据获取
  • Server API / Middleware——服务端 API 和中间件

自动导入与路由

写文件就是在写路由和组件

定义:Nuxt 3 会自动扫描 components/、composables/、pages/ 目录,自动注册组件和路由,无需手动 import。

白话:在别的框架里你要写三件事——import 组件、注册路由、导出组件。在 Nuxt 里你只需要创建文件,剩下的全自动。

项目结构:
├── components/
│   └── UserCard.vue       → 自动注册为 <UserCard />
├── composables/
│   └── useAuth.ts         → 自动导入为 useAuth()
├── pages/
│   ├── index.vue           → 路由 /
│   ├── users/
│   │   ├── index.vue       → 路由 /users
│   │   └── [id].vue        → 路由 /users/:id(动态路由)
│   └── about.vue           → 路由 /about

文件路由的命名规则:[id].vue 对应动态参数 route.params.id,[...slug].vue 对应 catch-all 路由。Nuxt 会根据目录结构自动生成 Vue Router 配置。

Nuxt 数据获取

useFetch 自动导入,无需 import

定义:useFetch 是 Nuxt 提供的组合式函数,在服务端预取数据,在客户端自动缓存和去重。useAsyncData 是它的底层 API,可以包装任意异步操作。

白话:useFetch = 自动帮你发请求 + 自动处理加载状态 + SSR 时在服务端执行一次。你不用关心"这段代码跑在服务端还是客户端"。

<!-- pages/index.vue -->
<script setup lang="ts">
// useFetch 自动导入,无需 import
const { data: posts, pending, error, refresh } = await useFetch('/api/posts', {
  key: 'homepage-posts',          // 缓存 key(防重复请求)
  transform: (data) => data.map(p => ({
    ...p,
    excerpt: p.content?.substring(0, 150) + '...',
  })),
  default: () => [],              // 初始默认值
});

// useAsyncData:包装任意异步逻辑
const { data: stats } = await useAsyncData('stats', () =>
  $fetch('/api/stats')
);

useHead({ title: '首页 - 我的博客' });
</script>

<template>
  <div v-if="pending">加载中...</div>
  <div v-else-if="error">{{ error.message }}</div>
  <template v-else>
    <article v-for="post in posts" :key="post.id">
      <h2>{{ post.title }}</h2>
      <p>{{ post.excerpt }}</p>
    </article>
    <button @click="refresh">刷新</button>
  </template>
</template>

Nuxt Server API

文件名即路由,开发体验一流

定义:在 server/api/ 目录下创建文件,文件名就是 API 路径。.get.ts 处理 GET,.post.ts 处理 POST,基于 h3 事件引擎。

// server/api/users.get.ts — 对应 GET /api/users
import { defineEventHandler, getQuery } from 'h3';

export default defineEventHandler(async (event) => {
  const query = getQuery(event);
  const page = Number(query.page) || 1;
  const limit = Number(query.limit) || 10;

  const [users, total] = await Promise.all([
    event.context.prisma.user.findMany({
      skip: (page - 1) * limit, take: limit,
      select: { id: true, name: true, email: true },
    }),
    event.context.prisma.user.count(),
  ]);

  return { users, total, page, limit };
});

Server Middleware

请求到达 API 之前的拦截器

定义:server/middleware/ 下的文件会在每个 API 请求之前执行,用于鉴权、日志、CORS 等跨切面逻辑。

白话:和 Next.js 的 middleware 类似,但 Nuxt 的运行在 Node.js(或 Nitro 运行时)中,可以使用完整的 Node.js API。

// server/middleware/auth.ts
import { defineEventHandler, createError, getHeader } from 'h3';

export default defineEventHandler((event) => {
  const url = event.path;

  // 跳过公开路由
  if (url.startsWith('/api/auth') || url.startsWith('/api/public')) {
    return;
  }

  const token = getHeader(event, 'authorization')?.replace('Bearer ', '');
  if (!token) {
    throw createError({ statusCode: 401, message: '未提供认证令牌' });
  }

  // 将解析后的用户信息挂到 event.context
  event.context.user = verifyToken(token);
});

Nuxt 的 Server Middleware 基于 Nitro 引擎,支持部署到 Node.js、Cloudflare Workers、Vercel Edge 等多种目标平台。通过 nuxt.config.ts 中的 nitro.preset 配置切换。

Part 3: Remix

由 Shopify 支持的 React 全栈框架。核心理念是回归 Web 标准——充分利用 FormData、Fetch API、HTTP 缓存等浏览器原生能力。

  • 渐进增强——即使 JavaScript 被禁用,表单提交仍然可以工作
  • Loader / Action 模式——每个路由模块导出 loader(读数据)和 action(写数据)
  • 嵌套路由——精确控制数据重获取范围,不浪费网络请求
  • 适合场景——内容管理、电商、需要高可靠性的表单密集型应用

Remix 路由与数据

Loader 读数据,Action 写数据

// app/routes/users.tsx
import { json } from '@remix-run/node';    // 服务端工具
import { Form, useLoaderData } from '@remix-run/react'; // 客户端组件
import type { ActionFunction, LoaderFunction } from '@remix-run/node';

// Loader:服务端获取数据(类似 GET handler)
export const loader: LoaderFunction = async () => {
  const users = await db.user.findMany();
  return json({ users });
};

// Action:处理表单提交(类似 POST handler)
export const action: ActionFunction = async ({ request }) => {
  const formData = await request.formData();
  const name = formData.get('name') as string;
  const email = formData.get('email') as string;
  await db.user.create({ data: { name, email } });
  return json({ ok: true });
};

// UI:默认是服务端渲染
export default function Users() {
  const { users } = useLoaderData<typeof loader>();
  return (
    <div>
      <h1>用户列表</h1>
      <ul>{users.map(u => <li key={u.id}>{u.name} - {u.email}</li>)}</ul>
      {/* 原生 Form,JS 禁用时也能提交 */}
      <Form method="post">
        <input name="name" placeholder="姓名" required />
        <input name="email" type="email" placeholder="邮箱" required />
        <button type="submit">添加用户</button>
      </Form>
    </div>
  );
}

Remix 的 <Form> 组件在 JS 加载后会用 fetch 拦截提交(体验更好),JS 没加载时则退化为原生 HTML form 提交(仍然可用)。这就是渐进增强。

Part 4: SvelteKit

Svelte 官方全栈框架。核心卖点是编译时优化——Svelte 在构建阶段就把响应式逻辑编译成高效的命令式 DOM 操作,客户端 JS 体积极小。

  • 零虚拟 DOM——编译后直接操作 DOM,运行时开销几乎为零
  • Load 函数——每个路由的 +page.ts 导出 load 函数获取数据
  • Actions——+page.server.ts 中的 form actions 处理表单提交
  • 适配器模式——通过 adapter 部署到 Vercel、Netlify、Cloudflare 等
  • 适合场景——对性能极致要求的 SPA、交互密集型应用、PWA

SvelteKit 路由与数据

load 读数据,actions 写数据

// src/routes/users/+page.server.ts
import type { PageServerLoad, Actions } from './$types';

// Load:服务端获取数据
export const load: PageServerLoad = async () => {
  const users = await db.user.findMany({
    select: { id: true, name: true, email: true },
  });
  return { users };
};

// Actions:处理表单提交
export const actions: Actions = {
  create: async ({ request }) => {
    const data = await request.formData();
    const name = data.get('name') as string;
    const email = data.get('email') as string;
    await db.user.create({ data: { name, email } });
    return { success: true };
  },
};
<!-- src/routes/users/+page.svelte -->
<script lang="ts">
  // $page.data 自动接收 load 返回的数据
  let { data } = $props();
</script>

<h1>用户列表</h1>
<ul>
  {#each data.users as user}
    <li>{user.name} - {user.email}</li>
  {/each}
</ul>

<form method="POST" action="?/create">
  <input name="name" placeholder="姓名" required />
  <input name="email" type="email" required />
  <button type="submit">添加用户</button>
</form>

四大框架对比

选哪个?看你的 UI 偏好

特性Next.jsNuxt 3RemixSvelteKit
UI 库ReactVue 3ReactSvelte
服务端渲染SSR/SSG/ISRSSR/SSG/SWRSSRSSR/SSG
自动导入不支持组件自动导入不支持部分支持
部署Vercel / NodeNitro 多目标任意 Node适配器模式
社区规模最大大中等增长中
学习曲线中等低中等低
客户端体积较大中等中等极小

框架选型指南

根据项目需求做决策

你的情况推荐框架理由
团队熟悉 React,需要灵活的渲染策略Next.jsSSR/SSG/ISR 全支持,生态最大
团队熟悉 Vue,追求开发效率Nuxt 3自动导入 + Nitro 多平台部署
重视 Web 标准、表单密集型应用Remix渐进增强、离线可用、FormData 原生
对性能和包体积极致追求SvelteKit编译优化、客户端代码极小
企业项目,合规要求自托管Next.js / Nuxt 3Docker 部署成熟、社区资源多
个人博客或文档站Nuxt 3 / SvelteKit配置少、上手快、静态导出好

核心决策因素:1) 团队最熟悉哪个 UI 库(React / Vue / Svelte);2) 项目对 SEO、性能、离线能力的要求;3) 部署环境限制。框架本身没有绝对优劣,选你团队最顺手的那个。

排错:常见问题

症状:在 Server Component 中使用了 useState,页面报错 "useState is not defined"。
原因:Server Component 默认运行在服务端,不支持 React 客户端 Hook。
修复:把需要交互的子组件拆出来,加上 'use client' 声明。
症状:Nuxt 项目中 useFetch 的数据在客户端重复请求。
原因:没有设置 key,或者 key 在服务端和客户端不一致。
修复:给 useFetch 设置稳定的 key,如 useFetch('/api/posts', { key: 'posts' })。
症状:项目越做越重,框架绑定了太多逻辑,迁移成本巨大。
原因:过度依赖框架的专有 API,没有做好抽象。
修复:核心业务逻辑放在独立模块中,框架只负责路由和渲染。如果后端需要消息队列、定时任务等复杂逻辑,用独立 Node.js 服务。

练习一:Next.js

任务

创建一个 Next.js App Router 项目,实现一个博客首页:使用 Server Component 从数据库获取文章列表,支持 ISR 每 10 分钟重新验证。

$ npx create-next-app@latest my-blog --typescript --app
$ cd my-blog
$ npm install @prisma/client
$ npx prisma init

参考答案

// app/page.tsx
export const revalidate = 600; // ISR: 每 10 分钟重新验证

export default async function Home() {
  const posts = await fetch('https://api.example.com/posts', {
    next: { revalidate: 600 },
  }).then(r => r.json());

  return (
    <main>
      <h1>我的博客</h1>
      {posts.map(post => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.excerpt}</p>
        </article>
      ))}
    </main>
  );
}

练习二:Nuxt 3

任务

创建一个 Nuxt 3 项目,实现用户列表页面:使用 useFetch 从 Server API 获取数据,并添加一个搜索过滤功能(Client 侧交互)。

$ npx nuxi@latest init my-nuxt-app
$ cd my-nuxt-app

参考答案

<!-- pages/users.vue -->
<script setup lang="ts">
const { data: users } = await useFetch('/api/users');
const search = ref('');
const filtered = computed(() =>
  users.value?.filter(u =>
    u.name.toLowerCase().includes(search.value.toLowerCase())
  )
);
</script>

<template>
  <div>
    <input v-model="search" placeholder="搜索..." />
    <ul>
      <li v-for="u in filtered" :key="u.id">
        {{ u.name }} - {{ u.email }}
      </li>
    </ul>
  </div>
</template>

练习三:框架对比

任务

选择 Remix 或 SvelteKit(二选一),创建一个简单的待办事项应用。要求:1) 使用路由 load/loader 获取数据;2) 使用 action/form actions 添加待办事项。

参考答案(SvelteKit 版)

// src/routes/todos/+page.server.ts
let todos = [
  { id: 1, text: '学习 SvelteKit', done: false },
];

export const load = async () => ({ todos });

export const actions = {
  add: async ({ request }) => {
    const data = await request.formData();
    const text = data.get('text') as string;
    todos.push({ id: Date.now(), text, done: false });
    return { success: true };
  },
};

📝 要点总结

本章核心知识点

渲染策略

SSR 首屏快、SSG 性能优、ISR 兼顾两者,按页面特性选择

Next.js

Server Components 减少客户端 JS,App Router 统一路由范式

Nuxt 3

自动导入 + 组合式 API + 全栈能力,Vue 生态的首选

选型建议

React 生态选 Next.js,Vue 选 Nuxt,追求极简选 SvelteKit

下一章

全栈框架让前后端融为一体。下一章我们学习 tRPC——一种让前后端类型自动同步的神奇框架。

1 / X