路由缓存

本页内容

Astro 的路由缓存会存储按需渲染页面的已渲染响应,因此缓存命中时可以跳过渲染。EmDash 会为页面所渲染的内容提供缓存标签,并在内容变更时使这些标签失效。页面可以缓存数天,而发布的内容通常在几秒内就能被访问者看到。

对象缓存存储的是查询结果,每个页面仍然需要渲染。配置了缓存提供商之后,匿名渲染会跳过对象缓存并直接从数据库读取,这样为已失效页面重新填充缓存的那次渲染就不会来自较旧的快照(参见缓存的内容)。随后由路由缓存为匿名访问者提供服务,而对象缓存主要用来加快已登录编辑者的渲染。

选择缓存提供商

路由缓存需要一个缓存提供商。请使用与你的部署相匹配的提供商:

部署方式提供商导入来源
Node.jsmemoryCache()astro/config
CloudflarecacheCloudflare()@astrojs/cloudflare/cache

memoryCache() 把条目保存在服务器进程的内存中。cacheCloudflare() 是一个 CDN 提供商:它把响应存储在 Cloudflare Workers Cache 中,缓存命中时根本不会运行 Worker。Astro 的 Netlify 和 Vercel 适配器同样提供 CDN 提供商;参见适配器缓存提供商。EmDash 通过 Astro 与提供商无关的 cache.invalidate() API 使缓存失效,因此它的标签适用于任何提供商。提供商差异介绍了取决于提供商的行为。astro dev 从不缓存,因此请针对生产构建来测试缓存。

配置路由缓存

注册提供商,并声明哪些路由可以被缓存、缓存多久,以及使用哪些标签。此示例使用 Cloudflare 提供商;在 Node 上,请使用 cache: { provider: memoryCache() },并从 astro/config 导入 memoryCache:

import { defineConfig } from "astro/config";
import { cacheCloudflare } from "@astrojs/cloudflare/cache";

export default defineConfig({
	// ...你现有的 output、适配器和集成
	cache: {
		provider: cacheCloudflare(),
	},
	routeRules: {
		"/": { maxAge: 86_400, swr: 604_800, tags: ["posts", "pages", "all"] },
		"/blog": { maxAge: 86_400, swr: 604_800, tags: ["posts", "all"] },
		"/blog/[slug]": { maxAge: 86_400, swr: 604_800, tags: ["posts", "all"] },
		"/sitemap.xml": { maxAge: 86_400, swr: 604_800, tags: ["posts", "pages", "all"] },
		"/sitemap-[collection].xml": { maxAge: 86_400, swr: 604_800, tags: ["posts", "pages", "all"] },
	},
});
  • maxAge 是缓存在路由重新渲染之前提供该条目的时长。EmDash 会在发布时使过期条目失效(见下文),但失效操作可能失败,因此 maxAge 同时也限定了页面最多可以保持过期多久,外加在 swr 重新验证期间返回的那一次过期响应。选择较长的值之前,请先查看提供商差异。
  • swr 让缓存在后台重新验证的同时继续提供过期条目,因此页面一旦被缓存,访问者几乎不必等待渲染。
  • tags 为条目命名,以便使其失效。请用集合名称为每个路由打上它所渲染的集合标签:EmDash 内置的失效机制使用集合名称,因此匹配的标签会被自动失效。在每个缓存路由上再加一个宽泛的额外标签(这里是 all,它不是集合名称,所以 EmDash 自己永远不会使它失效),可以作为紧急开关:使它失效,例如在你自己保护的端点中调用 context.cache.invalidate({ tags: ["all"] }),就能一次性清除所有内容。

查询辅助函数还会返回一个 cacheHint,你可以在页面内将它传给 Astro.cache.set()。它的标签指明了确切的条目(getEmDashEntry)、集合(getEmDashCollection)或页面外围资源(getMenuWithCacheHint、getSiteSettingsWithCacheHint、getTaxonomyTermsWithCacheHint、getWidgetAreaWithCacheHint)。这些正是 EmDash 在写入时使其失效的同一批标签,因此传入这些提示的页面,在它所渲染的条目或页面外围资源被写入时就会失效,包括其路由规则中没有列出的那些:

---
import { getEmDashEntry } from "emdash";

const { slug } = Astro.params;
if (!slug) return Astro.rewrite("/404");

const { entry, cacheHint } = await getEmDashEntry("posts", slug);
if (Astro.cache?.enabled) Astro.cache.set(cacheHint);
---

Astro.cache.set() 会与路由规则合并,标签在多次调用之间累积。缓存提示携带标签但不带 maxAge,因此页面缓存多久由路由规则决定。

