中间件

Qwik City 提供了服务器中间件,允许您集中和链式处理逻辑,例如身份验证、安全性、缓存、重定向和日志记录。中间件还可以用于定义端点。端点用于返回数据,例如 RESTful API 或 GraphQL API。

中间件由一组按照路由定义的特定顺序调用的函数组成。返回响应的中间件称为端点

中间件函数

通过在 src/routes 文件夹中的 layout.tsxindex.tsx 文件中导出一个名为 onRequest(或 onGetonPostonPutonPatchonDelete)的函数来定义中间件。

以下示例显示了一个简单的 onRequest 中间件函数,用于记录所有请求。

文件:src/routes/layout.tsx

import type { RequestHandler } from '@builder.io/qwik-city';
 
export const onRequest: RequestHandler = async ({next, url}) => {
  console.log('Before request', url);
  await next();
  console.log('After request', url);
};

如果您想拦截特定的 HTTP 方法,可以使用以下其中之一。例如,如果同时使用 onRequestonGet,则两者都会执行,但 onRequest 会在链中的 onGet 之前执行。

// 仅在特定的 HTTP 方法中调用
export const onGet: RequestHandler = async (requestEvent) => { ... }
export const onPost: RequestHandler = async (requestEvent) => { ... }
export const onPut: RequestHandler = async (requestEvent) => { ... }
export const onPatch: RequestHandler = async (requestEvent) => { ... }
export const onDelete: RequestHandler = async (requestEvent) => { ... }

每个中间件函数都会传递一个 RequestEvent 对象,该对象允许中间件控制响应。

调用顺序

中间件函数链的调用顺序由它们的位置决定。从最顶层的 layout.tsx 开始,以给定路由的 index.tsx 结束。(与路由路径定义的布局和路由组件的顺序相同的解析逻辑。)

例如,如果请求是 /api/greet/,则以下文件夹结构的调用顺序如下:

src/
└── routes/
    ├── layout.tsx            # 调用顺序:1(第一个)
    └── api/
        ├── layout.tsx        # 调用顺序:2   
        └── greet/
            └── index.ts      # 调用顺序:3(最后一个)

Qwik City 会按顺序查看每个文件,并检查它是否有导出的 onRequest(或 onGetonPostonPutonPatchonDelete)函数。如果找到该函数,则按照该顺序将函数添加到中间件执行链中。

routeLoader$routeAction$ 也被视为中间件的一部分,它们在 on* 函数之后执行,并在默认导出组件之前执行。

组件作为 HTML 端点

您可以将组件渲染视为隐式的 HTML 端点。因此,如果 index.tsx 有一个默认导出组件,则该组件隐式成为中间件链中的一个端点。由于组件渲染是中间件链的一部分,这使您可以拦截组件渲染,例如作为身份验证、日志记录或其他横切关注点的一部分。

import { component$ } from '@builder.io/qwik';
import { type RequestHandler } from '@builder.io/qwik-city';
 
export const onRequest: RequestHandler = async ({ redirect }) => {
  if (!isLoggedIn()) {
    throw redirect(308, '/login');
  }
};
 
export default component$(() => {
  return <div>You are logged in.</div>;
});
 
function isLoggedIn() {
  return true; // 模拟登录为 true
}

RequestEvent

所有中间件函数都会传递一个 RequestEvent 对象,该对象可用于控制 HTTP 响应的流程。例如,您可以读取/写入 cookie、头信息、重定向、生成响应并提前退出中间件链。中间件函数按照上述顺序执行,从最顶层的 layout.tsx 到最后一个 index.tsx

next()

使用 next() 函数执行链中的下一个中间件函数。当中间件函数正常返回且没有显式调用 next() 时,这是默认行为。可以使用 next() 函数在下一个中间件函数周围实现包装行为。

import { type RequestHandler } from '@builder.io/qwik-city';
 
// 泛型函数 `onRequest` 首先执行
export const onRequest: RequestHandler = async ({ next, sharedMap, json }) => {
  const log: string[] = [];
  sharedMap.set('log', log);
 
  log.push('onRequest start');
  await next(); // 执行下一个中间件函数(onGet)
  log.push('onRequest end');
 
  json(200, log);
};
 
