首页 / Astro 教程 / 中间件(Middleware)

Astro 教程

中间件(Middleware)

本教程共 56 篇 · 第 37 篇 · 更新于 2026-08-07 · 约 9 分钟阅读

AstroAstro 教程中间件Middlewarelocalssequence

本节目标:学会用中间件在每次页面或端点渲染前后统一”插手”,并借助 locals 在多处共享请求级数据。

有些逻辑你希望”每个页面都先跑一遍”:比如检查用户有没有登录、给页面统一加个标题、统计每次请求花了多久。**中间件(Middleware)**就是干这个的——它在每次页面或端点即将渲染时拦截下来,先执行你的代码,再继续往下走。

中间件长什么样

src/ 下建一个 middleware.js(或 middleware.ts,也能用 src/middleware/index.js)。在里面导出一个 onRequest() 函数——注意必须是命名导出,不能是默认导出。

// src/middleware.js
export function onRequest (context, next) {
  // 拦截请求里的数据
  // 可选地修改 context.locals 上的属性
  context.locals.title = "New title";
  context.locals.property = "information";

  // 返回 Response,或调用 next() 继续
  return next();
}

它接收两个参数:

  • context:上下文对象,包含这次请求的相关信息(比如 cookies、locals)。
  • next:一个函数,调用它就”放行”,让渲染继续;不调用就停在这里。

在任意 .astro 文件里,你可以通过 Astro.locals 拿到中间件放进去的数据:

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

中间件对预渲染页面是在构建时跑的;对按需渲染的页面是在请求时跑的。所以像 Cookie、请求头这类按需渲染才有的能力,也只有在请求时执行的中间件里才用得上。

用 locals 共享数据

context.locals 是个普通对象,你可以在中间件里往里塞任何东西:字符串、数字、函数,甚至 Map。它会在整个请求处理过程里一路传递,页面、端点、其它中间件都能读。

// src/middleware.js
export function onRequest (context, next) {
  context.locals.user = { id: 1, name: "John Wick" };
  context.locals.welcomeTitle = () => {
    return "Welcome back " + context.locals.user.name;
  };
  context.locals.orders = new Map([["1", { product: "socks" }]]);
  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>

要记住一点:locals 只活在一次请求里。这个路由渲染完,locals 就没了,下次请求是全新一个。需要跨多次请求保留的东西(比如登录会话),得存到别处,比如 Cookie 或数据库。

一个实用例子:脱敏

中间件能在页面最终 HTML 送出去之前改它。下面把 “PRIVATE INFO” 替换成 “REDACTED”(已编辑),避免敏感词出现在页面上:

// 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
  });
};

这里先 await next() 拿到渲染好的响应,再改它的正文,最后返回新响应。顺序很关键:先放行、再加工。

给中间件加类型

想要类型提示,用 astro:middleware 里的 defineMiddleware()

// src/middleware.ts
import { defineMiddleware } from "astro:middleware";

export const onRequest = defineMiddleware((context, next) => {
  // context 和 next 自动有类型
});

如果你用 JSDoc 而不是 TypeScript,可以用 MiddlewareHandler 类型达到同样效果。

想给 Astro.locals 里的内容加类型,在 src/env.d.ts 里扩展全局的 App.Locals 接口,之后在 .astro 文件和中间件里都会有自动补全:

// src/env.d.ts
type User = {
  id: number;
  name: string;
};

declare namespace App {
  interface Locals {
    user: User;
    welcomeTitle: () => string;
    orders: Map<string, object>;
  }
}

串联多个中间件

有时候一个中间件不够,你想把”校验、鉴权、打招呼”分成几个,按顺序跑。用 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

看出规律了吗?next() 之前的代码按排列顺序”从前到后”跑,next() 之后的代码按”从后到前”回来。这叫”洋葱模型”——请求一层层往里进,响应一层层往外出。

改写与重定向:rewrite

中间件里能用 context.rewrite() 显示另一个页面的内容,而把访客跳转到新地址。常用在”没登录就展示登录页”:

// src/middleware.js
import { isLoggedIn } from "~/auth.js"
export function onRequest (context, next) {
  if (!isLoggedIn(context)) {
    return context.rewrite(new Request("/login", {
      headers: {
        "x-redirect-to": context.url.pathname
      }
    }));
  }
  return next();
};

