Next.js 15 全栈开发系列之SWR请求

文摘   2024-11-14 19:44   湖北  

什么是 SWR?

SWR (stale-while-revalidate) 是由 Vercel 团队开发的数据获取 React Hooks 库。它的名字来源于 HTTP RFC 5861 中的 stale-while-revalidate 缓存策略:先返回缓存数据(stale),然后发送请求获取最新数据(revalidate)。

SWR 的核心特性

1. 自动数据缓存和重新验证

import useSWR from 'swr'

function Profile() {
  const { data, error } = useSWR('/api/user', fetcher)
  
  if (error) return <div>failed to load</div>
  if (!data) return <div>loading...</div>
  return <div>hello {data.name}!</div>
}

SWR 会自动缓存数据,当你重新访问组件时可以立即显示缓存数据,同时在后台重新请求以确保数据是最新的。

2. 自动重新请求

SWR 提供了多种重新请求策略:

  • • 窗口获得焦点时重新请求

  • • 网络恢复时重新请求

  • • 定时轮询

const { data } = useSWR('/api/todos', fetcher, {
  refreshInterval3000// 每3秒重新请求一次
  revalidateOnFocustrue// 窗口聚焦时重新请求
  revalidateOnReconnecttrue // 网络重连时重新请求
})

3. 请求去重和缓存共享

多个组件请求相同的数据时,SWR 会自动去重并共享缓存:

// 这两个组件会共享同一份数据和请求
function ComponentA() {
  const { data } = useSWR('/api/user', fetcher)
  return <div>{data.name}</div>
}

function ComponentB() {
  const { data } = useSWR('/api/user', fetcher)
  return <div>{data.email}</div>
}

4. 乐观更新

SWR 支持在本地立即更新数据,同时在后台发送请求:

import { mutate } from 'swr'

// 立即更新本地数据
mutate('/api/todos', [...todos, newTodo], false)
// 发送请求
await fetch('/api/todos', {
  method'POST',
  bodyJSON.stringify(newTodo)
})
// 重新验证
mutate('/api/todos')

5. 分页和无限加载

SWR 提供了专门的 Hook 用于处理分页数据:

function Posts() {
  const { data, size, setSize } = useSWRInfinite(
    (index) => `/api/posts?page=${index + 1}`,
    fetcher
  )
  
  return (
    <div>
      {data.map((posts, index) => (
        <div key={index}>
          {posts.map(post => (
            <div key={post.id}>{post.title}</div>
          ))}
        </div>
      ))}
      <button onClick={() => setSize(size + 1)}>加载更多</button>
    </div>

  )
}

在 Next.js 14 中的最佳实践

1. 全局配置

// app/swr-provider.tsx
'use client'

import { SWRConfig } from 'swr'

export function SWRProvider({ children }) {
  return (
    <SWRConfig 
      value={{
        fetcher: (url) =>
 fetch(url).then(res => res.json()),
        revalidateOnFocus: false,
        revalidateOnReconnect: false
      }}
    >
      {children}
    </SWRConfig>

  )
}

2. 配合 App Router 使用

Next.js 14 的 App Router 支持服务端组件,我们可以结合 SWR 实现最佳性能:

// app/page.tsx
async function Page() {
  // 服务端获取初始数据
  const initialData = await fetch('/api/data')
  
  return (
    <ClientComponent fallback={initialData} />
  )
}

// components/client.tsx
'use client'
function ClientComponent({ fallback }) {
  const { data } = useSWR('/api/data', { fallbackData: fallback })
  return <div>{data}</div>
}

3. 错误处理和重试

