中间件
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
之前在链中执行。
// 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
(或 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; // 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');
};
通常,函数的正常(非异常)返回将执行链中的下一个函数。但是,从函数中抛出错误将停止执行链。这通常用于身份验证或授权,并返回 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()) {
// 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。这可能对设置会话 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.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/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="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 });
};