Astro-路由导航

Astro 支持 src/pages/ 目录中的以下文件类型:

页面

页面是位于 Astro 项目的 src/pages/ 子目录中的文件。它们负责处理路由、数据加载以及网站中每个页面的整体页面布局。

支持的页面文件

Astro 支持 src/pages/ 目录中的以下文件类型:

基于文件的路由

Astro 使用一种名为 基于文件的路由 的路由策略。src/pages/ 目录中的每个文件都会根据其文件路径成为网站上的一个端点。

一个文件也可以使用动态路由来生成多个页面。这允许你即使内容不在特殊的 /pages/ 目录中,也可以创建页面,例如在内容集合内容管理系统中。

页面之间的链接

在你的 Astro 页面中,使用标准的 HTML <a> 元素来链接到网站上的其他页面。这需要使用 相对于根域名的 URL 路径 作为你的链接,而不是相对文件路径。

例如,要从 example.com 上的任何其他页面链接到 https://example.com/authors/sonali/,你可以像这样:

src/pages/index.astro

html
阅读更多 <a href="/authors/sonali/">关于 Sonali 的信息</a>。

Astro 页面

Astro 页面使用 .astro 文件扩展名,并支持与 Astro 组件相同的功能。

src/pages/index.astro

html
---
---
<html lang="en">
  <head>
    <title>我的主页</title>
  </head>
  <body>
    <h1>欢迎来到我的网站</h1>
  </body>
</html>

一个页面必须生成完整的 HTML 文档。如果没有显式包含,Astro 将会自动为位于 src/pages/ 目录下的任何 .astro 组件默认添加必要的 <!DOCTYPE html> 声明和 <head> 内容。你可以通过将组件标记为局部页面来为每个组件避免这种行为。

为了避免在每个页面上重复相同的 HTML 元素,你可以将常见的 <head><body> 元素移动到自己的 布局组件 中。你可以使用任意多的布局组件。

src/pages/index.astro

html
---

import MySiteLayout from '../layouts/MySiteLayout.astro';

---

<MySiteLayout>

  <p>包裹在 layout 里的页面内容!</p>

</MySiteLayout>

Markdown/MDX 页面

Astro 还将 src/pages/ 目录中的任何 Markdown (.md) 文件视为网站中的页面。如果你安装了 MDX 集成,它也会将 MDX (.mdx) 文件视为相同的页面。

备注

当内容为博客文章或者产品项目这类共享相似结构的相关 Markdown 文件时,请考虑创建 内容集合 而不是页面。

Markdown 文件可以使用特殊的 layout 前置属性来指定一个 布局组件,它将包装其 Markdown 内容在一个完整的 <html>...</html> 页面文档中。

src/pages/page.md

yaml
---
layout: ../layouts/MySiteLayout.astro
title: 我的 Markdown 页面
---
# 标题

这是我的页面,使用 **Markdown** 编写。

HTML 页面

.html 文件扩展名的文件可以放在 src/pages/ 中,并直接用作网站上的页面。请注意,某些关键的 Astro 功能在 HTML 组件 中不受支持。

自定义 404 错误页面

想要自定义 404 错误页面,你可以在 src/pages 中创建 404.astro404.md 文件。

它将生成 404.html 页面。大多数部署服务都自动找到并使用它。

自定义 500 错误页面

要为按需渲染的页面创建自定义的 500 错误页面,可以创建一个 src/pages/500.astro 文件。但这个自定义页面不适用于预渲染的页面。

如果在渲染此页面时发生错误,将显示主机默认的 500 错误页面给访问者。

添加于: astro@4.10.3

在开发过程中,如果你有一个 500.astro 文件,运行时抛出的错误将被记录在终端中,而不是显示在错误覆盖层中。

error

添加于: astro@4.11.0

src/pages/500.astro 是一个特殊页面,它会在渲染过程中对抛出的任何错误都会自动传递一个 error 属性。这允许你向访客展示详细的错误信息(例如来自页面、中间件等)。

error 属性的数据类型可以是任何类型,这可能会影响你在代码中的定义或使用值的方式:

src/pages/500.astro

js
---
interface Props {
  error: unknown;
}
const { error } = Astro.props;
---

<div>{error instanceof Error ? error.message : "Unknown error"}</div>

为了避免在展示内容时从 error 属性中显示敏感信息,请首先考虑评估错误,并根据抛出的错误返回适当的内容。例如,应避免显示错误的堆栈,因为它包含有关服务器上代码结构的信息。

局部页面(未记录)

添加于: astro@3.4.0

路由

Astro 使用基于文件的路由,它根据项目 src/pages 目录中的文件结构来生成你的构建链接。

页面之间的导航

Astro 使用标准 HTML a 元素在路由之间导航。

src/pages/index.astro

html
<p>Read more <a href="/about/">about</a> Astro!</p>

<!-- 配置了 `base: "/docs"` -->
<p>Learn more in our <a href="/docs/reference/">reference</a> section!</p>

静态路由

src/pages 目录中的 .astro 页面组件以及 Markdown 和 MDX 文件(.md.mdx)将自动作为网站页面。每个页面的路由对应于它在 src/pages/ 目录中的路径和文件名。

