中间件

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 之前在链中执行。

// Called only with a specific HTTP method
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            # Invocation order: 1 (first)
    └── api/
        ├── layout.tsx        # Invocation order: 2   
        └── greet/
            └── index.ts      # Invocation order: 3 (last)

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; // Mock login as true
}

RequestEvent

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

next()

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

import { type RequestHandler } from '@builder.io/qwik-city';
 
// Generic function `onRequest` is executed first
export const onRequest: RequestHandler = async ({ next, sharedMap, json }) => {
  const log: string[] = [];
  sharedMap.set('log', log);
 
  log.push('onRequest start');
  await next(); // Execute next middleware function (onGet)
  log.push('onRequest end');
 
  json(200, log);
};
 
// Specific functions such as `onGet` are executed next
export const onGet: RequestHandler = async ({ next, sharedMap }) => {
  const log = sharedMap.get('log') as string[];
 
  log.push('onGET start');
  // execute next middleware function
  // (in our case, there are no more middleware functions nor components.)
  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()) {
    // normal behavior call next middleware
    await next();
  } else {
    // If not logged in throw to prevent implicit call to the next middleware.
    throw json(404, log);
  }
};
 
export const onGet: RequestHandler = async ({ sharedMap }) => {
  const log = sharedMap.get('log') as string[];
  log.push('onGET');
};
 
function isLoggedIn() {
  return false; // always return false as mock example
}

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 {
  // this is where you would check cookie for user.
  if (cookie) {
    // just return mock user for this demo.
    return {
      username: `Mock User`,
      email: `[email protected]`,
    };
  } 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.myId 将允许您从以下路由定义中检索 myId/base/[myId]/something

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="https://qwik.node.org.cn"/>
      </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()

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

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 端点就像调用 json(status, object) 方法一样简单。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 ({send}) => {
  const response = new Response('Hello World', {
    status: 200,
    headers: {
      'Content-Type': 'text/plain',
    },
  });
  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 });
};

贡献者

感谢所有帮助改进此文档的贡献者!

  • adamdbradley
  • manucorporat
  • mhevery
  • CoralWombat
  • EamonHeffernan
  • lollyxsrinand
  • gparlakov
  • mrhoodz
  • harishkrishnan24
  • jemsco