路由钩子 #

一、钩子概述 #

钩子(Hooks)是Fastify请求生命周期中的函数,允许你在特定时机执行自定义逻辑。

1.1 钩子类型 #

text
请求生命周期
─────────────────────────────────────────────
Incoming Request
       │
       ▼
┌─────────────────┐
│   onRequest     │  ← 应用级/路由级
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│  preParsing     │  ← 应用级/路由级
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│   parsing       │  ← 内置解析
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│ preValidation   │  ← 应用级/路由级
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│   validation    │  ← Schema验证
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│  preHandler     │  ← 应用级/路由级
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│    handler      │  ← 路由处理函数
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│ preSerialization│  ← 应用级/路由级
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│  serialization  │  ← 响应序列化
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│     onSend      │  ← 应用级/路由级
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│   onResponse    │  ← 应用级/路由级
└────────┬────────┘
         │
         ▼
    Response Sent

1.2 钩子分类 #

类型 说明 作用范围
应用级钩子 全局生效 所有请求
路由级钩子 特定路由生效 单个路由
错误钩子 错误处理 所有错误

二、应用级钩子 #

2.1 onRequest #

在请求开始时执行,最早可用的钩子。

javascript
fastify.addHook('onRequest', async (request, reply) => {
  console.log('Request received:', request.url)
  request.startTime = Date.now()
})

fastify.addHook('onRequest', async (request, reply) => {
  const token = request.headers.authorization
  
  if (!token && request.url.startsWith('/api')) {
    reply.code(401).send({ error: 'Unauthorized' })
    return reply
  }
})

2.2 preParsing #

在请求体解析前执行。

javascript
fastify.addHook('preParsing', async (request, reply, payload) => {
  console.log('Content-Type:', request.headers['content-type'])
  return payload
})

fastify.addHook('preParsing', async (request, reply, payload) => {
  if (request.headers['content-encoding'] === 'gzip') {
    const zlib = require('zlib')
    return payload.pipe(zlib.createGunzip())
  }
  return payload
})

2.3 preValidation #

在Schema验证前执行。

javascript
fastify.addHook('preValidation', async (request, reply) => {
  if (request.body && typeof request.body === 'string') {
    try {
      request.body = JSON.parse(request.body)
    } catch (e) {
      reply.code(400).send({ error: 'Invalid JSON' })
      return reply
    }
  }
})

fastify.addHook('preValidation', async (request, reply) => {
  request.body = {
    ...request.body,
    timestamp: Date.now()
  }
})

2.4 preHandler #

在路由处理函数前执行,最常用的钩子。

javascript
fastify.addHook('preHandler', async (request, reply) => {
  console.log('Before handler')
})

fastify.register(require('@fastify/jwt'), { secret: 'secret' })

fastify.addHook('preHandler', async (request, reply) => {
  try {
    await request.jwtVerify()
  } catch (err) {
    reply.code(401).send({ error: 'Invalid token' })
  }
})

2.5 preSerialization #

在响应序列化前执行,可修改响应数据。

javascript
fastify.addHook('preSerialization', async (request, reply, payload) => {
  if (payload && typeof payload === 'object') {
    return {
      ...payload,
      timestamp: Date.now(),
      requestId: request.id
    }
  }
  return payload
})

fastify.addHook('preSerialization', async (request, reply, payload) => {
  if (request.query.wrap === 'true') {
    return {
      success: true,
      data: payload
    }
  }
  return payload
})

2.6 onSend #

在响应发送前执行,可修改响应内容。

javascript
fastify.addHook('onSend', async (request, reply, payload) => {
  reply.header('X-Response-Time', Date.now() - request.startTime)
  return payload
})

fastify.addHook('onSend', async (request, reply, payload) => {
  if (typeof payload === 'string') {
    return payload.toUpperCase()
  }
  return payload
})

fastify.addHook('onSend', async (request, reply, payload) => {
  if (request.query.pretty === 'true') {
    reply.header('Content-Type', 'application/json')
    return JSON.stringify(JSON.parse(payload), null, 2)
  }
  return payload
})

2.7 onResponse #

在响应发送后执行,用于清理和日志。

javascript
fastify.addHook('onResponse', async (request, reply) => {
  const duration = Date.now() - request.startTime
  console.log(`Request completed in ${duration}ms`)
})

