跳转到内容

实验性的实时内容集合

类型: boolean
默认值: false

添加于: astro@5.10.0

在你的项目中启用对实时内容集合的支持。

实时内容集合是一种新型的内容集合,它在运行时而不是构建时获取数据。这使你可以使用统一的 API 从 CMS、API、数据库或其他来源访问频繁更新的数据,而无需在数据更改时重建你的网站。

要启用此功能,请确保你已为按需渲染配置了适配器,并将 experimental.liveContentCollections 标志添加到你的 astro.config.mjs 文件中

astro.config.mjs
{
experimental: {
liveContentCollections: true,
},
}

然后在 src/content.config.ts 文件(如果你有的话)旁边创建一个新的 src/live.config.ts 文件,使用来自 astro:content 模块的新的 defineLiveCollection() 函数,通过实时加载器和可选的模式来定义你的实时集合。

src/live.config.ts
import { defineLiveCollection } from 'astro:content';
import { storeLoader } from '@mystore/astro-loader';
const products = defineLiveCollection({
loader: storeLoader({
apiKey: process.env.STORE_API_KEY,
endpoint: 'https://api.mystore.com/v1',
}),
});
export const collections = { products };

然后,你可以使用专用的 getLiveCollection()getLiveEntry() 函数来访问你的实时数据

---
export const prerender = false; // Not needed in 'server' mode
import { getLiveCollection, getLiveEntry } from 'astro:content';
// Get all products
const { entries: allProducts, error } = await getLiveCollection('products');
if (error) {
// Handle error appropriately
console.error(error.message);
}
// Get products with a filter (if supported by your loader)
const { entries: electronics } = await getLiveCollection('products', { category: 'electronics' });
// Get a single product by ID (string syntax)
const { entry: product, error: productError } = await getLiveEntry('products', Astro.params.id);
if (productError) {
return Astro.redirect('/404');
}
// Get a single product with a custom query (if supported by your loader) using a filter object
const { entry: productBySlug } = await getLiveEntry('products', { slug: Astro.params.slug });
---

实时内容集合专为频繁更改且在请求页面时需要保持最新的数据而设计。考虑在以下情况使用它们:

  • 你需要实时信息(例如,用户特定数据、当前库存水平)
  • 你想避免为经常更改的内容进行不断的重建
  • 你的数据更新频繁(例如,最新的产品库存、价格、可用性)
  • 你需要根据用户输入或请求参数将动态过滤器传递给你的数据源
  • 你正在为 CMS 构建预览功能,其中编辑者需要立即看到草稿内容

相反,在以下情况使用构建时内容集合:

  • 性能至关重要,你希望在构建时预渲染数据
  • 你的数据相对静态(例如,博客文章、文档、产品描述)
  • 你想从构建时优化和缓存中受益
  • 你需要处理 MDX 或执行图像优化
  • 你的数据可以一次性获取并在多个构建中重复使用

有关在实时集合和预加载集合之间进行选择的更多详细信息,请参阅实验性实时集合的限制与构建时集合的主要区别

你可以为你的数据源创建自己的实时加载器,也可以使用作为 npm 包分发的社区加载器。以下是如何使用示例 CMS 和电子商务加载器的方法:

src/live.config.ts
import { defineLiveCollection } from 'astro:content';
import { cmsLoader } from '@example/cms-astro-loader';
import { productLoader } from '@example/store-astro-loader';
const articles = defineLiveCollection({
loader: cmsLoader({
apiKey: process.env.CMS_API_KEY,
contentType: 'article',
}),
});
const products = defineLiveCollection({
loader: productLoader({
apiKey: process.env.STORE_API_KEY,
}),
});
export const collections = { articles, authors };

然后,你可以通过统一的 API 从两个加载器获取内容:

---
export const prerender = false; // Not needed in 'server' mode
import { getLiveCollection, getLiveEntry } from 'astro:content';
// Use loader-specific filters
const { entries: draftArticles } = await getLiveCollection('articles', {
status: 'draft',
author: 'john-doe',
});
// Get a specific product by ID
const { entry: product } = await getLiveEntry('products', Astro.params.slug);
---

实时加载器可能会因为网络问题、API 错误或验证问题而失败。该 API 旨在使错误处理变得明确。