// 其次执行特定的函数,例如 `onGet`
export const onGet: RequestHandler = async ({ next, sharedMap }) => {
  const log = sharedMap.get('log') as string[];
 
  log.push('onGET start');
  // 执行下一个中间件函数
  // (在我们的例子中,没有更多的中间件函数或组件。)
  await next();
  log.push('onGET end');
};

通常情况下,函数的正常(非异常)返回将执行链中的下一个函数。但是,如果从函数中抛出错误,将停止执行链。这通常用于身份验证或授权,并返回 401403 HTTP 状态码。由于 next() 是隐式的,为了防止调用链中的下一个中间件函数,必须使用 throw

import { type RequestHandler } from '@builder.io/qwik-city';
 
export const onRequest: RequestHandler = async ({ next, sharedMap, json }) => {
  const log: string[] = [];
  sharedMap.set('log', log);
 
  log.push('onRequest');
  if (isLoggedIn()) {
    // 正常行为调用下一个中间件
    await next();
  } else {
    // 如果未登录,则抛出以阻止对下一个中间件的隐式调用。
    throw json(404, log);
  }
};
 
export const onGet: RequestHandler = async ({ sharedMap }) => {
  const log = sharedMap.get('log') as string[];
  log.push('onGET');
};
 
function isLoggedIn() {
  return false; // 始终返回 false 作为模拟示例
}

sharedMap

使用 sharedMap 在中间件函数之间共享数据。sharedMap 的作用域限定为 HTTP 请求。常见用例是使用 sharedMap 存储用户详细信息,以便其他中间件函数、routeLoader$() 或组件可以使用它。

import { component$ } from '@builder.io/qwik';
import {
  routeLoader$,
  type RequestHandler,
  type Cookie,
} from '@builder.io/qwik-city';
 
interface User {
  username: string;
  email: string;
}
 
export const onRequest: RequestHandler = async ({
  sharedMap,
  cookie,
  send,
}) => {
  const user = loadUserFromCookie(cookie);
  if (user) {
    sharedMap.set('user', user);
  } else {
    throw send(401, 'NOT_AUTHORIZED');
  }
};
 
function loadUserFromCookie(cookie: Cookie): User | null {
  // 这是您将检查 cookie 的位置。
  if (cookie) {
    // 仅为此演示返回模拟用户。
    return {
      username: `Mock User`,
      email: `mock@users.com`,
    };
  } else {
    return null;
  }
}
 
export const useUser = routeLoader$(({ sharedMap }) => {
  return sharedMap.get('user') as User;
});
 
export default component$(() => {
  const log = useUser();
  return (
    <div>
      {log.value.username} ({log.value.email})
    </div>
  );
});

headers

使用 headers 设置与当前请求关联的响应头。 (要读取请求头,请参见 request.headers。)中间件可以手动向响应添加响应头,使用 headers 属性。

import { type RequestHandler } from '@builder.io/qwik-city';
 
export const onGet: RequestHandler = async ({ headers, json }) => {
  headers.set('X-SRF-TOKEN', Math.random().toString(36).replace('0.', ''));
  const obj: Record<string, string> = {};
  headers.forEach((value, key) => (obj[key] = value));
  json(200, obj);
};

使用 cookie 设置和检索请求的 cookie 信息。中间件可以手动读取和设置 cookie,使用 cookie 函数。这可能对于设置会话 cookie(例如 JWT 令牌)或跟踪用户的 cookie 很有用。

import { type RequestHandler } from '@builder.io/qwik-city';
 
export const onGet: RequestHandler = async ({ cookie, json }) => {
  let count = cookie.get('Qwik.demo.count')?.number() || 0;
  count++;
  cookie.set('Qwik.demo.count', count);
  json(200, { count });
};

method

返回当前的 HTTP 请求方法:GETPOSTPATCHPUTDELETE

import { type RequestHandler } from '@builder.io/qwik-city';
 