fastify.addHook('onResponse', async (request, reply) => {
  fastify.log.info({
    method: request.method,
    url: request.url,
    statusCode: reply.statusCode,
    responseTime: reply.elapsedTime
  })
})

2.8 onError #

在错误发生时执行。

javascript
fastify.addHook('onError', async (request, reply, error) => {
  console.error('Error occurred:', error.message)
})

fastify.addHook('onError', async (request, reply, error) => {
  if (error.validation) {
    error.message = 'Validation failed: ' + error.message
  }
})

fastify.addHook('onError', async (request, reply, error) => {
  fastify.log.error({
    error: error.message,
    stack: error.stack,
    requestId: request.id
  })
})

三、路由级钩子 #

3.1 在路由中定义钩子 #

javascript
fastify.get('/users/:id', {
  onRequest: async (request, reply) => {
    console.log('Route-level onRequest')
  },
  preHandler: async (request, reply) => {
    console.log('Route-level preHandler')
  },
  handler: async (request, reply) => {
    return { userId: request.params.id }
  },
  onResponse: async (request, reply) => {
    console.log('Route-level onResponse')
  }
})

3.2 多个钩子 #

javascript
fastify.get('/protected', {
  preHandler: [
    async (request, reply) => {
      console.log('First preHandler')
    },
    async (request, reply) => {
      console.log('Second preHandler')
    },
    async (request, reply) => {
      console.log('Third preHandler')
    }
  ],
  handler: async (request, reply) => {
    return { message: 'Protected route' }
  }
})

3.3 认证钩子示例 #

javascript
fastify.register(require('@fastify/jwt'), { secret: 'secret' })

fastify.decorate('authenticate', async function (request, reply) {
  try {
    await request.jwtVerify()
  } catch (err) {
    reply.code(401).send({ error: 'Unauthorized' })
  }
})

fastify.get('/profile', {
  preHandler: fastify.authenticate
}, async (request, reply) => {
  return { user: request.user }
})

fastify.get('/admin', {
  preHandler: [
    fastify.authenticate,
    async (request, reply) => {
      if (!request.user.isAdmin) {
        reply.code(403).send({ error: 'Forbidden' })
      }
    }
  ]
}, async (request, reply) => {
  return { message: 'Admin area' }
})

四、钩子执行顺序 #

4.1 执行顺序规则 #

text
1. 应用级 onRequest
2. 路由级 onRequest
3. 应用级 preParsing
4. 路由级 preParsing
5. 应用级 preValidation
6. 路由级 preValidation
7. 应用级 preHandler
8. 路由级 preHandler
9. 路由处理函数
10. 应用级 preSerialization
11. 路由级 preSerialization
12. 应用级 onSend
13. 路由级 onSend
14. 应用级 onResponse
15. 路由级 onResponse

4.2 钩子执行示例 #

javascript
fastify.addHook('onRequest', async () => console.log('App onRequest'))
fastify.addHook('preHandler', async () => console.log('App preHandler'))

fastify.get('/test', {
  onRequest: async () => console.log('Route onRequest'),
  preHandler: async () => console.log('Route preHandler'),
  handler: async () => {
    console.log('Handler')
    return { ok: true }
  }
})

输出:

text
App onRequest
Route onRequest
App preHandler
Route preHandler
Handler

五、钩子与插件 #

5.1 在插件中注册钩子 #

javascript
const fp = require('fastify-plugin')

async function authPlugin(fastify, opts) {
  fastify.decorate('authenticate', async function (request, reply) {
    const token = request.headers.authorization
    
    if (!token) {
      reply.code(401).send({ error: 'No token provided' })
      return
    }
    
    try {
      request.user = await verifyToken(token)
    } catch (err) {
      reply.code(401).send({ error: 'Invalid token' })
    }
  })
  
  fastify.addHook('onRequest', async (request, reply) => {
    if (request.url.startsWith('/api')) {
      await fastify.authenticate(request, reply)
    }
  })
}

module.exports = fp(authPlugin)

5.2 条件性钩子 #