js
# 示例:静态路由
src/pages/index.astro        -> mysite.com/
src/pages/about.astro        -> mysite.com/about
src/pages/about/index.astro  -> mysite.com/about
src/pages/about/me.astro     -> mysite.com/about/me
src/pages/posts/1.md         -> mysite.com/posts/1

Astro 项目没有单独的路由配置!当你在 src/pages 目录新增文件时,会自动生成一个新的路由。在静态生成中,你可以使用 build.format 配置项自定义文件输出格式。

动态路由

Astro 页面文件可以在其文件名中指定动态路由参数以生成多个匹配页面

例如,src/pages/authors/[author].astro 可以为你博客上的每一位作者生成一个简介页面,而 author 将作为一个你可以从页面内部访问的 参数

在 Astro 默认的静态输出模式下,这些页面会在构建时生成,因此你必须预先确定要生成页面的作者列表。 而在 SSR 模式 下,只要有请求匹配到对应路由,页面就会按需动态生成

静态 (SSG) 模式

因为必须在构建时确定所有路由,所以动态路由必须导出一个 getStaticPaths() ,它返回一个具有 params 属性的对象数组。数组中的每一个对象都会生成相应的路由。

[dog].astro 在其文件名中定义一个 dog 参数,则 getStaticPaths() 返回的这些对象的 params 中必须包含 dog 。 然后页面可以使用 Astro.params 访问该参数。

src/pages/dogs/[dog].astro

js
---
export function getStaticPaths() {
  return [
    { params: { dog: "clifford" }},
    { params: { dog: "rover" }},
    { params: { dog: "spot" }},
  ];
}

const { dog } = Astro.params;
---
<div>Good dog, {dog}!</div>

这将生成三个页面: /dogs/clifford/dogs/rover/dogs/spot ,每个页面显示相应的狗名。

文件名可以包含多个参数,这些参数必须都包含在 getStaticPaths()params 对象中:

src/pages/[lang]-[version]/info.astro

js
---
export function getStaticPaths() {
  return [
    { params: { lang: "en", version: "v1" }},
    { params: { lang: "fr", version: "v2" }},
  ];
}

const { lang, version } = Astro.params;
---
...

这将生成 /en-v1/info/fr-v2/info 路由。

参数也可以包含在路径的单独部分中。例如, src/pages/[lang]/[version]/info.astro 文件与上面相同的 getStaticPaths() 将生成路由 /en/v1/info/fr/v2/info

解码 params

提供给 getStaticPaths() 函数的 params 不会被解码。当你需要解码参数值时,请使用 decodeURI 函数。

src/pages/[slug].astro

js
---
export function getStaticPaths() {
  return [
    { params: { slug: decodeURI("%5Bpage%5D") }}, // 解码成 "[page]"
      //这一步只是为了演示:如果你的参数中出现了 URL 编码内容(例如中文、特殊符号),需要先解码。
      //构建时会生成一个静态页面: /[page]
  ]
}
---

了解更多关于 getStaticPaths() 的内容.

剩余参数

如果你的URL路由需要更加灵活,你可以在 .astro 文件名中使用一个 剩余参数[...path]),去匹配任何深度的文件路径

src/pages/sequences/[…path].astro

---
export function getStaticPaths() {
  return [
    { params: { path: "one/two/three" }},
    { params: { path: "four" }},
    { params: { path: undefined }}
  ]
}

const { path } = Astro.params;
---

这将生成 /sequences/one/two/three/sequences/four/sequences 。(将剩余参数设置为 undefined 允许它匹配顶级页面。)

剩余参数可以与 其他命名参数 一起使用。例如, GitHub 的文件查看器可以用以下动态路由表示:

/[org]/[repo]/tree/[branch]/[...file]

在这个示例中,对 /withastro/astro/tree/main/docs/public/favicon.svg 的请求将拆分为以下命名参数:

{
  org: "withastro",
  repo: "astro",
  branch: "main",
  file: "docs/public/favicon.svg"
}
示例:多级动态页面

在下面的示例中, 剩余参数([...slug])和 getStaticPaths()props 为不同深度的 slug 生成页面。

src/pages/[…slug].astro

js
---
export function getStaticPaths() {
  const pages = [
    {
      slug: undefined,
      title: "Astro Store",
      text: "Welcome to the Astro store!",
    },
    {
      slug: "products",
      title: "Astro products",
      text: "We have lots of products for you",
    },
    {
      slug: "products/astro-handbook",
      title: "The ultimate Astro handbook",
      text: "If you want to learn Astro, you must read this book.",
    },
  ];

  return pages.map(({ slug, title, text }) => {
    return {
      params: { slug },
      props: { title, text },
    };
  });
}

const { title, text } = Astro.props;
---
<html>
  <head>
    <title>{title}</title>
  </head>
  <body>
    <h1>{title}</h1>
    <p>{text}</p>
  </body>
</html>

按需动态路由

在使用适配器进行按需渲染时,动态路由的定义方式相同:在文件名中包含 [param][...path] ,用以匹配任意字符串或路径。 但由于这些路由不再在构建时提前生成,页面会对任何匹配的路由请求进行响应。 由于这些不是“静态”路由,因此不应使用 getStaticPaths

