Next.js 14 数据处理:从服务端组件到状态管理的最佳实践

11 阅读2分钟

Next.js 14 带来了全新的数据处理范式,特别是在服务端组件和数据获取方面有了重大改进。今天,我们就来深入探讨如何在 Next.js 14 中进行高效的数据处理和状态管理。

Server Components 数据获取

1. 基础数据获取

Next.js 14 提供了多种数据获取方式,默认在服务端组件中执行:

// app/posts/page.tsx
async function getPosts() {
  // 默认缓存 GET 请求
  const posts = await fetch('https://api.example.com/posts');
  
  // 强制重新获取数据
  const dynamicData = await fetch('https://api.example.com/stats', {
    cache: 'no-store'
  });
  
  // 增量静态再生成(ISR)
  const revalidatedData = await fetch('https://api.example.com/content', {
    next: { revalidate: 3600 } // 1小时后重新验证
  });
  
  return {
    posts: await posts.json(),
    stats: await dynamicData.json(),
    content: await revalidatedData.json()
  };
}

export default async function PostsPage() {
  const { posts, stats, content } = await getPosts();
  
  return (
    <div>
      <Stats data={stats} />
      <PostList posts={posts} />
      <Content data={content} />
    </div>
  );
}

2. 并行数据请求

// app/dashboard/page.tsx
async function DashboardPage() {
  // 并行发起多个请求
  const [users, orders, analytics] = await Promise.all([
    fetch('https://api.example.com/users').then(res => res.json()),
    fetch('https://api.example.com/orders').then(res => res.json()),
    fetch('https://api.example.com/analytics').then(res => res.json())
  ]);
  
  return (
    <div className="dashboard">
      <UserStats data={users} />
      <OrderList orders={orders} />
      <AnalyticsChart data={analytics} />
    </div>
  );
}

3. 流式数据加载

// app/feed/page.tsx
import { Suspense } from 'react';

async function SlowComponent() {
  const data = await fetch('https://api.example.com/slow-data');
  return <SlowDataView data={await data.json()} />;
}

export default function FeedPage() {
  return (
    <div className="feed">
      {/* 快速加载的内容 */}
      <Header />
      
      {/* 使用 Suspense 包裹慢速组件 */}
      <Suspense fallback={<LoadingSkeleton />}>
        <SlowComponent />
      </Suspense>
      
      {/* 继续显示其他内容 */}
      <Sidebar />
    </div>
  );
}

数据缓存策略

1. 请求去重和缓存

// lib/data.ts
import { cache } from 'react';

export const getUser = cache(async (id: string) => {
  const res = await fetch(`https://api.example.com/users/${id}`);
  return res.json();
});

// app/users/[id]/page.tsx
export default async function UserPage({ params }: { params: { id: string } }) {
  // 多次调用相同的请求会被自动去重
  const user = await getUser(params.id);
  
  return (
    <div>
      <UserProfile user={user} />
      <UserPosts user={user} />
      <UserActivity user={user} />
    </div>
  );
}

2. 缓存标签和重新验证

// app/api/revalidate/route.ts
import { revalidateTag } from 'next/cache';

export async function POST(request: Request) {
  const { tag } = await request.json();
  
  // 重新验证特定标签的数据
  revalidateTag(tag);
  
  return Response.json({ revalidated: true });
}

// app/products/page.tsx
async function getProducts() {
  const res = await fetch('https://api.example.com/products', {
    next: { tags: ['products'] } // 使用标签标记请求
  });
  
  return res.json();
}

状态管理方案

1. 服务端状态

// lib/db.ts
import { createClient } from '@vercel/postgres';

const db = createClient();

export async function getServerState() {
  const { rows } = await db.query('SELECT * FROM app_state');
  return rows[0];
}

// app/layout.tsx
export default async function RootLayout({
  children
}: {
  children: React.ReactNode
}) {
  const serverState = await getServerState();
  
  return (
    <html>
      <body>
        <ServerStateProvider initialState={serverState}>
          {children}
        </ServerStateProvider>
      </body>
    </html>
  );
}

2. 客户端状态

// lib/state.ts
import { create } from 'zustand';

interface AppState {
  theme: 'light' | 'dark';
  setTheme: (theme: 'light' | 'dark') => void;
  user: any;
  setUser: (user: any) => void;
}

export const useAppStore = create<AppState>((set) => ({
  theme: 'light',
  setTheme: (theme) => set({ theme }),
  user: null,
  setUser: (user) => set({ user })
}));

// components/ThemeToggle.tsx
'use client';

export function ThemeToggle() {
  const { theme, setTheme } = useAppStore();
  
  return (
    <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
      Toggle Theme
    </button>
  );
}