当你调用 getLiveCollection()getLiveEntry() 时,错误将是以下之一:

  • 加载器定义的错误类型(如果它返回了错误)
  • 如果未找到条目,则为 LiveEntryNotFoundError
  • 如果集合数据与预期模式不匹配,则为 LiveCollectionValidationError
  • 如果缓存提示无效,则为 LiveCollectionCacheHintError
  • 对于其他错误,例如在加载器中抛出的未捕获错误,则为 LiveCollectionError

这些错误有一个静态的 is() 方法,你可以在运行时使用它来检查错误的类型:

---
export const prerender = false; // Not needed in 'server' mode
import { getLiveEntry, LiveEntryNotFoundError } from 'astro:content';
const { entry, error } = await getLiveEntry('products', Astro.params.id);
if (error) {
if (LiveEntryNotFoundError.is(error)) {
console.error(`Product not found: ${error.message}`);
Astro.response.status = 404;
} else {
console.error(`Error loading product: ${error.message}`);
return Astro.redirect('/500');
}
}
---

实时加载器是一个具有两个方法的对象:loadCollection()loadEntry()。这些方法应该优雅地处理错误,并返回数据或一个Error对象。

标准模式是导出一个返回此加载器对象的函数,允许你传递 API 密钥或端点等配置选项。

这是一个基本示例:

myloader.ts
import type { LiveLoader } from 'astro/loaders';
import { fetchFromCMS } from './cms-client.js';
interface Article {
id: string;
title: string;
content: string;
author: string;
}
export function articleLoader(config: { apiKey: string }): LiveLoader<Article> {
return {
name: 'article-loader',
loadCollection: async ({ filter }) => {
try {
const articles = await fetchFromCMS({
apiKey: config.apiKey,
type: 'article',
filter,
});
return {
entries: articles.map((article) => ({
id: article.id,
data: article,
})),
};
} catch (error) {
return {
error: new Error(`Failed to load articles: ${error.message}`),
};
}
},
loadEntry: async ({ filter }) => {
try {
// filter will be { id: "some-id" } when called with a string
const article = await fetchFromCMS({
apiKey: config.apiKey,
type: 'article',
id: filter.id,
});
if (!article) {
return {
error: new Error('Article not found'),
};
}
return {
id: article.id,
data: article,
};
} catch (error) {
return {
error: new Error(`Failed to load article: ${error.message}`),
};
}
},
};
}

加载器可以通过在条目中返回一个 rendered 属性来增加对直接渲染内容的支持。这允许你使用render() 函数和 <Content /> 组件直接在你的页面中渲染内容。如果加载器没有为条目返回 rendered 属性,<Content /> 组件将不会渲染任何内容。

myloader.ts
// ...
export function articleLoader(config: { apiKey: string }): LiveLoader<Article> {
return {
name: 'article-loader',
loadEntry: async ({ filter }) => {
try {
const article = await fetchFromCMS({
apiKey: config.apiKey,
type: 'article',
id: filter.id,
});
return {
id: article.id,
data: article,
rendered: {
// Assuming the CMS returns HTML content
html: article.htmlContent,
},
};
} catch (error) {
return {
error: new Error(`Failed to load article: ${error.message}`),
};
}
},
// ...
};
}

然后,你可以使用与构建时集合相同的方法,在页面中渲染实时集合条目的内容和元数据。你还可以访问实时加载器返回的任何错误,例如,在无法显示内容时重写到 404 页面:

---
export const prerender = false; // Not needed in 'server' mode
import { getLiveEntry, render } from 'astro:content';
const { entry, error } = await getLiveEntry('articles', Astro.params.id);
if (error) {
return Astro.rewrite('/404');
}
const { Content } = await render(entry);
---
<h1>{entry.data.title}</h1>
<Content />

加载器应处理所有错误,并为错误返回一个Error子类。如果需要,你可以创建自定义错误类型,并使用它们进行更具体的错误处理。如果在加载器中抛出错误,它将被捕获并返回,包装在 LiveCollectionError 中。你还可以创建自定义错误类型以实现正确的类型定义。

Astro 会根据加载器的响应自行生成一些错误:

  • 如果 loadEntry 返回 undefined,Astro 将向用户返回一个 LiveEntryNotFoundError
  • 如果为集合定义了模式,并且数据与模式不匹配,Astro 将返回一个 LiveCollectionValidationError
  • 如果加载器返回无效的缓存提示,Astro 将返回一个 LiveCollectionCacheHintErrorcacheHint 字段是可选的,因此如果你没有有效的数据可以返回,可以简单地省略它。