rewrite 会触发一次新的渲染,中间件会再跑一遍。如果你只想”原地改写当前请求”而不重跑中间件,可以给 next() 传一个路径参数:

export const onRequest = sequence(first, second);

async function first(context, next) {
  console.log(context.url.pathname); // 输出 "/blog"
  return next("/"); // 改写请求到首页,但不重跑中间件
}

async function second(context, next) {
  console.log(context.url.pathname); // 输出 "/"
  return next();
}
Tip

多个中间件用 sequence 串联时,给 next() 传路径是”就地改写”,不会重跑中间件;用 context.rewrite() 则会重跑。处理 HTML 表单提交(Astro Actions)时,官方建议在 .astro 模板里用 Astro.rewrite() 而不是在中间件里做,避免请求体被提前消费导致报错。

错误页面也会经过中间件

中间件会尽量对所有按需渲染页面执行,包括 Astro 默认的 404 页和你自定义的 404 页。不过最终是否执行,由你用的适配器决定,某些平台会直接返回它们自己的错误页。500 错误页在渲染前也会先跑中间件——除非错误恰好出在中间件自己身上。

一个鉴权守卫的例子

把前面几点合起来,中间件最常见的用法就是”鉴权守卫”:没登录就挡在门外,登录了才放行,并把用户信息通过 locals 传给页面。

// src/middleware.js
import { sequence } from "astro:middleware";

async function authGuard(context, next) {
  const token = context.cookies.get("sb-access-token");
  if (!token) {
    // 没令牌,改写到登录页(不重跑中间件)
    return next("/login");
  }
  // 有令牌,把用户名放进取,页面里直接用
  context.locals.userName = "已登录用户";
  return next();
}

export const onRequest = sequence(authGuard);

页面里就能直接用:

---
const name = Astro.locals.userName;
---
{name ? <p>欢迎,{name}</p> : <a href="/login">去登录</a>}

注意这里用 next("/login") 做原地改写,既不会让用户地址栏跳走,也不会重跑中间件造成死循环。若用 context.rewrite() 则会重跑,用在登录守卫时要小心无限重定向。

Tip

多个中间件用 sequence 串联时,越靠前越”外层”。把通用预处理(如计时、日志)放前面,把具体业务(如鉴权)放后面,结构更清楚。

在中间件里兜底错误

next() 之后的渲染过程也可能抛错(比如后面的页面崩了)。用 try/catchnext() 包起来,就能在出错时给个友好响应,而不是对访客甩一张 500 白页:

export const onRequest = async (context, next) => {
  try {
    return await next();
  } catch (err) {
    return new Response("出错了,稍后再试", { status: 500 });
  }
};

不过要留意:如果错误恰恰发生在中间件自己内部,这一段是兜不住的,页面会走到适配器提供的错误页。所以中间件里的代码也要尽量简单、别放容易崩的重逻辑。

用中间件统一加响应头

有些响应头你想「每个页面都带上」,比如告诉浏览器别缓存后台页面,或者统一加一个安全相关的头。与其每个页面都写一遍,不如放在中间件里一次搞定。注意:改头要等响应回来之后,所以先把 next() 的结果接住再改:

// src/middleware.js
export const onRequest = async (context, next) => {
  const response = await next();
  response.headers.set("X-Content-Type-Options", "nosniff");
  return response;
};

这里先 await next() 拿到渲染好的响应,再对它的 headers 做修改,最后把改过的响应返回去。顺序不能反——响应还没生成,头上什么都没有可改。

小提醒:预渲染的静态页面在构建时就跑过一次中间件了。这种「每次请求都统一加头」的活儿,更适合在按需渲染的站点上用,效果才真正每次都生效。

小结

中间件就是 src/middleware.js 里导出的 onRequest(context, next)。它能拦截每次渲染、用 context.locals 在页面和端点间共享数据、用 sequence() 按洋葱模型串联多个、用 rewrite() 改写请求。它是做统一鉴权、全局数据、内容加工的好地方。

下一章我们整体看看后端服务和 CMS 怎么跟 Astro 配合。中间件常在这些外部系统接入时充当”统一入口”,值得提前吃透。