中间件
Qwik City 提供了服务器中间件,允许您集中和链式处理逻辑,例如身份验证、安全性、缓存、重定向和日志记录。中间件还可以用于定义端点。端点用于返回数据,例如 RESTful API 或 GraphQL API。
中间件由一组按照路由定义的特定顺序调用的函数组成。返回响应的中间件称为端点。
中间件函数
通过在 src/routes
文件夹中的 layout.tsx
或 index.tsx
文件中导出一个名为 onRequest
(或 onGet
、onPost
、onPut
、onPatch
和 onDelete
)的函数来定义中间件。
以下示例显示了一个简单的 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 方法,可以使用以下其中之一。例如,如果同时使用 onRequest
和 onGet
,则两者都会执行,但 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
(或 onGet
、onPost
、onPut
、onPatch
和 onDelete
)函数。如果找到该函数,则按照该顺序将函数添加到中间件执行链中。
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');
};
通常情况下,函数的正常(非异常)返回将执行链中的下一个函数。但是,如果从函数中抛出错误,将停止执行链。这通常用于身份验证或授权,并返回 401
或 403
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
函数。这可能对于设置会话 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 请求方法:GET
、POST
、PATCH
、PUT
、DELETE
。
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/json
、application/x-www-form-urlencoded
和 multipart/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 });
};