3. 服务端操作(Server Actions)

// app/actions.ts
'use server';

import { revalidateTag } from 'next/cache';
import { db } from '@/lib/db';

export async function updatePost(id: string, data: any) {
  try {
    // 直接在服务端执行数据库操作
    await db.post.update({
      where: { id },
      data
    });
    
    // 重新验证相关数据
    revalidateTag('posts');
    
    return { success: true };
  } catch (error) {
    return { error: error.message };
  }
}

// app/posts/[id]/edit/page.tsx
export default function EditPost({ params }: { params: { id: string } }) {
  async function handleSubmit(formData: FormData) {
    'use server';
    
    const title = formData.get('title');
    const content = formData.get('content');
    
    const result = await updatePost(params.id, { title, content });
    
    if (result.error) {
      return { error: result.error };
    }
    
    redirect(`/posts/${params.id}`);
  }
  
  return (
    <form action={handleSubmit}>
      <input type="text" name="title" />
      <textarea name="content" />
      <button type="submit">Update Post</button>
    </form>
  );
}

数据库集成

1. Prisma 集成

// prisma/schema.prisma
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model Post {
  id        String   @id @default(cuid())
  title     String
  content   String?
  published Boolean  @default(false)
  author    User     @relation(fields: [authorId], references: [id])
  authorId  String
}

// lib/prisma.ts
import { PrismaClient } from '@prisma/client';

const prismaClientSingleton = () => {
  return new PrismaClient();
};

declare global {
  var prisma: undefined | ReturnType<typeof prismaClientSingleton>;
}

const prisma = globalThis.prisma ?? prismaClientSingleton();

export default prisma;

if (process.env.NODE_ENV !== 'production') globalThis.prisma = prisma;

2. API Routes 开发

// app/api/posts/route.ts
import { NextResponse } from 'next/server';
import prisma from '@/lib/prisma';

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);
  const published = searchParams.get('published') === 'true';
  
  try {
    const posts = await prisma.post.findMany({
      where: { published },
      include: { author: true }
    });
    
    return NextResponse.json(posts);
  } catch (error) {
    return NextResponse.json(
      { error: 'Failed to fetch posts' },
      { status: 500 }
    );
  }
}

export async function POST(request: Request) {
  try {
    const json = await request.json();
    
    const post = await prisma.post.create({
      data: json,
      include: { author: true }
    });
    
    return NextResponse.json(post);
  } catch (error) {
    return NextResponse.json(
      { error: 'Failed to create post' },
      { status: 500 }
    );
  }
}

性能优化

1. 数据预取

// app/posts/prefetch.ts
import prisma from '@/lib/prisma';

export async function prefetchPosts() {
  // 预取热门文章
  const posts = await prisma.post.findMany({
    where: { 
      published: true,
      featured: true
    },
    take: 10
  });
  
  return posts;
}

// app/posts/page.tsx
import { Suspense } from 'react';
import { prefetchPosts } from './prefetch';

export default async function PostsPage() {
  // 预取数据
  const prefetchedPosts = prefetchPosts();
  
  return (
    <div>
      <Suspense fallback={<LoadingSkeleton />}>
        <PostList promise={prefetchedPosts} />
      </Suspense>
    </div>
  );
}

2. 数据分页

// lib/pagination.ts
export async function getPaginatedData(page: number, limit: number) {
  const offset = (page - 1) * limit;
  
  const [data, total] = await Promise.all([
    prisma.post.findMany({
      skip: offset,
      take: limit,
      orderBy: { createdAt: 'desc' }
    }),
    prisma.post.count()
  ]);
  
  return {
    data,
    metadata: {
      total,
      currentPage: page,
      totalPages: Math.ceil(total / limit),
      hasNextPage: offset + limit < total
    }
  };
}

// app/posts/page.tsx
export default async function PostsPage({
  searchParams
}: {
  searchParams: { page: string }
}) {
  const page = parseInt(searchParams.page) || 1;
  const { data, metadata } = await getPaginatedData(page, 10);
  
  return (
    <div>
      <PostList posts={data} />
      <Pagination {...metadata} />
    </div>
  );
}

写在最后

Next.js 14 的数据处理和状态管理方案为我们提供了强大而灵活的工具。在实际应用中,需要注意以下几点:

  1. 合理使用服务端组件和客户端组件
  2. 正确配置数据缓存策略
  3. 选择合适的状态管理方案
  4. 注意数据预取和性能优化
  5. 合理组织 API 路由

在下一篇文章中,我们将深入探讨 Next.js 14 的认证与授权实现。如果你有任何问题或建议,欢迎在评论区讨论!

如果觉得这篇文章对你有帮助,别忘了点个赞 👍