export const onRequest: RequestHandler = async ({ method, json }) => {
  json(200, { method });
};

url

返回当前的 HTTP 请求 URL。(如果需要在组件中获取当前 URL,请使用 useLocation()url 用于中间件函数。)

import { type RequestHandler } from '@builder.io/qwik-city';
 
export const onGet: RequestHandler = async ({ url, json }) => {
  json(200, { url: url.toString() });
};

basePathname

返回应用程序挂载的当前基本路径名 URL。通常为 /,但如果应用程序挂载在子路径中,则可能不同。请参见 vite qwikCity({root: '/my-sub-path-location'})

import { type RequestHandler } from '@builder.io/qwik-city';
 
export const onGet: RequestHandler = async ({ basePathname, json }) => {
  json(200, { basePathname });
};

params

检索 URL 的 "params"。例如,params.myId 将允许您从此路由定义 /base/[myId]/something 中检索 myId

import { type RequestHandler } from '@builder.io/qwik-city';
 
export const onGet: RequestHandler = async ({ params, json }) => {
  json(200, { params });
};

query

使用 query 检索 URL 查询参数。(这是 url.searchParams 的简写。)它提供给中间件函数使用,组件应使用 useLocation() API。

import { type RequestHandler } from '@builder.io/qwik-city';
 
export const onGet: RequestHandler = async ({ query, json }) => {
  const obj: Record<string, string> = {};
  query.forEach((v, k) => (obj[k] = v));
  json(200, obj);
};

parseBody()

使用 parseBody() 解析提交到 URL 的表单数据。

此方法将检查请求头中的 Content-Type 头,并相应地解析主体。它支持 application/jsonapplication/x-www-form-urlencodedmultipart/form-data 内容类型。

如果未设置 Content-Type 头,它将返回 null

import { type RequestHandler } from '@builder.io/qwik-city';
 
export const onGet: RequestHandler = async ({ html }) => {
  html(
    200,
    `
      <form id="myForm" method="POST">
        <input type="text" name="project" value="Qwik"/>
        <input type="text" name="url" value="http://qwik.builder.io"/>
      </form>
      <script>myForm.submit()</script>`
  );
};
 
export const onPost: RequestHandler = async ({ parseBody, json }) => {
  json(200, { body: await parseBody() });
};

cacheControl

设置缓存头的便捷 API。

import { type RequestHandler } from '@builder.io/qwik-city';
 
export const onGet: RequestHandler = async ({
  cacheControl,
  headers,
  json,
}) => {
  cacheControl({ maxAge: 42, public: true });
  const obj: Record<string, string> = {};
  headers.forEach((value, key) => (obj[key] = value));
  json(200, obj);
};

platform

部署平台(Azure、Bun、Cloudflare、Deno、Google Cloud Run、Netlify、Node.js、Vercel 等)特定的 API。

import { type RequestHandler } from '@builder.io/qwik-city';
 
export const onGet: RequestHandler = async ({ platform, json }) => {
  json(200, Object.keys(platform));
};

locale()

设置或检索当前区域设置。

import { type RequestHandler } from '@builder.io/qwik-city';
 
export const onRequest: RequestHandler = async ({ locale, request }) => {
  const acceptLanguage = request.headers.get('accept-language');
  const [languages] = acceptLanguage?.split(';') || ['?', '?'];
  const [preferredLanguage] = languages.split(',');
  locale(preferredLanguage);
};
 
export const onGet: RequestHandler = async ({ locale, json }) => {
  json(200, { locale: locale() });
};

status()

独立于写入响应设置响应的状态,对于流式传输很有用。端点可以手动更改响应的 HTTP 状态码,使用 status() 方法。

import { type RequestHandler } from '@builder.io/qwik-city';
 
export const onGet: RequestHandler = async ({ status, getWritableStream }) => {
  status(200);
  const stream = getWritableStream();
  const writer = stream.getWriter();
  writer.write(new TextEncoder().encode('Hello World!'));
  writer.close();
};

redirect()

重定向到新的 URL。请注意,抛出错误以防止其他中间件函数运行的重要性。redirect() 方法将自动将 Location 头设置为重定向的 URL。