请为每个公开路由提供明确的规则。规则按请求路径匹配,所以请逐个列出你的公开路由:站点根路径上的剩余参数,例如 /[...slug],同样会匹配 /_emdash/ 下的管理后台和 API 路由。

发布时的失效是内置的

只要线上内容发生变化,EmDash 的内容 API 路由就会使路由缓存失效:发布、取消发布、删除、从回收站恢复条目,以及保存影响线上条目的更改,都会使标签 [collection, id] 失效。创建或复制条目不会使任何内容失效,因为新条目是草稿。因此,即使 maxAge 很长,任何带有集合名称标签(如上所示)的路由规则通常也会在发布后的几秒内刷新。

站点外围内容也在覆盖范围内:保存站点设置会使 emdash:settings 失效,菜单编辑会使 emdash:menu:{name} 失效,分类法和小组件区域的写入会使与之对应的 emdash:taxonomy:{name} / emdash:widget-area:{name} 标签失效。当页面使用 getMenuWithCacheHint()、getSiteSettingsWithCacheHint()、getTaxonomyTermsWithCacheHint() 或 getWidgetAreaWithCacheHint() 返回的 cacheHint 时,会自动获得这些标签。

菜单标签仅覆盖菜单的编辑。发布、取消发布、移入回收站或恢复菜单所链接的条目,或者更改被链接条目或分类术语的 slug,都不会使该菜单的标签失效。显示该菜单的页面,如果其标签中同时包含该条目、其集合或该术语所属的分类法,就会获得此类变更;否则要等到页面过期时才会更新。

创建、编辑或删除重定向,会使位于该重定向源路径上(带与不带末尾斜杠)的缓存页面失效,使重定向在下一次请求时生效。

恢复较早的修订,以及对署名、媒体库和评论的更改,都不会使任何内容失效。受它们影响的缓存页面会在过期后,或渲染它们的条目再次被保存时更新。定时发布仅在 Cloudflare 上会使已发布条目的标签失效;参见提供商差异。

这也是标签约定很重要的原因:EmDash 会使集合名称、条目 id 和 emdash:* 外围标签失效。如果你的路由规则使用其他标签名称,内置的失效机制将无法匹配它们,过期条目会一直保留到 maxAge 到期。

让响应不进入缓存

路由规则按请求路径匹配,因此它们也会作用于你不希望以较长 TTL 缓存的响应。EmDash 会自动让为已登录用户渲染的响应、带有 Cache-Control: private 或 no-store 发送的响应,以及 404 响应不进入路由缓存。其他错误响应以及你自己的中间件重定向,则由模板负责,通过 Astro.cache.set(false) 按请求选择退出。

Astro.cache.set(false) 只会保持到下一次 Astro.cache.set() 调用为止。布局在页面前置内容之后渲染,因此,设置了缓存提示的布局会让已选择退出的页面重新开启缓存。请把退出设置传给布局(例如作为 prop),并在设置了它时跳过布局中的 Astro.cache.set() 调用。

预览和编辑链接会被自动处理

EmDash 的中间件会自行让这些响应退出路由缓存,因此无需任何模板代码:携带 ?_preview= 令牌的请求(无论有效与否,因为这些 URL 是按令牌区分的,否则缓存的草稿在令牌过期后仍然可以被访问),以及注入了编辑器工具栏的响应。使用 toolbar: "client" 时,?_edit URL 同样适用。有效的预览响应和注入了工具栏的响应还会额外为浏览器和下游 CDN 标记 Cache-Control: private, no-store。

错误响应

EmDash 会让每个 404 都不进入路由缓存,因此之后发布一个原本缺失的条目时,它会立即显示出来。其他错误响应,例如缓存路由上渲染出的 500,会继承规则的 TTL。请使用 Astro.cache.set(false) 让它们退出缓存。

中间件中的重定向

在中间件中通过重定向直接返回的情况(例如主机名规范化),必须使用 context.cache?.set(false) 选择退出。否则该重定向会按路由规则的 TTL 被缓存,而在不按主机名区分缓存条目的 CDN 提供商上,它还可能被提供给针对规范主机名的请求,从而立即造成重定向循环。请使用可选链:context.cache 在某些响应上是 undefined,例如与任何路由都不匹配的 URL 所返回的 404 页面。

编辑者与缓存页面

路由缓存为每个 URL 存储一个共享的变体,其缓存键会忽略 cookie,因此缓存命中永远不会反映编辑者的会话。使用默认的服务端渲染工具栏时,浏览公开站点的编辑者通常会得到缓存的匿名变体(不含工具栏),emdash-edit-mode cookie 对已缓存的 URL 不起作用。为已登录编辑者渲染的页面绝不会存入共享缓存;由下一位匿名访问者来填充它。