对于按需渲染的路由,文件名中只能使用一个使用展开语法的剩余参数。 例如:

js
src/pages/[locale]/[...slug].astro
src/pages/[...locale]/[slug].astro

是允许的; 但以下写法不允许:

js
src/pages/[...locale]/[...slug].astro

src/pages/resources/[resource]/[id].astro

js
---
export const prerender = false; // 'server' 模式下,无需配置
const { resource, id } = Astro.params;
---
<h1>{resource}: {id}</h1>
修改 [...slug] 示例用于SSR

由于 SSR 页面无法使用 getStaticPaths(),因此无法接收 props。可以通过在对象中查找 slug 参数的值,将 前面的示例 调整为 SSR 模式。如果路由位于根目录(“/”),则 slug 参数为 undefined。如果对象中不存在该值,将重定向到 404 页面。

src/pages/[…slug].astro

js
---
const pages = [
  {
    slug: undefined,
    title: 'Astro Store',
    text: 'Welcome to the Astro store!',
  },
  {
    slug: 'products',
    title: 'Astro products',
    text: 'We have lots of products for you',
  },
  {
    slug: 'products/astro-handbook',
    title: 'The ultimate Astro handbook',
    text: 'If you want to learn Astro, you must read this book.',
  }
];

const { slug } = Astro.params;
const page = pages.find((page) => page.slug === slug);
if (!page) return Astro.redirect("/404");
const { title, text } = page;
---
<html>
  <head>
    <title>{title}</title>
  </head>
  <body>
    <h1>{title}</h1>
    <p>{text}</p>
  </body>
</html>

重定向

有时你需要将读者重定向到一个新页面,这可能是因为网站结构发生了变化(永久重定向),也可能是出于某种操作(例如登录后进入受保护的页面)。

备注

你可以在你的 Astro 配置中定义规则来将用户 永久重定向到已移动的页面。或者,根据用户的使用情况,动态地将用户重定向

配置重定向

添加于: astro@2.9.0

在你的 Astro 配置中,你可以使用 redirects 值来指定永久重定向的映射关系。

对于内部重定向,这是旧路由路径到新路由的映射。从 Astro v5.2.0 开始,还可以重定向到以 httphttps 开头的外部 URL,并且可以被解析:

astro.config.mjs

js
import { defineConfig } from "astro/config";

export default defineConfig({
  redirects: {
    "/old-page": "/new-page",
    "/blog": "https://example.com/blog"
  }
});

这些重定向遵循与基于文件路由相同的优先级规则,并且优先级将会始终低于现有项目中同名的页面文件。例如:如果你的项目包含 src/pages/old-page.astro 文件,那么 /old-page 将不会重定向到 /new-page

只要新旧路由都包含相同的参数,就可以使用动态路由,例如:

json
{
  "/blog/[...slug]": "/articles/[...slug]"
}

使用 SSR(服务器端渲染)或者静态适配器,你还可以将对象作为值提供,除了新的 destination 端点指向之外,还可以指定 status 状态码:

astro.config.mjs

js
import { defineConfig } from "astro/config";

export default defineConfig({
  redirects: {
    "/old-page": {
      status: 302,
      destination: "/new-page"
    },
    "/news": {
      status: 302,
      destination: "https://example.com/news"
    }
  }
});

在运行 astro build 时,默认情况下,Astro 将输出带有 meta refresh 标签的 HTML 文件。支持的适配器将会使用托管服务器的配置文件写入重定向信息。

状态码默认为 301。如果构建为 HTML 文件,则服务器不会使用状态码。

动态重定向

在全局的 Astro 对象上,Astro.redirect 方法允许你动态地重定向到另一个页面。例如,你可以在检查用户是否已登录(通过从 Cookie 中获取其会话)之后执行此操作。

src/pages/account.astro

js
---
import { isLoggedIn } from "../utils";

const cookie = Astro.request.headers.get("cookie");

// 如果用户未登录,将其重定向到登录页面。
if (!isLoggedIn(cookie)) {
  return Astro.redirect("/login");
}
---

重要

由于 Astro 在按需渲染中使用 HTML 流式传输,重定向必须在页面级别完成,而不是在子组件中完成。

重写

添加于: astro@4.13.0

重写允许你提供不同的路由,而无需将浏览器重定向到不同的页面。浏览器将在 URL 栏中显示原始地址,但实际上会显示 Astro.rewrite() 提供的 URL 的内容。

对于永久性移动的内容,或者将用户重定向到具有新 URL 的不同页面(例如登录后的用户仪表盘),请使用 重定向

