后端在服务端调用外部 API 并管理缓存

在服务端调用外部 API 并管理缓存

在 getServerSideProps 里取站点之外的数据,用 ctx.cacheDepends 声明依赖,再通过发布接口作废页面缓存——不声明依赖,页面会一直停在第一次渲染的结果上。

页面可以在服务端取站点之外的数据:第三方 API、你自己的服务、平台的公开接口。渲染结果会进 HTML 缓存,而缓存只在收到 publish 事件时失效。站点自己的数据(CMS、表单、数据表)取数时会自动登记依赖,外部数据不会——所以外部数据必须由页面自己声明依赖,否则页面会一直显示第一次渲染时的内容。

取数放在哪里

放在 getServerSideProps

公开的只读数据、首屏就要出现、需要被搜索引擎和 AI 抓到的内容。数据会渲染进 HTML。

需要密钥就放 Func

getServerSidePropsctx 里没有密钥,也没有数据库写入能力。凡是要带 API Key、要签名、要落库的调用,写在 /backend/func 里,页面调用函数键。

只在交互后才需要就放客户端

点击后才加载的数据用浏览器端 fetch。它不进 HTML 缓存,也就不需要声明依赖。

站点自己的数据不用管

通过 talizen/cms 等平台包取的数据会自动登记依赖,内容一改缓存就失效,不需要 cacheDepends

在服务端调用外部 API

fetch 在服务端渲染期间是全局可用的,用法与浏览器一致。返回的 props 会被序列化进 HTML 交给页面组件。

// /page/templates.tsx
export async function getServerSideProps(ctx) {
  const res = await fetch('https://api.example.com/v1/items?limit=12')

  // 外部服务挂了不应该让整个页面 500,给一个能渲染的兜底。
  if (!res.ok) return { props: { items: [] } }

  const data = await res.json()
  return { props: { items: data.list ?? [] } }
}

export default function Templates({ items }) {
  return (
    <ul>
      {items.map((item) => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  )
}

抛出异常会让页面渲染失败。外部依赖务必判断响应状态并准备降级内容。

声明缓存依赖

getServerSideProps 里调用 ctx.cacheDepends(key),告诉渲染端「这个页面依赖名为 key 的外部数据」。key 是你自己定义的字符串,只要发布时用同一个即可。

export async function getServerSideProps(ctx) {
  ctx.cacheDepends('templates')

  const res = await fetch('https://api.example.com/v1/items?limit=12')
  if (!res.ok) return { props: { items: [] } }
  return { props: { items: (await res.json()).list ?? [] } }
}
  • 可以一次声明多个:ctx.cacheDepends('templates', 'template-tags'),也可以分多次调用,重复的 key 会自动去重。
  • 实际记录到缓存里的是 custom/{site_id}/templates。站点命名空间由渲染端按当前站点补上,页面写不出别的站点的 key。
  • 只要在 getServerSideProps 执行期间调用即可,放在 fetch 之前或之后都行。
  • 不声明的后果:这个页面只会在站点发布、页面改动这类既有事件下失效,外部数据再怎么变都不会重新渲染。

作废缓存

外部数据变更之后,调用发布接口把声明了该 key 的页面缓存作废。下一个访问者会触发重新渲染并拿到新数据。

POST /api/p/project/{project_id}/site/{site_id}/render_cache/publish
Authorization: Bearer <token>
Content-Type: application/json

{ "keys": ["templates"] }

返回实际被作废的依赖 key:

{ "published": ["custom/{site_id}/templates"] }

权限

与「发布站点」相同。站点命名空间取自 URL 上的 site_id,不是请求体里的字段,因此拿这个接口清不到别人站点的缓存。

keys 不能为空

空数组或全是空字符串会返回 400。这种调用不会清掉任何东西,返回 200 只会让你以为生效了。

一次最多 20 个

超过返回 400。正常场景不需要一次发这么多。

发布未声明的 key 是安全的

不会报错,只是没有任何缓存被清掉。

作废是「删除」而不是「重算」:缓存条目直接失效,重新渲染的成本由下一个访问者承担。需要预热就在发布之后自己请求一次页面。

完整例子

一个从外部接口取列表、支持按分类筛选的页面。注意 key 与筛选条件无关:同一份数据源的所有分页和筛选组合共用一个 key,一次发布全部作废。

// /page/templates.tsx
const API = 'https://api.example.com/v1/templates'

export async function getServerSideProps(ctx) {
  ctx.cacheDepends('templates')

  const category = ctx.query.category ?? ''
  const url = category ? `${API}?category=${encodeURIComponent(category)}` : API

  const res = await fetch(url)
  if (!res.ok) return { props: { items: [], category } }

  const data = await res.json()
  return { props: { items: data.list ?? [], category } }
}

外部数据变更后:

await fetch(
  `https://creght.cn/api/p/project/${projectId}/site/${siteId}/render_cache/publish`,
  {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${token}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ keys: ['templates'] }),
  },
)

缓存边界

  • 缓存按站点、域名、路径和查询串区分。?category=a?category=b 是两条缓存,但它们声明的是同一个 key,一次发布会一起作废。
  • getServerSideProps 里读 Cookie,页面会按读到的 Cookie 名分变体缓存;写 Cookie 则整页不进缓存,此时声明依赖没有意义。
  • 预览模式不走缓存,调试时看到的永远是最新数据。要验证缓存行为请用正式域名访问。
  • props 会原样序列化进 HTML,浏览器可见。只放页面要渲染的字段,不要把上游接口的完整响应直接透传出去。

检查清单

  • 外部接口的调用写在 getServerSideProps 里,需要密钥或写入的改走 Func。
  • 响应状态判断和降级内容都写了,外部服务不可用时页面仍能渲染。
  • 每一份外部数据都有对应的 ctx.cacheDepends(key)
  • 数据变更方会调用发布接口,key 与页面里写的完全一致。
  • props 里没有密钥、内部 ID 和用不到的上游字段。

Render diagnostics