跳转到内容

中间件

中间件允许你拦截请求和响应,并在每次要渲染页面或端点时动态注入行为。对于所有预渲染的页面,这个渲染过程发生在构建时,但对于按需渲染的页面,它发生在请求路由时,这使得像 cookie 和请求头等额外的 SSR 功能可用。

中间件还允许你通过修改 locals 对象来设置和共享请求特定的信息,该对象在所有 Astro 组件和 API 端点中都可用。即使此中间件在构建时运行,该对象也可用。

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

  2. 在此文件中,导出一个 onRequest() 函数,该函数可以接收一个context 对象next() 函数。这不能是默认导出。

    src/middleware.js
    export function onRequest (context, next) {
    // intercept data from a request
    // optionally, modify the properties in `locals`
    context.locals.title = "New title";
    // return a Response or the result of calling `next()`
    return next();
    };
  3. 在任何 .astro 文件中,使用 Astro.locals 访问响应数据。

    src/components/Component.astro
    ---
    const data = Astro.locals;
    ---
    <h1>{data.title}</h1>
    <p>This {data.property} is from middleware.</p>

context 对象包含在渲染过程中提供给其他中间件、API 路由和 .astro 路由的信息。

这是传递给 onRequest() 的一个可选参数,其中可能包含 locals 对象以及在渲染期间共享的任何其他属性。例如,context 对象可能包含用于身份验证的 cookie。

context.locals 是一个可以在中间件内部操作的对象。

这个 locals 对象在请求处理过程中被转发,并作为 APIContextAstroGlobal 的一个属性可用。这允许在中间件、API 路由和 .astro 页面之间共享数据。这对于在整个渲染步骤中存储请求特定的数据(例如用户数据)非常有用。

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

src/middleware.js
export function onRequest (context, next) {
// intercept data from a request
// optionally, modify the properties in `locals`
context.locals.user.name = "John Wick";
context.locals.welcomeTitle = () => {
return "Welcome back " + locals.user.name;
};
// return a Response or the result of calling `next()`
return next();
};

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

src/pages/orders.astro
---
const title = Astro.locals.welcomeTitle();
const orders = Array.from(Astro.locals.orders.entries());
const data = Astro.locals;
---
<h1>{title}</h1>
<p>This {data.property} is from middleware.</p>
<ul>
{orders.map(order => {
return <li>{/* do something with each order */}</li>;
})}
</ul>

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

下面的示例使用中间件将“PRIVATE INFO”替换为“REDACTED”,以允许你在页面上渲染修改后的 HTML。

src/middleware.js
export const onRequest = async (context, next) => {
const response = await next();
const html = await response.text();
const redactedHtml = html.replaceAll("PRIVATE INFO", "REDACTED");
return new Response(redactedHtml, {
status: 200,
headers: response.headers
});
};

你可以导入并使用工具函数 defineMiddleware() 来利用类型安全。

src/middleware.ts
import { defineMiddleware } from "astro:middleware";
// `context` and `next` are automatically typed
export const onRequest = defineMiddleware((context, next) => {
});

或者,如果你使用 JSDoc 来利用类型安全,你可以使用 MiddlewareHandler

src/middleware.js
/**
* @type {import("astro").MiddlewareHandler}
*/
// `context` and `next` are automatically typed
export const onRequest = (context, next) => {
};

要为 Astro.locals 中的信息添加类型,以便在 .astro 文件和中间件代码中获得自动补全功能,请在 env.d.ts 文件中声明一个全局命名空间。

src/env.d.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
import { sequence } from "astro:middleware";
async function validation(_, next) {
console.log("validation request");
const response = await next();
console.log("validation response");
return response;
}
async function auth(_, next) {
console.log("auth request");
const response = await next();
console.log("auth response");
return response;
}
async function greeting(_, next) {
console.log("greeting request");
const response = await next();
console.log("greeting response");
return response;
}
export const onRequest = sequence(validation, auth, greeting);

这将导致以下控制台输出顺序:

终端窗口
validation request
auth request
greeting request
greeting response
auth response
validation response

新增于: astro@4.13.0

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

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

src/middleware.js
import { isLoggedIn } from "~/auth.js"
export function onRequest (context, next) {
if (!isLoggedIn(context)) {
// If the user is not logged in, update the Request to render the `/login` route and
// add header to indicate where the user should be sent after a successful login.
// Re-execute middleware.
return context.rewrite(new Request("/login", {
headers: {
"x-redirect-to": context.url.pathname
}
}));
}
return next();
};

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

src/middleware.js
import { isLoggedIn } from "~/auth.js"
export function onRequest (context, next) {
if (!isLoggedIn(context)) {
// If the user is not logged in, update the Request to render the `/login` route and
// add header to indicate where the user should be sent after a successful login.
// Return a new `context` to any following middlewares.
return next(new Request("/login", {
headers: {
"x-redirect-to": context.url.pathname
}
}));
}
return next();
};

next() 函数接受与Astro.rewrite() 函数相同的负载。重写路径的位置可以作为字符串、URL 或 Request 提供。

当你通过 sequence() 链接了多个中间件函数时,向 next() 提交一个路径将就地重写 Request,并且中间件不会再次执行。链中的下一个中间件函数将接收到带有更新后 context 的新 Request

使用此签名调用 next() 将使用旧的 ctx.request 创建一个新的 Request 对象。这意味着,无论是在重写之前还是之后,尝试消费 Request.body 都会抛出一个运行时错误。这个错误经常在使用 HTML 表单的 Astro Actions 中出现。在这些情况下,我们建议使用 Astro.rewrite() 从你的 Astro 模板中处理重写,而不是使用中间件。

src/middleware.js
// Current URL is https://example.com/blog
// First middleware function
async function first(_, next) {
console.log(context.url.pathname) // this will log "/blog"
// Rewrite to a new route, the homepage
// Return updated `context` which is passed to next function
return next("/")
}
// Current URL is still https://example.com/blog
// Second middleware function
async function second(context, next) {
// Receives updated `context`
console.log(context.url.pathname) // this will log "/"
return next()
}
export const onRequest = sequence(first, second);

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

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

贡献 社区 赞助