重写对于在不同路径(例如 /products/shoes/men//products/men/shoes/)显示相同内容非常有用,而无需维护两个不同的源文件。

重写对于用户体验和 SEO 也非常有用。它允许你显示需要将访问者重定向到其他页面或返回 404 状态的内容。 重写的一个常见用途是为语言的不同变体显示相同的本地化内容。

以下示例使用重写来在访问 /es-CU/(古巴西班牙语)URL 路径时渲染 /es/ 版本的页面。当访问者导航到 /es-cu/articles/introduction URL 时,Astro 将渲染由文件 src/pages/es/articles/introduction.astro 生成的内容。

src/pages/es-cu/articles/introduction.astro

js
---
return Astro.rewrite("/es/articles/introduction");
---

使用 context.rewrite() 在你的端点文件中重定向到不同的页面:

src/pages/api.js

js
export function GET(context) {
  if (!context.locals.allowed) {
    return context.rewrite("/");
  }
}

如果传递给 Astro.rewrite() 的 URL 引发运行时错误,Astro 将在开发中显示错误覆盖层,并在生产中返回 500 状态码。如果你的项目中不存在该 URL,则将返回 404 状态码。

你可以有意创建一个重写来渲染你的 /404 页面,例如,以指示你的电子商店中的产品不再可用:

src/pages/[item].astro

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

你也可以根据 HTTP 响应状态有条件地重写,例如在访问不存在的 URL 时显示站点上的某个页面:

src/middleware.mjs

js
export const onRequest = async (context, next) => {
  const response = await next();
  if (response.status === 404) {
    return context.rewrite("/");
  }
  return response;
}

显示指定重写路径的内容之前,Astro.rewrite() 函数将触发一个新的完整渲染阶段。这将为新的路由/请求重新执行任何中间件。

有关更多信息,请查阅 Astro.rewrite() API 参考

路由优先级顺序

可能有多个已定义的路由试图构建相同的 URL 路径。例如,下面所有这些路由都有可能构建 /posts/create

image-20251012214922181

Astro 需要知道哪个路由应该被用来构建页面。为此,它会按照以下规则按顺序对它们进行排序:

  • Astro 预留路由
  • 路径段更多的路由将优先于不太具体的路由。在上面的例子中,所有在 /posts/ 下的路由优先于根目录的 /[...slug].astro
  • 没有路径参数的静态路由将优先于动态路由。例如,/posts/create.astro 优先于示例中的所有其他路由。
  • 使用命名参数的动态路由优先于剩余参数。例如,/posts/[page].astro 优先于 /posts/[...slug].astro
  • 预渲染的动态路由优先于服务器动态路由。
  • 端点优先于页面。
  • 基于文件的路由优先于重定向。
  • 如果以上规则都无法决定顺序,路由将根据你的 Node 安装的默认语言环境按字母顺序排序。

鉴于上面的示例,下面用几个例子,说明这些规则如何匹配请求的 URL 与用于建立 HTML 的路由。

  • pages/posts/create.astro - 将只构建 /posts/create
  • pages/posts/[pid].ts - 将构建 /posts/abc/posts/xyz 等,但不包括 /posts/create
  • pages/posts/[page].astro - 将构建 /posts/1/posts/2 等,但不包括 /posts/create/posts/abc 以及 /posts/xyz
  • pages/posts/[...slug].astro - 将构建 /posts/1/2/posts/a/b/c 等,但不包括 /posts/create/posts/1/posts/abc
  • pages/[...slug].astro - 将构建 /abc/xyz/abc/xyz 等,但不包括 /posts/create/posts/1/posts/abc

预留路由

内部路由优先于任何用户定义或集成定义的路由,因为 Astro 功能需要它们才能工作。以下是 Astro 的预留路由:

  • _astro/: 向客户端提供所有静态资源,包括 CSS 文档、打包的客户端脚本、优化后的图像和任何 Vite 资源。
  • _server_islands/: 为延迟渲染的 服务器群岛 动态组件提供服务。
  • _actions/: 服务于任何已定义的 action

分页

Astro 支持内置分页,用于需要分割成多个页面的大量数据。Astro 会生成常见的分页属性,包括上一页/下一页链接、总页数等。

分页的路由名应该使用与标准动态路由一样的 [bracket] 语法。例如,文件名 /astronauts/[page].astro 将生成 /astronauts/1/astronauts/2 等路由,其中 [page] 是生成的页码。

你可以用 paginate() 函数根据数组值生成这些页面:

src/pages/astronauts/[page].astro

js
---
export function getStaticPaths({ paginate }) {
  const astronautPages = [
    { astronaut: "Neil Armstrong" },
    { astronaut: "Buzz Aldrin" },
    { astronaut: "Sally Ride" },
    { astronaut: "John Glenn" },
  ];

  // 根据宇航员数组生成页面,每页2项
  return paginate(astronautPages, { pageSize: 2 });
}
// 所有分页数据都在 "page" 参数中传递
const { page } = Astro.props;
---
<!-- 显示当前页面。也可以使用 `Astro.params.page`-->
<h1>Page {page.currentPage}</h1>
<ul>
  <!-- 列出宇航员信息数组 -->
  {page.data.map(({ astronaut }) => <li>{astronaut}</li>)}
</ul>

将生成以下两个页面,每页 2 项:

  • /astronauts/1 - 第一页显示“Neil Armstrong”和“Buzz Aldrin”
  • /astronauts/2 - 第二页显示“Sally Ride”和“John Glenn”

page 属性

当你使用 paginate() 函数时,每个页面将通过 page 传递数据。page 有很多有用的属性,你可以使用它们来构建页面以及页面之间的链接:

ts
interface Page<T = any> {
  /** 数组,包含了传递给 paginate() 函数的页面数据片段 */
  data: T[];
  /** 元数据 */
  /** 页面第一项的计数,从 0 开始。 */
  start: number;
  /** 页面最后一项的计数,从 0 开始 */
  end: number;
  /** 结果总计数 */
  total: number;
  /** 当前页码, 从 1 开始 */
  currentPage: number;
  /** 每个页面的项数(默认为 10) */
  size: number;
  /** 最后一页的序号 */
  lastPage: number;
  url: {
    /** 当前页面链接 */
    current: string;
    /** 前一页的链接(如果有) */
    prev: string | undefined;
    /** 下一页的链接(如果有) */
    next: string | undefined;
    /** 第一页的链接 (如果当前页面不是第一页) */
    first: string | undefined;
    /** 最后一页的链接 (如果当前页面不是最后一页) */
    last: string | undefined;
  };
}

以下示例,展示了当前页面相对于其他相关页面的导航链接:

src/pages/astronauts/[page].astro

js
---
// 将与上一个示例相同的 `{ astronaut }` 对象列表进行分页
export function getStaticPaths({ paginate }) { /* ... */ }
const { page } = Astro.props;
---
<h1>Page {page.currentPage}</h1>
<ul>
  {page.data.map(({ astronaut }) => <li>{astronaut}</li>)}
</ul>
{page.url.first ? <a href={page.url.first}>First</a> : null}
{page.url.prev ? <a href={page.url.prev}>Previous</a> : null}
{page.url.next ? <a href={page.url.next}>Next</a> : null}
{page.url.last ? <a href={page.url.last}>Last</a> : null}

了解更多关于 page 分页参数 的内容。

嵌套分页

分页的一个更高级的用例是嵌套分页。当分页与其他动态路由参数相结合时,你可以使用嵌套式分页和一些属性或标签来将分页进行分类。

例如,如果你想通过一些标签来分组你的分页的 Markdown 帖子,你可以通过创建一个 /src/pages/[tag]/[page].astro 的页面来使用嵌套分页,该页面将匹配以下链接。

  • /red/1 (tag=red)
  • /red/2 (tag=red)
  • /blue/1 (tag=blue)
  • /green/1 (tag=green)

嵌套分页的工作原理是使用 getStaticPaths() 返回每个分组的 paginate() 结果数组。

在下面的例子中,我们将实现嵌套分页来建立上面列出的URL。

src/pages/[tag]/[page].astro

js
---
export function getStaticPaths({paginate}) {
  const allTags = ["red", "blue", "green"];
  const allPosts = Object.values(import.meta.glob("../pages/post/*.md", { eager: true }));
  // 每个标签都会返回 `paginate()` 的结果。
  // 确保将 `{ params: { tag }}` 传递给 `paginate()`
  // 这样 Astro 才知道怎么把这些结果进行分组
  return allTags.flatMap((tag) => {
    const filteredPosts = allPosts.filter((post) => post.frontmatter.tag === tag);
    return paginate(filteredPosts, {
      params: { tag },
      pageSize: 10
    });
  });
}

const { page } = Astro.props;
const params = Astro.params;

排除页面

你可以通过在文件名前加上下划线(_)来排除 src/pages 中的页面或目录的构建。带有 _ 前缀的文件不会被路由识别,也不会被放入 dist/ 目录。

你可以使用它来暂时禁用页面,并将测试、工具函数和组件放在与其相关页面同一个文件夹中。

在这个例子中,只有 src/pages/index.astrosrc/pages/projects/project1.md 将被构建为页面路由和 HTML 文件。

image-20251012215042779

端点(Endpoints)

Astro 允许你创建自定义端点,用于提供任意类型的数据。你可以用它来生成图片、公开 RSS 文档,或者将其作为API 路由,为你的网站构建完整的 API。

在静态生成(SSG)的网站中,自定义端点会在构建时被调用,用于生成静态文件。如果你启用了 SSR(服务端渲染)模式,自定义端点则会变成实时服务器端点,在每次请求时被调用。静态端点与 SSR 端点的定义方式类似,但 SSR 端点支持更多特性。

静态文件端点(Static File Endpoints)

要创建一个自定义端点,只需在 /pages 目录中添加一个 .js.ts 文件。 在构建过程中,.js.ts 扩展名会被移除,因此文件名中应包含你想生成的数据类型的扩展名。例如,src/pages/data.json.ts 将会生成一个 /data.json 端点。

端点需要导出一个 GET 函数(可以是异步的 async),该函数会接收一个上下文对象(context object),其属性与 Astro 全局对象类似。 在这里,它返回一个包含 nameurlResponse 对象。Astro 会在构建时调用它,并使用响应体(body)的内容生成对应的文件。

src/pages/builtwith.json.ts

ts
// 输出:/builtwith.json
export function GET({ params, request }) {
  return new Response(
    JSON.stringify({
      name: "Astro",
      url: "https://astro.build/",
    }),
  );
}

访问结果:

image-20251013103435736

如果不使用JSON.stringify,就会输出:

image-20251013103537148

从 Astro v3.0 开始,返回的 Response 对象不再需要包含 encoding 属性。例如,生成一个二进制 png 图像:

src/pages/astro-logo.png.ts

ts
export async function GET({ params, request }) {
  const response = await fetch(
    "https://docs.astro.build/assets/full-logo-light.png",
  );

  return new Response(await response.arrayBuffer());
}

你还可以使用 APIRoute 类型来约束 API 端点函数:

ts
import type { APIRoute } from "astro";

export const GET: APIRoute = async ({ params, request }) => {...}

params 和动态路由

端点支持与页面相同的 动态路由 功能。使用中括号包裹参数作为文件名,并导出 getStaticPaths() 函数。然后,你可以使用传递给 API 端点函数的 params 属性访问参数:

src/pages/api/[id].json.ts

js
import type { APIRoute } from "astro";

const usernames = ["张三", "李四", "王五", "赵六"];

export const GET: APIRoute = ({ params, request }) => {
  const id = params.id;

  return new Response(
    JSON.stringify({
      name: usernames[id],
    }),
  );
};

export function getStaticPaths() {
  return [
    { params: { id: "0" } },
    { params: { id: "1" } },
    { params: { id: "2" } },
    { params: { id: "3" } },
  ];
}

这将在构建时生成四个 JSON 端点:/api/0.json/api/1.json/api/2.json/api/3.json。带端点的动态路由与页面的工作方式相同,但由于端点是一个函数而不是组件,因此 props 是不被支持的。

request

所有端点都会接收 request 属性,但在静态模式下,你只能访问 request.url。它将返回当前端点的完整 URL,其工作方式与 Astro.request.url 对页面的作用相同。

src/pages/request-path.json.ts

js
import type { APIRoute } from "astro";

export const GET: APIRoute = ({ params, request }) => {
  return new Response(
    JSON.stringify({
      path: new URL(request.url).pathname,
    }),
  );
};

服务器端点(API 路由)

静态文件端点部分中描述的所有内容,同样适用于 SSR 模式:文件可以导出一个 GET 函数,该函数会接收一个上下文对象(context object),其属性与全局 Astro 对象类似。

但与静态模式不同的是,当你为某个路由启用按需渲染时,该端点会在被请求时才构建。这解锁了构建时无法使用的新特性,使你能够创建在运行时监听请求、并在服务器上安全执行代码的 API 路由。

服务器模式(server mode) 下,路由默认会按需渲染。 而在 静态模式(static mode) 下,你需要为每个自定义端点显式关闭预渲染功能:

js
export const prerender = false;

注意

在尝试这些示例之前,请务必确认已经 启用了按需渲染模式 并且避免了 static 模式下的预渲染。

服务器端点可以在不导出 getStaticPaths 的情况下访问 params。服务器端点可以返回一个 Response 对象,其允许你为返回的数据设置状态码和标头:

src/pages/[id].json.js

js
import { getProduct } from "../db";

export async function GET({ params }) {
  const id = params.id;
  const product = await getProduct(id);

  if (!product) {
    return new Response(null, {
      status: 404,
      statusText: "Not found",
    });
  }

  return new Response(JSON.stringify(product), {
    status: 200,
    headers: {
      "Content-Type": "application/json",
    },
  });
}

这将响应与动态路由匹配的任何请求。例如,如果我们导航到 /helmet.jsonparams.id 将被设置为 helmet。如果我们模拟的数据库中存在 helmet,端点将会使用 Response 对象(JSON 格式)来响应,并返回成功的 HTTP 状态码。如果没有,它将使用一个 Response 对象返回 404 响应。

在 SSR 模式下,需要提供 Content-Type 头来返回图像。在这种情况下,使用 Response 对象来指定 headers 属性。例如,生成一个二进制 .png 图像:

src/pages/astro-logo.png.ts

js
export async function GET({ params, request }) {
  const response = await fetch(
    "https://docs.astro.build/assets/full-logo-light.png",
  );
  const buffer = Buffer.from(await response.arrayBuffer());

  return new Response(buffer, {
    headers: { "Content-Type": "image/png" },
  });
}

HTTP 方法

除了 GET 函数,你还可以使用任何 HTTP 方法 作为名称导出函数。当有请求进来时,Astro 会检查该方法并调用相应的函数。

你还可以导出 ALL 函数以匹配任何没有相应导出函数的方法。如果请求方法不匹配,它将重定向到你的网站的 404 页面

src/pages/methods.json.ts

ts
export const GET: APIRoute = ({ params, request }) => {
  return new Response(
    JSON.stringify({
      message: "这是个 GET 请求!",
    }),
  );
};

export const POST: APIRoute = ({ request }) => {
  return new Response(
    JSON.stringify({
      message: "这是个 POST 请求!",
    }),
  );
};

export const DELETE: APIRoute = ({ request }) => {
  return new Response(
    JSON.stringify({
      message: "这是个 DELETE 请求!",
    }),
  );
};

export const ALL: APIRoute = ({ request }) => {
  return new Response(
    JSON.stringify({
      message: `这是个 ${request.method} 请求!`,
    }),
  );
};

如果你定义了一个 GET 函数,但没有定义 HEAD 函数,Astro 会自动处理 HEAD 请求:它会调用你的 GET 函数,然后从响应中移除响应体(body)。

HEAD 请求是一种 HTTP 请求方法,和 GET 请求非常相似,但有一个关键区别:

HEAD 请求只会请求响应的“头部信息(headers)”,不会返回正文内容(body)。

HEAD 通常用于:

  1. 检查资源是否存在(不下载整个内容)
    • 比如判断一个文件、页面或图片是否有效。
  2. 获取元信息(如文件大小、最后修改时间、内容类型等)
    • 适合做缓存验证或下载前的检查。
  3. 测试接口连通性
    • 比如探测一个 API 是否在线。

img相关操作指南

request

在 SSR 模式下,request 属性返回一个可用的 Request 对象,该对象引用当前请求。这允许你接收数据并检查 headers

src/pages/test-post.json.ts

ts
export const POST: APIRoute = async ({ request }) => {
  if (request.headers.get("Content-Type") === "application/json") {
    const body = await request.json();
    const name = body.name;

    return new Response(
      JSON.stringify({
        message: "Your name was: " + name,
      }),
      {
        status: 200,
      },
    );
  }

  return new Response(null, { status: 400 });
};

重定向

端点上下文导出一个类似于 Astro.redirectredirect() 函数:

src/pages/links/[id].js

js
import { getLinkUrl } from "../db";

export async function GET({ params }) {
  const { id } = params;
  const link = await getLinkUrl(id);

  if (!link) {
    return new Response(null, {
      status: 404,
      statusText: "Not found"
    });
  }

  return Response.redirect(link, 307);
}

中间件

中间件允许你拦截请求和响应,并在即将渲染页面或端点时动态注入行为。对于所有预渲染的页面,这种渲染发生在构建时,但对于按需渲染的页面,这种渲染发生在请求路由时,这时可以使用 额外的 SSR 功能如 cookie 和标头

中间件(Middleware)还允许你通过修改一个名为 locals 的对象,在所有端点和页面之间设置并共享与当前请求相关的信息。该对象在所有 Astro 组件和 API 端点中都可用,即使中间件在构建时运行,它也同样可被访问。

基本用法

  1. 创建 src/middleware.js|ts(或者,你也可以创建 src/middleware/index.js|ts

  2. 在这个文件中,导出一个接收 context 对象的 onRequest() 函数。注意这里不能是默认导出

src/middleware.js

js
export function onRequest (context, next) {
    // 拦截一个请求里的数据
    // 可选地修改 `locals` 中的属性
    context.locals.title = "New title";

    // 返回一个 Response 或者调用 `next()` 的结果
    return next();
};
  1. 在任何 .astro 文件中,使用 Astro.locals 访问响应数据。

src/components/Component.astro

js
---
const data = Astro.locals;
---
<h1>{data.title}</h1>
<p>这个 {data.property} 来自中间件。</p>

context 对象

context 对象包含需要对其他中间件、API 路由和 .astro 路由在渲染过程中可用的信息。

它是一个传入 onRequest() 的可选参数,可能包含 locals 对象以及其他在渲染期间共享的任何属性。例如,context 对象可能包含用于认证的 cookies。

context.locals 中存储数据

context.locals 是一个可以在中间件中操作、包含来自 Response 的数据的对象。

这个 locals 对象在请求处理过程中被传递,并作为 APIContextAstroGlobal 的属性可用。这使得中间件,API 路由和 .astro 页面之间可以共享数据。这对于在渲染步骤中存储请求特定的数据(例如用户数据)很有用。

集成属性

集成可能会在 locals 对象中设置属性和提供一些功能。如果你在使用一个集成,检查它的文档并确认你没有覆盖掉它的属性或者在做重复的工作。

你可以在 locals 中存储任何类型的数据:字符串,数字,甚至复杂的数据类型,如函数和映射。

src/middleware.js

js
export function onRequest (context, next) {
    // 拦截一个请求里的数据
    // 可选地修改 `locals` 中的属性
    context.locals.user.name = "John Wick";
    context.locals.welcomeTitle = () => {
        return "Welcome back " + locals.user.name;
    };
    // 返回一个 Response 或者调用 `next()` 的结果
    return next();
};

然后你可以在任何 .astro 文件中通过 Astro.locals 使用这个信息。

src/pages/orders.astro

js
---
const title = Astro.locals.welcomeTitle();
const orders = Array.from(Astro.locals.orders.entries());
const data = Astro.locals;
---
<h1>{title}</h1>
<p>这个 {data.property} 来自中间件。</p>
<ul>
    {orders.map(order => {
        return <li>{/* do something with each order */}</li>;
    })}
</ul>

locals 是一个在单个 Astro 路由中创建和销毁的对象;当你的路由页面渲染完后,locals 将不再存在,并会创建一个新的。需要在多个页面请求之间保持的信息必须存储在其他地方。

备注

locals 的值不能在运行时被覆盖。这样做会有清除用户存储的所有信息的风险。Astro 会进行检查,如果 locals 被覆盖将会抛出错误。

示例:删除敏感信息

下面的示例使用中间件将”私人信息”替换为”已删除”,以便你在页面上渲染修改后的 HTML:

src/middleware.js

js
export const onRequest = async (context, next) => {
    const response = await next();
    const html = await response.text();
    const redactedHtml = html.replaceAll("私人信息", "已删除");

    return new Response(redactedHtml, {
        status: 200,
        headers: response.headers
    });
};

中间件类型

你可以导入并使用 defineMiddleware() 实用函数来提供类型安全:

src/middleware.ts

ts
import { defineMiddleware } from "astro:middleware";

// `context` 和 `next` 会自动被类型化
export const onRequest = defineMiddleware((context, next) => {

});

或者,如果你使用 JsDoc 来提供类型安全,你可以使用 MiddlewareHandler

src/middleware.js

ts
/**
 * @type {import("astro").MiddlewareHandler}
 */
// `context` 和 `next` 会自动被类型化
export const onRequest = (context, next) => {

};

要给 Astro.locals 内的信息定义类型,也就是在 .astro 文件和中间件代码中能提供自动补全,通过在 env.d.ts 文件中声明一个全局命名空间来 扩展全局类型

src/env.d.ts

ts
type User = {
  id: number;
  name: string;
};

declare namespace App {
  interface Locals {
    user: User;
    welcomeTitle: () => string;
    orders: Map<string, object>;
    session: import("./lib/server/session").Session | null;
  }
}

然后,在中间件中,你就可以使用自动补全和类型安全了。

中间件链式调用

可以使用 sequence() 按指定顺序连接多个中间件:

src/middleware.js

js
import { sequence } from "astro:middleware";

async function validation(_, next) {
    console.log("验证请求");
    const response = await next();
    console.log("验证响应");
    return response;
}

async function auth(_, next) {
    console.log("授权请求");
    const response = await next();
    console.log("授权响应");
    return response;
}

async function greeting(_, next) {
    console.log("问候请求");
    const response = await next();
    console.log("问候响应");
    return response;
}

export const onRequest = sequence(validation, auth, greeting);

控制台打印结果顺序如下:

image-20251013094302876

重写

添加于: astro@4.13.0

APIContext 中暴露了一个名为 rewrite() 的方法,它的工作方式与 Astro.rewrite 相同。

使用 context.rewrite() 在中间件中显示不同页面的内容,而不会将访问者 重定向 到新页面。这将触发一个新的渲染阶段,导致任何中间件被重新执行。

src/middleware.js

js
import { isLoggedIn } from "~/auth.js"
export function onRequest (context, next) {
  if (!isLoggedIn(context)) {
    // 如果用户未登录,则更新请求来渲染 `/login` 路由,
    // 并添加标头以指示在成功登录应该把用户重定向到何处。
    // 重新执行中间件。
    return context.rewrite(new Request("/login", {
      headers: {
        "x-redirect-to": context.url.pathname
      }
    }));
  }

  return next();
};

你也可以向 next() 函数传递一个可选的 URL 路径参数,以重写当前的 Request不重新触发新的渲染阶段(绕过中间件)。重写路径的位置可以作为字符串、URL 或 Request 提供:

src/middleware.js

js
import { isLoggedIn } from "~/auth.js"
export function onRequest (context, next) {
  if (!isLoggedIn(context)) {
    // 如果用户未登录,则更新请求来渲染 `/login` 路由,
    // 并添加标头以指示在成功登录应该把用户重定向到何处。
    // 将新的 `context` 返回给任何后续中间件。
    return next(new Request("/login", {
      headers: {
        "x-redirect-to": context.url.pathname
      }
    }));
  }

  return next();
};

next() 函数接受与 Astro.rewrite() 相同的参数。重写路径的位置可以通过字符串、URLRequest 提供。

当你使用 sequence() 将多个中间件函数串联时,如果在 next() 中传入了一个路径参数,请求将会被就地重写(rewrite),并且中间件链不会重新执行。下一个中间件函数将接收到这个带有更新上下文的新请求(Request)

使用这种方式调用 next() 时,会基于旧的 ctx.request 创建一个新的 Request 对象。这意味着如果你在重写前或重写后尝试读取 Request.body,会抛出运行时错误。

这种错误常见于使用 HTML 表单的 Astro Actions 中。在这些情况下,建议在 Astro 模板中使用 Astro.rewrite() 来处理重写逻辑,而不要在中间件中使用。

src/middleware.js

js
// 当前 URL 是 https://example.com/blog

// 第一个中间件函数
async function first(context, next) {
  console.log(context.url.pathname) // 这里会打印 "/blog"
  // 重写到一个新的路由,首页
  // 返回更新的 `context`,传递给下一个函数
  return next("/")
}

// 当前 URL 仍然是 https://example.com/blog

// 第二个中间件函数
async function second(context, next) {
  // 接收更新后的 `context`
  console.log(context.url.pathname) // 这里会打印  "/"
  return next()
}

export const onRequest = sequence(first, second);

错误页面

即使找不到匹配的路由,中间件也会尝试为所有按需渲染的页面运行。这包括 Astro 的默认(空白)404 页面和任何自定义 404 页面。然而,是否运行该代码取决于适配器。一些适配器可能会提供特定平台的错误页面。

在提供 500 错误页面之前,中间件也会尝试运行,包括自定义的 500 页面,除非服务器错误发生在中间件本身的执行中。如果你的中间件没有成功运行,那么你将无法访问 Astro.locals 来渲染你的 500 页面。

APIAPI

https://docs.astro.build/zh-cn/reference/api-reference/#redirect

评论

评论加载中……