javascript
fastify.register(async function (fastify, opts) {
  fastify.addHook('preHandler', async (request, reply) => {
    if (request.routeConfig.auth) {
      await fastify.authenticate(request, reply)
    }
  })
  
  fastify.get('/public', {
    config: { auth: false }
  }, async (request, reply) => {
    return { public: true }
  })
  
  fastify.get('/private', {
    config: { auth: true }
  }, async (request, reply) => {
    return { private: true }
  })
})

六、常见使用场景 #

6.1 请求日志 #

javascript
fastify.addHook('onRequest', async (request, reply) => {
  request.startTime = Date.now()
  fastify.log.info({
    type: 'request',
    method: request.method,
    url: request.url,
    requestId: request.id
  })
})

fastify.addHook('onResponse', async (request, reply) => {
  const duration = Date.now() - request.startTime
  fastify.log.info({
    type: 'response',
    method: request.method,
    url: request.url,
    statusCode: reply.statusCode,
    duration: `${duration}ms`
  })
})

6.2 请求ID追踪 #

javascript
const { v4: uuidv4 } = require('uuid')

fastify.addHook('onRequest', async (request, reply) => {
  request.id = request.headers['x-request-id'] || uuidv4()
  reply.header('X-Request-Id', request.id)
})

6.3 CORS处理 #

javascript
fastify.addHook('onRequest', async (request, reply) => {
  reply.header('Access-Control-Allow-Origin', '*')
  reply.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
  reply.header('Access-Control-Allow-Headers', 'Content-Type, Authorization')
  
  if (request.method === 'OPTIONS') {
    reply.code(204).send()
    return reply
  }
})

6.4 请求限流 #

javascript
const rateLimit = new Map()

fastify.addHook('onRequest', async (request, reply) => {
  const ip = request.ip
  const limit = 100
  const windowMs = 60000
  
  const now = Date.now()
  const windowStart = now - windowMs
  
  if (!rateLimit.has(ip)) {
    rateLimit.set(ip, [])
  }
  
  const requests = rateLimit.get(ip).filter(time => time > windowStart)
  
  if (requests.length >= limit) {
    reply.code(429).send({ error: 'Too many requests' })
    return reply
  }
  
  requests.push(now)
  rateLimit.set(ip, requests)
})

6.5 响应包装 #

javascript
fastify.addHook('preSerialization', async (request, reply, payload) => {
  if (payload && typeof payload === 'object' && !payload.statusCode) {
    return {
      success: true,
      data: payload,
      timestamp: new Date().toISOString()
    }
  }
  return payload
})

6.6 错误转换 #

javascript
fastify.addHook('onError', async (request, reply, error) => {
  if (error.name === 'ValidationError') {
    error.statusCode = 400
    error.message = 'Validation failed'
  }
  
  if (error.name === 'UnauthorizedError') {
    error.statusCode = 401
    error.message = 'Authentication required'
  }
})

七、钩子最佳实践 #

7.1 钩子命名 #

javascript
const hooks = {
  logRequest: async (request, reply) => {
    console.log('Request:', request.url)
  },
  authenticate: async (request, reply) => {
    // 认证逻辑
  },
  validateAccess: async (request, reply) => {
    // 权限验证
  }
}

fastify.addHook('onRequest', hooks.logRequest)
fastify.addHook('preHandler', hooks.authenticate)

7.2 钩子复用 #

javascript
const authHook = async (request, reply) => {
  const token = request.headers.authorization
  if (!token) {
    reply.code(401).send({ error: 'Unauthorized' })
  }
}

fastify.get('/profile', { preHandler: authHook }, handler)
fastify.get('/settings', { preHandler: authHook }, handler)
fastify.get('/orders', { preHandler: authHook }, handler)

7.3 避免阻塞 #

javascript
fastify.addHook('onRequest', async (request, reply) => {
  setImmediate(() => {
    console.log('Non-blocking log')
  })
})

八、总结 #

本章我们学习了:

  1. 钩子类型:onRequest、preParsing、preValidation、preHandler等
  2. 应用级钩子:全局生效的钩子
  3. 路由级钩子:特定路由的钩子
  4. 执行顺序:钩子的执行顺序规则
  5. 钩子与插件:在插件中使用钩子
  6. 常见场景:日志、认证、限流、响应包装
  7. 最佳实践:命名、复用、避免阻塞

接下来让我们学习插件系统!

最后更新:2026-03-28