import { type RequestHandler } from '@builder.io/qwik-city';
 
export const onGet: RequestHandler = async ({ redirect, url }) => {
  throw redirect(
    308,
    new URL('/demo/qwikcity/middleware/status/', url).toString()
  );
};

error()

设置错误响应。

import { type RequestHandler } from '@builder.io/qwik-city';
 
export const onGet: RequestHandler = async ({ error }) => {
  throw error(500, 'ERROR: Demonstration of an error response.');
};

text()

发送基于文本的响应。只需调用 text(status, string) 方法即可创建文本端点。text() 方法将自动将 Content-Type 头设置为 text/plain; charset=utf-8

import { type RequestHandler } from '@builder.io/qwik-city';
 
export const onGet: RequestHandler = async ({ text }) => {
  text(200, 'Text based response.');
};

html()

发送 HTML 响应。

import { type RequestHandler } from '@builder.io/qwik-city';
 
export const onGet: RequestHandler = async ({ html }) => {
  html(
    200,
    ` 
      <html>
        <body>
          <h1>HTML response</h1>
        </body>
      </html>`
  );
};

json()

只需调用 json(status, object) 方法即可创建 JSON 端点。json() 方法将自动将 Content-Type 头设置为 application/json; charset=utf-8 并对数据进行 JSON 字符串化。

import { type RequestHandler } from '@builder.io/qwik-city';
 
export const onGet: RequestHandler = async ({ json }) => {
  json(200, { hello: 'world' });
};

send()

只需调用 send(Response) 方法即可创建原始端点。send() 方法接受标准的 Response 对象,可以使用 Response 构造函数创建该对象。

import type { RequestHandler } from '@builder.io/qwik-city';
 
export const onGet: RequestHandler = async (requestEvent) => {
  const response = new Response('Hello World', {
    status: 200,
    headers: {
      'Content-Type': 'text/plain',
    },
  });
  requestEvent.send(response);
};

exit()

抛出以停止中间件函数的执行。

import { type RequestHandler } from '@builder.io/qwik-city';
 
export const onGet: RequestHandler = async ({ exit }) => {
  throw exit();
};

env

以与平台无关的方式检索环境属性。

import { type RequestHandler } from '@builder.io/qwik-city';
 
export const onGet: RequestHandler = async ({ env, json }) => {
  json(200, {
    USER: env.get('USER'),
    MODE_ENV: env.get('MODE_ENV'),
    PATH: env.get('PATH'),
    SHELL: env.get('SHELL'),
  });
};

getWritableStream()

设置流式响应。

import type { RequestHandler } from '@builder.io/qwik-city';
 
export const onGet: RequestHandler = async (requestEvent) => {
  const writableStream = requestEvent.getWritableStream();
  const writer = writableStream.getWriter();
  const encoder = new TextEncoder();
 
  writer.write(encoder.encode('Hello World\n'));
  await wait(100);
  writer.write(encoder.encode('After 100ms\n'));
  await wait(100);
  writer.write(encoder.encode('After 200ms\n'));
  await wait(100);
  writer.write(encoder.encode('END'));
  writer.close();
};
 
const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

headerSent

检查是否已设置头。

import { type RequestHandler } from '@builder.io/qwik-city';
 
export const onGet: RequestHandler = async ({ headersSent, json }) => {
  if (!headersSent) {
    json(200, { response: 'default response' });
  }
};
 
export const onRequest: RequestHandler = async ({ status }) => {
  status(200);
};

request

获取 HTTP 请求对象。用于获取请求数据,例如头信息。

import { type RequestHandler } from '@builder.io/qwik-city';
 
export const onGet: RequestHandler = async ({ json, request }) => {
  const obj: Record<string, string> = {};
  request.headers.forEach((v, k) => (obj[k] = v));
  json(200, { headers: obj });
};

Contributors

Thanks to all the contributors who have helped make this documentation better!

  • adamdbradley
  • manucorporat
  • mhevery
  • CoralWombat
  • harishkrishnan24