对于使用路由缓存的站点,请设置 toolbar: "client"。随后会有一个小脚本在缓存页面上向已登录编辑者显示编辑按钮,而编辑视图从始终实时渲染的 ?_edit URL 加载。无论哪种模式,预览链接都会绕过缓存。

提供商差异

使用 memoryCache() 的 Node.js

  • 每个服务器进程都有自己的缓存,失效操作只会清除处理该写入的进程。如果你运行多个进程或实例,其他进程会继续提供它们缓存的页面,直到 maxAge 到期。
  • 重启会清空缓存。
  • memoryCache() 只存储带有 maxAge 的响应,因此没有规则的路由永远不会被缓存,即使页面使用缓存提示调用了 Astro.cache.set()。
  • 在 Node 上,定时发布不会使路由缓存失效。列出了定时发布条目的页面会在其缓存副本过期后才显示该条目,因此在列出定时内容的路由上请保持较短的 maxAge。

使用 cacheCloudflare() 的 Cloudflare

  • 该提供商会写入 Cloudflare-CDN-Cache-Control 和 Cache-Tag 响应标头。Workers Cache 会使用这两个标头;客户端永远看不到它们,你的页面为浏览器设置的任何 Cache-Control 标头也不受影响。
  • 配置了缓存提供商时,@astrojs/cloudflare 适配器会自动启用 Workers Cache:构建会把 "cache": { "enabled": true } 写入生成的 Wrangler 配置,wrangler deploy 会读取该配置。
  • 没有规则的路由会被标记为 no-store,但前提是在它渲染期间没有任何代码调用 Astro.cache.set()。不带 maxAge 的缓存提示(例如来自 getSiteSettingsWithCacheHint() 和 getMenuWithCacheHint() 的提示)会产生不带有效期的 Cloudflare-CDN-Cache-Control: public,因此 Workers Cache 会回退到它的默认缓存时长(200 响应为 2 小时)。如果你的布局设置了这些提示,那么每个使用该布局的页面,无论有没有规则,都会变得可缓存。
  • 默认情况下,Worker 版本是缓存键的一部分,因此每次部署都从空缓存开始,部署后的最初几次请求会在 Worker 中渲染。若要让条目在多次部署之间保留,请在 Wrangler 配置中设置 "cache": { "enabled": true, "cross_version_cache": true }(参见跨版本缓存)。这样一来,模板更改只会在受影响的条目过期或被失效之后才会显示。
  • 定时发布会由 Cron Trigger 使其所发布条目的标签失效。

清除速率限制

Cloudflare 提供商通过清除 Workers Cache 来实现失效。无论你的账户使用哪种套餐,Workers Cache 的清除始终采用 Cloudflare 的免费套餐清除限制:每个账户每分钟 5 个请求,突发上限为 25(参见清除速率限制)。大多数内容写入各消耗一次清除请求:改变线上内容的保存(包括在直接保存到线上条目的集合中的自动保存)、发布、取消发布、删除、恢复、排期,以及为条目分配分类法术语,外加每一次设置、菜单、分类法或小组件的编辑。一次重定向写入会按源路径变体各消耗一次清除请求,因此当重定向的源发生变化时,最多会消耗四次。

被限速的清除会在管理后台中静默失败,受影响的页面会保留其缓存版本,直到 maxAge 到期。在批量编辑期间,或有多名编辑者同时工作时,请选择一个你能接受的 maxAge,把它当作内容过期的最坏情况。

浏览器缓存是独立的

路由缓存之所以能够使用较长的 TTL,是因为 EmDash 会使它失效;而浏览器缓存不会被失效。EmDash 的提示只提供清除标签,不会自动提供 Last-Modified 验证器,也不会提供 Astro 隐式的 Cache-Control: no-cache 标头。单个条目的修改时间并不涵盖设置、相关列表或已删除的条目,因此把它用作页面验证器,可能会通过条件性的 304 响应保留过期的正文。

请使用 Astro.response.headers.set("Cache-Control", ...) 设置浏览器缓存:使用 no-cache 让每次访问都重新检查,或者使用较短的有效期(以分钟计,而不是以天计)。若要启用条件请求,请提供你自己的 lastModified 提示,并确保响应的任何依赖项发生变化时,其时间戳都会随之前进。

对于条目所显示的修改日期,请使用 entry.data.updatedAt,而不是 EmDash 不会自动填充的 cacheHint.lastModified。