Next.js 15 App Router路由架构与Server Actions数据交互实战

Next.js 15App Router做了多项性能优化和API调整,Server Actions从实验特性转为稳定特性,成为前后端数据交互的新范式。相比传统API Route,Server Actions将服务端逻辑直接内联到组件中,省去了手写fetch请求和序列化/反序列化的样板代码。本文从App Router路由架构入手,演示Server Actions在表单提交、数据变更和乐观更新中的完整用法。

App Router路由系统与文件约定

App Router使用app目录下的文件夹层级映射URL路径。每个目录下的特殊文件对应不同功能:page.tsx定义路由页面UI,layout.tsx定义共享布局,loading.tsx定义加载态,error.tsx定义错误边界,not-found.tsx定义404页面。

// 目录结构示例
app/
├── layout.tsx          # 根布局(必须)
├── page.tsx            # 首页 /
├── blog/
│   ├── layout.tsx      # blog区块布局
│   ├── page.tsx        # /blog列表页
│   ├── [slug]/
│   │   └── page.tsx    # /blog/:slug详情页
│   └── loading.tsx     # blog路由加载态
├── dashboard/
│   ├── layout.tsx      # dashboard布局(含鉴权)
│   ├── settings/
│   │   └── page.tsx    # /dashboard/settings
│   └── error.tsx       # dashboard错误边界

动态路由用方括号语法[slug],catch-all路由用[…slug]。Layout组件是嵌套的,父layout包裹子layout,渲染时不重新挂载。page组件切换时重新渲染。

Server Components与Client Components区分

App Router默认所有组件都是Server Components,在服务端渲染,不能使用useState、useEffect等浏览器API。需要客户端交互的组件通过”use client”指令声明。

// app/components/Counter.tsx
"use client";

import { useState } from "react";

export default function Counter() {
  const [count, setCount] = useState(0);
  return (
    <button onClick={() => setCount(count + 1)}>
      点击次数: {count}
    </button>
  );
}

Server Components和Client Components的边界划分原则:将交互逻辑和状态管理隔离到叶子节点,尽量缩小”use client”的范围。Server Components可以直接访问数据库和文件系统,渲染后的HTML流式传输给客户端,减少客户端JS包体积。

Server Actions基本用法与表单处理

Server Actions是服务端执行的异步函数,通过”use server”指令声明。前端组件直接调用该函数,框架自动处理网络传输和序列化。

// app/actions/post.ts
"use server";

import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import { db } from "@/lib/db";

export async function createPost(formData: FormData) {
  const title = formData.get("title") as string;
  const content = formData.get("content") as string;

  if (!title || title.trim().length < 2) {
    return { error: "标题至少2个字符" };
  }

  await db.post.create({
    data: { title, content, authorId: 1 },
  });

  revalidatePath("/blog");
  redirect("/blog");
}

export async function deletePost(postId: number) {
  await db.post.delete({ where: { id: postId } });
  revalidatePath("/blog");
  revalidatePath(`/blog/${postId}`);
}

在组件中使用Server Actions配合原生form元素:

// app/blog/new/page.tsx
import { createPost } from "@/app/actions/post";

export default function NewPostPage() {
  return (
    <form action={createPost}>
      <input
        type="text"
        name="title"
        placeholder="文章标题"
        required
        minLength={2}
      />
      <textarea
        name="content"
        placeholder="文章内容"
        rows={10}
        required
      />
      <button type="submit">发布</button>
    </form>
  );
}

form的action属性直接绑定Server Action,提交时框架自动序列化FormData并发送到服务端执行。无需手写onSubmit和fetch调用。

useFormState与useFormStatus处理表单状态

Next.js 15集成了React的useFormState和useFormStatus Hook,用于追踪表单提交状态和返回结果。这两个Hook只能在Client Components中使用。

// app/components/PostForm.tsx
"use client";

import { useFormState, useFormStatus } from "react-dom";
import { createPost } from "@/app/actions/post";

const initialState = { error: null };

function SubmitButton() {
  const { pending } = useFormStatus();
  return (
    <button type="submit" disabled={pending}>
      {pending ? "发布中..." : "发布"}
    </button>
  );
}

export default function PostForm() {
  const [state, formAction] = useFormState(createPost, initialState);

  return (
    <form action={formAction}>
      <input type="text" name="title" placeholder="标题" />
      <textarea name="content" placeholder="内容" />
      {state.error && (
        <p className="error">{state.error}</p>
      )}
      <SubmitButton />
    </form>
  );
}

useFormState返回状态和包装后的action,状态值由Server Action的返回值更新。useFormStatus提供pending状态,用于禁用按钮和显示加载文案。

缓存与revalidatePath刷新策略

App Router默认对页面做静态缓存(Static Caching)。当数据变更后需要主动刷新缓存,Server Actions中通过revalidatePath或revalidateTag实现。

import { revalidatePath, revalidateTag } from "next/cache";

// 刷新指定路径缓存
revalidatePath("/blog");
revalidatePath(`/blog/${postId}`);

// 刷新指定标签的所有缓存数据
revalidateTag("posts-list");

fetch请求可通过next参数配置缓存策略:

// 强制每次请求都重新获取(动态渲染)
const res = await fetch("https://api.example.com/posts", {
  next: { tags: ["posts-list"], revalidate: 3600 },
});

// ISR:每小时重新生成
const res2 = await fetch("https://api.example.com/data", {
  next: { revalidate: 3600 },
});

tags与revalidateTag配合使用:当Server Action修改了数据,调用revalidateTag("posts-list")会标记所有使用该tag的fetch缓存为过期,下次请求时自动重新获取。这种精确的缓存失效策略比全页面刷新更高效。

中间件与路由守卫配置

middleware.ts文件放在app目录的同级,用于在请求到达页面之前执行鉴权、重定向等逻辑。

// middleware.ts
import { NextRequest, NextResponse } from "next/server";
import { getToken } from "next-auth/jwt";

export async function middleware(request: NextRequest) {
  const token = await getToken({
    req: request,
    secret: process.env.NEXTAUTH_SECRET,
  });

  const isLoginPage = request.nextUrl.pathname.startsWith("/login");

  if (!token && !isLoginPage) {
    return NextResponse.redirect(new URL("/login", request.url));
  }

  if (token && isLoginPage) {
    return NextResponse.redirect(new URL("/dashboard", request.url));
  }

  return NextResponse.next();
}

export const config = {
  matcher: ["/dashboard/:path*", "/login"],
};

matcher字段限定中间件只对指定路径生效,避免对所有请求都执行鉴权逻辑。中间件运行在Edge Runtime上,延迟低但不能使用Node.js API。

原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/nextjs15approuter-lu-you-jia-gou-yu-serveractions-shu-ju/

(0)
小编小编
上一篇 13小时前
下一篇 13小时前

相关推荐