function Profile() {
  const { data, error, isLoading, isValidating, mutate } = useSWR(
    '/api/user',
    fetcher,
    {
      onErrorRetry(error, key, config, revalidate, { retryCount }) => {
        // 超过 10 次后停止重试
        if (retryCount >= 10return
        
        // 5xx 错误才重试
        if (error.status >= 500) {
          setTimeout(() => revalidate({ retryCount }), 5000)
        }
      }
    }
  )
  
  if (error) {
    return (
      <div>
        加载失败
        <button onClick={() => mutate()}>重试</button>
      </div>

    )
  }
  
  return <div>{isValidating ? '刷新中...' : data}</div>
}

进阶使用技巧

1. 条件请求

// 当 user 存在时才请求
const { data: orders } = useSWR(
  user ? `/api/orders?uid=${user.id}` : null,
  fetcher
)

2. 依赖请求

// 第二个请求依赖第一个请求的结果
const { data: user } = useSWR('/api/user', fetcher)
const { data: orders } = useSWR(
  () => user ? `/api/orders?uid=${user.id}` : null,
  fetcher
)

3. 预请求

import { preload } from 'swr'

// 提前加载数据
preload('/api/posts', fetcher)

function Posts() {
  // 数据可能已经在缓存中
  const { data } = useSWR('/api/posts', fetcher)
  return <div>{data}</div>
}

使用 SWR 构建现代博客系统

一、用户认证系统实现

1. 登录状态管理

首先,我们实现一个全局的用户状态管理hook:

// hooks/useAuth.ts
import useSWR from 'swr'
import { useRouter } from 'next/navigation'

interface User {
  idstring
  usernamestring
  emailstring
}

export function useAuth() {
  const router = useRouter()
  
  const { data: user, error, mutate } = useSWR<User>('/api/user'async (url) => {
    const res = await fetch(url)
    if (!res.okthrow new Error('Failed to fetch user')
    return res.json()
  }, {
    revalidateOnFocustrue// 窗口聚焦时重新验证用户状态
    dedupingInterval2000   // 2秒内重复请求会使用缓存
  })

  const login = async (emailstringpasswordstring) => {
    try {
      const res = await fetch('/api/login', {
        method'POST',
        bodyJSON.stringify({ email, password })
      })
      if (!res.okthrow new Error('Login failed')
      
      // 登录成功后立即更新用户数据
      await mutate()
      router.push('/dashboard')
    } catch (error) {
      throw error
    }
  }

  const logout = async () => {
    await fetch('/api/logout', { method'POST' })
    // 退出后清空用户数据
    await mutate(null)
    router.push('/login')
  }

  return {
    user,
    loading: !error && !user,
    error,
    login,
    logout
  }
}

2. 登录组件实现

// components/LoginForm.tsx
'use client'
import { useState } from 'react'
import { useAuth } from '@/hooks/useAuth'

export function LoginForm() {
  const { login, loading } = useAuth()
  const [email, setEmail] = useState('')
  const [password, setPassword] = useState('')

  const handleSubmit = async (eReact.FormEvent) => {
    e.preventDefault()
    try {
      await login(email, password)
    } catch (error) {
      alert('登录失败')
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        value={email}
        onChange={(e) =>
 setEmail(e.target.value)}
        placeholder="邮箱"
      />
      <input
        type="password"
        value={password}
        onChange={(e) =>
 setPassword(e.target.value)}
        placeholder="密码"
      />
      <button type="submit" disabled={loading}>
        {loading ? '登录中...' : '登录'}
      </button>
    </form>

  )
}

二、博客系统实现

1. 文章列表与分页

// hooks/useArticles.ts
import useSWRInfinite from 'swr/infinite'

interface Article {
  idstring
  titlestring
  contentstring
  createdAtstring
  author: {
    namestring
    avatarstring
  }
}

export function useArticles(pageSize = 10) {
  const getKey = (pageIndexnumber) => 
    `/api/articles?page=${pageIndex + 1}&limit=${pageSize}`

  const {
    data,
    error,
    size,
    setSize,
    mutate,
    isValidating
  } = useSWRInfinite<Article[]>(
    getKey,
    async (url) => {
      const res = await fetch(url)
      return res.json()
    },
    {
      revalidateFirstPagefalse,
      persistSizetrue
    }
  )

  const articles = data ? data.flat() : []
  const isLoadingInitialData = !data && !error
  const isLoadingMore = size > 0 && data && typeof data[size - 1] === "undefined"
  const isEmpty = data?.[0]?.length === 0
  const isReachingEnd = isEmpty || (data && data[data.length - 1]?.length < pageSize)

  return {
    articles,
    error,
    isLoadingInitialData,
    isLoadingMore,
    isReachingEnd,
    loadMore() => setSize(size + 1),
    mutate
  }
}

2. 文章详情与实时更新

// hooks/useArticle.ts
import useSWR from 'swr'

export function useArticle(articleIdstring) {
  const { data: article, error, mutate } = useSWR(
    `/api/articles/${articleId}`,
    async (url) => {
      const res = await fetch(url)
      if (!res.okthrow new Error('Failed to fetch article')
      return res.json()
    },
    {
      revalidateOnFocusfalse,
      dedupingInterval5000
    }
  )

  const updateLikes = async () => {
    // 乐观更新
    await mutate(
      { ...article, likes: article.likes + 1 },
      false
    )
    
    // 发送实际请求
    await fetch(`/api/articles/${articleId}/like`, {
      method'POST'
    })
    
    // 重新验证数据
    await mutate()
  }

  return {
    article,
    error,
    updateLikes
  }
}

3. 博客列表组件

// components/ArticleList.tsx
'use client'
import { useArticles } from '@/hooks/useArticles'

export function ArticleList() {
  const {
    articles,
    error,
    isLoadingInitialData,
    isLoadingMore,
    isReachingEnd,
    loadMore
  } = useArticles()

  if (error) return <div>加载失败</div>
  if (isLoadingInitialData) return <div>加载中...</div>

  return (
    <div>
      {articles.map(article => (
        <article key={article.id}>
          <h2>{article.title}</h2>
          <div>作者: {article.author.name}</div>
          <p>{article.content}</p>
        </article>
      ))}
      
      {!isReachingEnd && (
        <button
          onClick={loadMore}
          disabled={isLoadingMore}
        >

          {isLoadingMore ? '加载中...' : '加载更多'}
        </button>
      )}
    </div>

  )
}

三、高级特性应用

1. 预加载与数据预取

// utils/prefetch.ts
import { preload } from 'swr'

export function prefetchArticle(articleIdstring) {
  preload(`/api/articles/${articleId}`async (url) => {
    const res = await fetch(url)
    return res.json()
  })
}

// 在文章列表中预加载
function ArticlePreview({ article }) {
  return (
    <div onMouseEnter={() => prefetchArticle(article.id)}>
      <h3>{article.title}</h3>
    </div>

  )
}

2. 全局配置与错误处理

// app/swr-provider.tsx
'use client'
import { SWRConfig } from 'swr'

export function SWRProvider({ children }) {
  return (
    <SWRConfig
      value={{
        // 全局 fetcher
        fetcher: async (url: string) =>
 {
          const res = await fetch(url)
          if (!res.ok) {
            const error = new Error('API 请求失败')
            error.info = await res.json()
            error.status = res.status
            throw error
          }
          return res.json()
        },
        // 全局错误重试配置
        onErrorRetry: (error, key, config, revalidate, { retryCount }) => {
          // 401 错误不重试
          if (error.status === 401) return
          
          // 最多重试 3 次
          if (retryCount >= 3) return
          
          // 5 秒后重试
          setTimeout(() => revalidate({ retryCount }), 5000)
        }
      }}
    >
      {children}
    </SWRConfig>

  )
}

3. 条件请求与数据依赖

// hooks/useComments.ts
export function useComments(articleIdstring) {
  const { data: article } = useSWR(`/api/articles/${articleId}`)
  
  // 只有文章存在且允许评论时才获取评论
  const { data: comments } = useSWR(
    () => article && article.allowComments 
      ? `/api/articles/${articleId}/comments` 
      : null
  )

  return { comments }
}

四、性能优化实践

  1. 1. 合理使用缓存策略

const { data } = useSWR('/api/articles', {
  // 静态内容可以缓存更久
  dedupingInterval3600000// 1小时
  // 降低重新验证频率
  refreshInterval0,
  revalidateOnFocusfalse
})
  1. 1. 优化数据粒度

// 分离数据请求,避免不必要的重新渲染
function ArticlePage() {
  // 文章基础信息
  const { data: article } = useSWR(`/api/articles/${id}/basic`)
  // 文章统计信息
  const { data: stats } = useSWR(`/api/articles/${id}/stats`, {
    refreshInterval5000 // 频繁更新
  })
  // 文章内容
  const { data: content } = useSWR(`/api/articles/${id}/content`, {
    revalidateOnFocusfalse // 内容不常变化
  })
}


通过这个完整的博客系统案例,我们可以看到 SWR 在实际应用中的强大能力:

  1. 1. 简化了数据获取和状态管理

  2. 2. 提供了优秀的缓存策略

  3. 3. 支持复杂的数据依赖关系

  4. 4. 内置了性能优化机制

在 Next.js 14 中使用 SWR,可以充分利用服务端组件和客户端缓存,打造性能优秀的现代 Web 应用。希望这篇文章能帮助你更好地理解和使用 SWR!

更多关于 Next.js的全栈内容请点击下面的合集:


字节笔记本
专注于科技领域的分享,AIGC,全栈开发,产品运营
 最新文章