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.astro 或 404.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 开始,还可以重定向到以 http 或 https 开头的外部 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.astro 和 src/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 全局对象类似。 在这里,它返回一个包含 name 和 url 的 Response 对象。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.json,params.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.redirect 的 redirect() 函数:

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 对象在请求处理过程中被传递,并作为 APIContext 和 AstroGlobal 的属性可用。这使得中间件,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() 相同的参数。重写路径的位置可以通过字符串、URL 或 Request 提供。

当你使用 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

评论

评论区加载中

正在获取评论,请稍候。

登录