my-loader.ts
import type { LiveLoader } from 'astro/loaders';
import { MyLoaderError } from './errors.js';
export function myLoader(config): LiveLoader<MyData, undefined, undefined, MyLoaderError> {
return {
name: 'my-loader',
loadCollection: async ({ filter }) => {
// Return your custom error type
return {
error: new MyLoaderError('Failed to load', 'LOAD_ERROR'),
};
},
// ...
};
}

加载器可以在你的网站中定义,也可以作为一个单独的 npm 包。如果你想与社区分享你的加载器,你可以使用 astro-componentastro-loader 关键字将其发布到 NPM

加载器应该导出一个返回 LiveLoader 对象的函数,允许用户使用他们自己的设置来配置它。

与常规内容集合一样,实时集合可以被类型化以确保你数据中的类型安全。支持使用 Zod 模式,但并非定义实时集合类型的必要条件。与构建时定义的预加载集合不同,实时加载器可以选择将泛型类型传递给 LiveLoader 接口。你可以为你的集合和条目数据定义类型,以及用于查询的自定义过滤器类型和用于错误处理的自定义错误类型。

实时加载器可以为其返回的数据定义类型。这使得 TypeScript 在你的组件中使用数据时能够提供类型检查和自动补全。

store-loader.ts
import type { LiveLoader } from 'astro/loaders';
import { fetchProduct, fetchCategory, type Product } from './store-client';
export function storeLoader(): LiveLoader<Product> {
// ...
}

当你使用 getLiveCollection()getLiveEntry() 时,TypeScript 将根据加载器的定义推断类型:

---
export const prerender = false; // Not needed in 'server' mode
import { getLiveEntry } from 'astro:content';
const { entry: product } = await getLiveEntry('products', '123');
// TypeScript knows product.data is of type Product
console.log(product?.data.name);
---

实时加载器可以为 getLiveCollection()getLiveEntry() 定义自定义过滤器类型。这可以实现与你的 API 功能相匹配的类型安全查询,使用户更容易发现可用的过滤器并确保它们被正确使用。如果你在过滤器类型中包含 JSDoc 注释,用户在使用加载器时会在其 IDE 中看到这些提示。

store-loader.ts
import type { LiveLoader } from 'astro/loaders';
import { fetchProduct, fetchCategory, type Product } from './store-client';
interface CollectionFilter {
category?: string;
/** Minimum price to filter products */
minPrice?: number;
/** Maximum price to filter products */
maxPrice?: number;
}
interface EntryFilter {
/** Alias for `sku` */
id?: string;
slug?: string;
sku?: string;
}
export function productLoader(config: {
apiKey: string;
endpoint: string;
}): LiveLoader<Product, EntryFilter, CollectionFilter> {
return {
name: 'product-loader',
loadCollection: async ({ filter }) => {
// filter is typed as CollectionFilter
const data = await fetchCategory({
apiKey: config.apiKey,
category: filter?.category ?? 'all',
minPrice: filter?.minPrice,
maxPrice: filter?.maxPrice,
});
return {
entries: data.products.map((product) => ({
id: product.sku,
data: product,
})),
};
},
loadEntry: async ({ filter }) => {
// filter is typed as EntryFilter | { id: string }
const product = await fetchProduct({
apiKey: config.apiKey,
slug: filter.slug,
sku: filter.sku || filter.id,
});
if (!product) {
return {
error: new Error('Product not found'),
};
}
return {
id: product.sku,
entry: product,
};
},
};
}

你可以为加载器返回的错误创建自定义错误类型,并将它们作为泛型传递以获得正确的类型定义:

my-loader.ts
class MyLoaderError extends Error {
constructor(
message: string,
public code?: string
) {
super(message);
this.name = 'MyLoaderError';
}
}
export function myLoader(config): LiveLoader<MyData, undefined, undefined, MyLoaderError> {
return {
name: 'my-loader',
loadCollection: async ({ filter }) => {
// Return your custom error type
return {
error: new MyLoaderError('Failed to load', 'LOAD_ERROR'),
};
},
// ...
};
}

当你使用 getLiveCollection()getLiveEntry() 时,TypeScript 将推断出自定义错误类型,从而允许你适当地处理它:

---
export const prerender = false; // Not needed in 'server' mode
import { getLiveEntry } from 'astro:content';
const { entry, error } = await getLiveEntry('products', '123');
if (error) {
if (error.name === 'MyLoaderError') {
console.error(`Loader error: ${error.message} (code: ${error.code})`);
} else {
console.error(`Unexpected error: ${error.message}`);
}
return Astro.rewrite('/500');
}
---

