Next.jsStrapiREST APITypeScript

Next.js 连接 Strapi 5:数据获取、缓存与安全

在 Next.js App Router 中封装 Strapi 5 REST 请求,处理 populate、缓存、错误、草稿预览和 API Token,避免把私密凭据暴露到客户端。

·更新于 ·阅读约 12 分钟·计算中...
Next.js 连接 Strapi 5:数据获取、缓存与安全

Next.js 连接 Strapi 的推荐边界是:页面和布局保持 Server Component,在服务端数据层调用 Strapi;只有交互需要时才下沉 Client Component。私密 API Token 只存在服务器环境变量中,不能使用 NEXT_PUBLIC_ 前缀。

目录

环境变量

STRAPI_URL=http://localhost:1337
STRAPI_API_TOKEN=replace-with-server-only-token

公开内容可以配置 Strapi Public 角色的只读权限,不一定需要 Token。草稿、会员内容和后台预览必须在服务端鉴权。

建立统一请求函数

// src/lib/strapi.ts
const STRAPI_URL = process.env.STRAPI_URL

if (!STRAPI_URL) {
  throw new Error("Missing STRAPI_URL")
}

type StrapiResponse<T> = {
  data: T
  meta?: Record<string, unknown>
}

export async function strapiFetch<T>(
  path: string,
  options: RequestInit & { next?: { revalidate?: number; tags?: string[] } } = {},
): Promise<StrapiResponse<T>> {
  const token = process.env.STRAPI_API_TOKEN
  const response = await fetch(`${STRAPI_URL}${path}`, {
    ...options,
    headers: {
      Accept: "application/json",
      ...(token ? { Authorization: `Bearer ${token}` } : {}),
      ...options.headers,
    },
    next: options.next ?? { revalidate: 300 },
  })

  if (!response.ok) {
    throw new Error(`Strapi ${response.status}: ${await response.text()}`)
  }

  return response.json()
}

统一封装能确保错误不会被静默吞掉,并让认证、缓存和日志策略集中维护。

在 Server Component 中读取列表

type Article = {
  documentId: string
  title: string
  slug: string
  excerpt: string
}

export default async function BlogPage() {
  const { data: articles } = await strapiFetch<Article[]>(
    "/api/articles?fields[0]=title&fields[1]=slug&fields[2]=excerpt",
    { next: { revalidate: 300, tags: ["articles"] } },
  )

  return (
    <ul>
      {articles.map((article) => (
        <li key={article.documentId}>{article.title}</li>
      ))}
    </ul>
  )
}

Strapi 5 文档通常使用 documentId,内容字段采用扁平结构。如果代码依赖 Strapi 4 的 attributes,需要先确认版本并迁移类型。

查询详情与关联内容

复杂查询建议用 qs 构造,避免手写长方括号 URL:

import qs from "qs"

const query = qs.stringify(
  {
    filters: { slug: { $eq: slug } },
    fields: ["title", "slug", "excerpt", "content"],
    populate: {
      cover: { fields: ["url", "alternativeText"] },
      author: { fields: ["name"] },
    },
  },
  { encodeValuesOnly: true },
)

const { data } = await strapiFetch<Article[]>(`/api/articles?${query}`)
const article = data[0]

列表页和详情页应该分别定义查询形状,不要为了复用一个函数让列表页也加载完整正文。关联查询详见 Strapi populate 指南

缓存如何选择

内容 建议
公开且很少更新的文章 revalidate 定时再验证
登录用户私有数据 cache: "no-store"
发布后需要立即刷新 cache tag + webhook 触发再验证
后台草稿预览 动态请求,不与公开缓存混用

不要给带用户身份或私密 Token 的个性化响应使用公共缓存。缓存键、请求头和部署平台行为需要在实际环境验证。

用 Route Handler 隔离客户端请求

浏览器交互确实需要请求 Strapi 时,可以让客户端访问自己的 Route Handler:

// app/api/articles/search/route.ts
import { NextRequest, NextResponse } from "next/server"

export async function GET(request: NextRequest) {
  const query = request.nextUrl.searchParams.get("q")?.trim() ?? ""

  if (query.length < 2) {
    return NextResponse.json({ data: [] })
  }

  const result = await searchArticles(query)
  return NextResponse.json(result)
}

这样可以在服务端限制输入、速率和返回字段,同时隐藏 Strapi 地址与私密 Token。但公开 API 地址本身不是秘密,真正的安全仍依赖 Strapi 权限配置。

图片域名与 URL

Strapi 媒体 URL 可能是相对地址:

export function strapiMedia(url?: string) {
  if (!url) return undefined
  if (url.startsWith("http")) return url
  return `${process.env.STRAPI_URL}${url}`
}

使用 Next.js Image 时,还需要在 Next 配置中允许对应的远程图片域名。生产环境最好通过对象存储或 CDN 提供媒体,并确认替代文本真实可用。

常见错误

  • 在 Client Component 中读取私密 Token。
  • 所有请求都使用 populate=*,导致响应不断膨胀。
  • 捕获错误后返回空数组,让线上故障看起来像“没有内容”。
  • 把 Strapi 4 与 Strapi 5 的响应类型混在一起。
  • 为私有内容错误配置静态缓存。
  • 只在前端隐藏按钮,却给 Public 角色开放写权限。

内容模型与权限还未确定时,先阅读 Strapi 5 入门指南

参考资料

订阅 FreeMac

每周精选:免费 Mac 软件评测、可信来源更新、替代方案和少折腾指南。