Fastify 的生命周期以钩子(Hooks)形式暴露,让开发者能在请求的不同阶段插入逻辑。与 Koa 的洋葱模型「全局统一」不同,Fastify 的钩子既可以是全局的,也可以是路由级或封装级的,粒度更细。
前置知识:先阅读 Fastify 路由与 Handler 与 插件与封装机制。
客户端请求
│
▼
① onRequest ──────────────▶ 最先执行,可提前拒绝
│
▼
② preParsing ─────────────▶ 解析请求体之前(可替换流)
│
▼
③ preValidation ──────────▶ Schema 校验之前,可改写 body/query
│
▼
④ [请求体解析 / Schema 校验]
│
▼
⑤ preHandler ─────────────▶ 进入 Handler 前的最后一步(鉴权常用)
│
▼
⑥ Handler ────────────────▶ 业务处理
│
▼
⑦ preSerialization ───────▶ 序列化响应之前,可改写返回数据
│
▼
⑧ onSend ─────────────────▶ 响应发送前,可改 header/body
│
▼
⑨ onResponse ─────────────▶ 响应已发送,仅清理、统计
│
▼
响应返回客户端异常路径:任何一步抛出错误都会进入 onError(和全局错误处理),随后视恢复情况继续后续的 onSend。
钩子可通过 app.addHook('name', fn)(全局)、app.register 作用域内(封装级)、路由选项内(路由级)三种方式挂载。
// 全局:对所有请求生效
app.addHook('onRequest', async (req, reply) => {
req.log.info({ url: req.url }, 'incoming');
});
// 封装级:仅对其内注册的路由生效
await app.register(async (scope) => {
scope.addHook('preHandler', auth);
scope.get('/private', () => ({ sec: true }));
});
// 路由级:仅对单条路由生效
app.get('/special', {
preHandler: [auth],
handler: () => ({ special: true }),
});onRequest:入口拦截在请求被处理前执行,适合做粗粒度拦截(IP 黑白名单、全局统计):
app.addHook('onRequest', async (req, reply) => {
const allowed = ['127.0.0.1'];
if (!allowed.includes(req.ip)) {
reply.code(403).send({ code: 'FORBIDDEN' });
}
});preValidation / preHandler:鉴权与预处理preValidation 在 Schema 校验之前执行(可在此改写即将被校验的 body/query),preHandler 在校验之后、进入 Handler 前执行——两者都是常见的鉴权位置:
app.decorate('authenticate', async (req, reply) => {
try { await req.jwtVerify(); }
catch { reply.code(401).send({ code: 'UNAUTHORIZED' }); }
});
app.get('/profile', {
preHandler: [app.authenticate], // 路由级数组写法
handler: async (req) => req.user,
});
preHandler数组里的钩子按顺序执行;任一reply.send()后后续钩子不再运行。
onSend:统一改写响应在响应序列化后、真正发送前执行,适合统一加响应头、脱敏或压缩:
app.addHook('onSend', async (req, reply, payload) => {
reply.header('X-Powered-By', 'Fastify');
// payload 是已序列化的字符串/对象,可改写后返回
return payload;
});
// 自定义 Content-Type 时的序列化改写
app.addHook('onSend', async (req, reply, payload) => {
if (reply.getHeader('content-type') === 'text/plain') {
return `<custom>${payload}</custom>`;
}
return payload;
});onError:错误兜底与日志错误进入统一处理前可在此捕获:
app.addHook('onError', async (req, reply, error) => {
req.log.error({ error }, 'request error');
// 若在此 reply.send() 则覆盖默认错误响应
});preParsing:请求体前处理可用于压缩流或自定义解析前逻辑:
app.addHook('preParsing', async (req, reply, payload) => {
req.log.info('about to parse body');
return payload; // 返回被替换的请求体流(如有需要)
});| 钩子 | 触发时机 | 典型用途 |
|---|---|---|
onReady | 所有插件注册完成、服务开始监听前 | 连接数据库、预热缓存 |
onListen | 服务开始监听后 | 打印运行时信息 |
onClose | 服务关闭时 | 释放连接、清空队列 |
onError | 请求出错时 | 错误上报 |
app.addHook('onReady', async () => {
await db.connect();
await cache.warmup();
});
app.addHook('onClose', async () => {
await db.disconnect();
});多个同类型全局钩子按注册顺序执行;路由级钩子数组按数组顺序执行。全局钩子先于路由级钩子在对应阶段执行。
请求级钩子的 this 指向当前封装的 app(或用闭包捕获),可访问 req/reply/app(如 req.log、reply.code)。
app.addHook('preHandler', async function (req, reply) {
// 用普通函数以保留 this === app
console.log(this.config); // 当前封装实例
});import Fastify from 'fastify';
const app = Fastify({ logger: true });
// 1) 日志(全局 onRequest)
app.addHook('onRequest', async (req) => {
req.log.info({ method: req.method, url: req.url });
});
// 2) 统一响应包裹(onSend 改写 payload)
app.addHook('onSend', async (req, reply, payload) => {
if (typeof payload === 'string' && payload.startsWith('{')) {
return JSON.stringify({ ok: true, code: 0, data: JSON.parse(payload) });
}
return payload;
});
// 3) 鉴权(路由级 preHandler)
app.decorate('authed', async (req, reply) => {
if (!req.headers['x-token']) {
reply.code(401).send({ ok: false, message: 'missing token' });
}
});
app.get('/', { preHandler: [app.authed] }, async () => ({ hello: 'world' }));
await app.listen({ port: 3000 });reply.send() 后 next 钩子还会执行吗?不会。reply.send() 之后,同一阶段的后续普通钩子被短路,请求直接进入发送流程。
preHandler 数组和单独注册的区别?数组是路由级的、只在当前路由生效;addHook('preHandler') 是全局生效。推荐在封装插件里用 addHook 统一管理。
全局放 addHook,特殊逻辑放路由选项的 preHandler/preValidation 数组,二者组合即可覆盖绝大多数场景。