就像构建时集合一样,你可以将 Zod 模式与实时集合一起使用,在运行时验证和转换数据。当你定义一个模式时,它在查询集合时会优先于加载器的类型

src/live.config.ts
import { z, defineLiveCollection } from 'astro:content';
import { apiLoader } from './loaders/api-loader';
const products = defineLiveCollection({
loader: apiLoader({ endpoint: process.env.API_URL }),
schema: z
.object({
id: z.string(),
name: z.string(),
price: z.number(),
// Transform the API's category format
category: z.string().transform((str) => str.toLowerCase().replace(/\s+/g, '-')),
// Coerce the date to a Date object
createdAt: z.coerce.date(),
})
.transform((data) => ({
...data,
// Add a formatted price field
displayPrice: `$${data.price.toFixed(2)}`,
})),
});
export const collections = { products };

使用 Zod 模式时,验证错误会自动被捕获并作为 AstroError 对象返回:

---
export const prerender = false; // Not needed in 'server' mode
import { getLiveEntry, LiveCollectionValidationError } from 'astro:content';
const { entry, error } = await getLiveEntry('products', '123');
// You can handle validation errors specifically
if (LiveCollectionValidationError.is(error)) {
console.error(error.message);
return Astro.rewrite('/500');
}
// TypeScript knows entry.data matches your Zod schema, not the loader's type
console.log(entry?.data.displayPrice); // e.g., "$29.99"
---

实时加载器可以提供缓存提示以帮助响应缓存。你可以使用这些数据来发送 HTTP 缓存头或以其他方式为你的缓存策略提供信息。

my-loader.ts
export function myLoader(config): LiveLoader<MyData> {
return {
name: 'cached-loader',
loadCollection: async ({ filter }) => {
// ... fetch data
return {
entries: data.map((item) => ({
id: item.id,
data: item,
// You can optionally provide cache hints for each entry
cacheHint: {
tags: [`product-${item.id}`, `category-${item.category}`],
},
})),
cacheHint: {
// All fields are optional, and are combined with each entry's cache hints
// tags are merged from all entries
// maxAge is the shortest maxAge of all entries and the collection
// lastModified is the most recent lastModified of all entries and the collection
lastModified: new Date(item.lastModified),
tags: ['products'],
maxAge: 300, // 5 minutes
},
};
},
loadEntry: async ({ filter }) => {
// ... fetch single item
return {
id: item.id,
data: item,
cacheHint: {
lastModified: new Date(item.lastModified),
tags: [`product-${item.id}`, `category-${item.category}`],
maxAge: 3600, // 1 hour
},
};
},
};
}

然后你可以在你的页面中使用这些提示:

---
export const prerender = false; // Not needed in 'server' mode
import { getLiveEntry } from 'astro:content';
const { entry, error, cacheHint } = await getLiveEntry('products', Astro.params.id);
if (error) {
return Astro.redirect('/404');
}
// Apply cache hints to response headers
if (cacheHint?.tags) {
Astro.response.headers.set('Cache-Tag', cacheHint.tags.join(','));
}
if (cacheHint?.maxAge) {
Astro.response.headers.set('Cache-Control', `s-maxage=${cacheHint.maxAge}`);
}
if (cacheHint?.lastModified) {
Astro.response.headers.set('Last-Modified', cacheHint.lastModified.toUTCString());
}
---
<h1>{entry.data.name}</h1>
<p>{entry.data.description}</p>

与构建时集合相比,实时内容集合有一些限制:

  • 不支持 MDX:MDX 无法在运行时渲染
  • 没有图像优化:图像无法在运行时处理
  • 性能考虑:每次请求都会获取数据(除非已缓存)
  • 没有数据存储持久化:数据不会保存到内容层数据存储中

实时集合使用的 API 与当前的预加载内容集合不同。主要区别包括:

  1. 执行时间:在请求时运行,而不是在构建时运行
  2. 配置文件:使用 src/live.config.ts 而不是 src/content.config.ts
  3. 集合定义:使用 defineLiveCollection() 而不是 defineCollection()
  4. 加载器 API:实现 loadCollectionloadEntry 方法,而不是 load 方法
  5. 数据返回:直接返回数据,而不是存储在数据存储中
  6. 面向用户的函数:使用 getLiveCollection/getLiveEntry 而不是 getCollection/getEntry

要获得此实验性 API 的完整概述并提供反馈,请参阅实时内容集合 RFC

贡献 社区 赞助