编写插件 #

一、插件基础 #

1.1 最简插件 #

javascript
async function myPlugin(fastify, options) {
  fastify.decorate('hello', () => {
    return 'Hello World'
  })
}

module.exports = myPlugin

使用插件:

javascript
const fastify = require('fastify')()

fastify.register(require('./my-plugin'))

fastify.get('/', async (request, reply) => {
  return { message: fastify.hello() }
})

1.2 使用fastify-plugin #

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

async function myPlugin(fastify, options) {
  fastify.decorate('utility', () => {
    return 'Utility function'
  })
}

module.exports = fp(myPlugin, {
  name: 'my-plugin'
})

1.3 插件选项 #

javascript
async function myPlugin(fastify, options) {
  const defaults = {
    enabled: true,
    timeout: 5000
  }
  
  const config = { ...defaults, ...options }
  
  fastify.decorate('config', config)
}

module.exports = myPlugin

二、装饰器模式 #

2.1 装饰Fastify实例 #

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

async function databasePlugin(fastify, options) {
  const { MongoClient } = require('mongodb')
  
  const client = new MongoClient(options.url)
  await client.connect()
  
  const db = client.db(options.database)
  
  fastify.decorate('mongo', client)
  fastify.decorate('db', db)
  
  fastify.addHook('onClose', async (instance) => {
    await client.close()
  })
}

module.exports = fp(databasePlugin, {
  name: 'database'
})

2.2 装饰Request #

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

async function authPlugin(fastify, options) {
  fastify.decorateRequest('user', null)
  
  fastify.addHook('onRequest', async (request, reply) => {
    const token = request.headers.authorization
    
    if (token) {
      try {
        request.user = await verifyToken(token)
      } catch (err) {
        request.log.warn('Token verification failed')
      }
    }
  })
}

module.exports = fp(authPlugin, {
  name: 'auth'
})

2.3 装饰Reply #

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

async function responsePlugin(fastify, options) {
  fastify.decorateReply('success', function (data) {
    return this.send({
      success: true,
      data,
      timestamp: new Date().toISOString()
    })
  })
  
  fastify.decorateReply('error', function (message, code = 400) {
    return this.code(code).send({
      success: false,
      error: message,
      timestamp: new Date().toISOString()
    })
  })
}

module.exports = fp(responsePlugin, {
  name: 'response'
})

2.4 装饰器检查 #

javascript
async function myPlugin(fastify, options) {
  if (fastify.hasDecorator('db')) {
    throw new Error('Database decorator already exists')
  }
  
  if (!fastify.hasRequestDecorator('user')) {
    fastify.decorateRequest('user', null)
  }
  
  if (!fastify.hasReplyDecorator('success')) {
    fastify.decorateReply('success', function (data) {
      return this.send({ success: true, data })
    })
  }
}

三、钩子集成 #

3.1 请求钩子 #

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

async function loggingPlugin(fastify, options) {
  fastify.addHook('onRequest', async (request, reply) => {
    request.startTime = Date.now()
    request.log.info({ method: request.method, url: request.url }, 'Request started')
  })
  
  fastify.addHook('onResponse', async (request, reply) => {
    const duration = Date.now() - request.startTime
    request.log.info({
      method: request.method,
      url: request.url,
      statusCode: reply.statusCode,
      duration
    }, 'Request completed')
  })
}

module.exports = fp(loggingPlugin, {
  name: 'logging'
})

3.2 认证钩子 #

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

async function authPlugin(fastify, options) {
  fastify.decorate('authenticate', async function (request, reply) {
    const token = request.headers.authorization
    
    if (!token) {
      reply.code(401).send({ error: 'No token provided' })
      return
    }
    
    try {
      const decoded = await verifyToken(token)
      request.user = decoded
    } catch (err) {
      reply.code(401).send({ error: 'Invalid token' })
    }
  })
  
  fastify.decorate('authorize', function (roles) {
    return async function (request, reply) {
      if (!request.user) {
        reply.code(401).send({ error: 'Unauthorized' })
        return
      }
      
      if (!roles.includes(request.user.role)) {
        reply.code(403).send({ error: 'Forbidden' })
      }
    }
  })
}

module.exports = fp(authPlugin, {
  name: 'auth'
})

3.3 错误处理钩子 #

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

async function errorHandlerPlugin(fastify, options) {
  fastify.setErrorHandler((error, request, reply) => {
    if (error.validation) {
      reply.code(400).send({
        statusCode: 400,
        error: 'Validation Error',
        message: error.message,
        details: error.validation
      })
      return
    }
    
    if (error.statusCode) {
      reply.code(error.statusCode).send({
        statusCode: error.statusCode,
        error: error.name,
        message: error.message
      })
      return
    }
    
    fastify.log.error(error)
    reply.code(500).send({
      statusCode: 500,
      error: 'Internal Server Error',
      message: process.env.NODE_ENV === 'production' 
        ? 'Something went wrong' 
        : error.message
    })
  })
}

module.exports = fp(errorHandlerPlugin, {
  name: 'error-handler'
})

四、服务封装 #

4.1 数据库服务 #

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

class UserService {
  constructor(db) {
    this.collection = db.collection('users')
  }
  
  async findAll(query = {}) {
    return this.collection.find(query).toArray()
  }
  
  async findById(id) {
    return this.collection.findOne({ _id: id })
  }
  
  async create(data) {
    const result = await this.collection.insertOne({
      ...data,
      createdAt: new Date()
    })
    return this.findById(result.insertedId)
  }
  
  async update(id, data) {
    await this.collection.updateOne(
      { _id: id },
      { $set: { ...data, updatedAt: new Date() } }
    )
    return this.findById(id)
  }
  
  async delete(id) {
    const result = await this.collection.deleteOne({ _id: id })
    return result.deletedCount > 0
  }
}

async function servicesPlugin(fastify, options) {
  fastify.decorate('services', {
    user: new UserService(fastify.db)
  })
}

module.exports = fp(servicesPlugin, {
  name: 'services',
  dependencies: ['database']
})

4.2 缓存服务 #

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

class CacheService {
  constructor(redis) {
    this.redis = redis
  }
  
  async get(key) {
    const data = await this.redis.get(key)
    return data ? JSON.parse(data) : null
  }
  
  async set(key, value, ttl = 3600) {
    await this.redis.set(key, JSON.stringify(value), 'EX', ttl)
  }
  
  async delete(key) {
    await this.redis.del(key)
  }
  
  async getOrSet(key, fn, ttl = 3600) {
    const cached = await this.get(key)
    
    if (cached) {
      return cached
    }
    
    const data = await fn()
    await this.set(key, data, ttl)
    return data
  }
}

async function cachePlugin(fastify, options) {
  fastify.decorate('cache', new CacheService(fastify.redis))
}

module.exports = fp(cachePlugin, {
  name: 'cache',
  dependencies: ['redis']
})

4.3 邮件服务 #

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

class EmailService {
  constructor(options) {
    this.transporter = nodemailer.createTransport(options)
    this.from = options.from
  }
  
  async send(to, subject, body) {
    await this.transporter.sendMail({
      from: this.from,
      to,
      subject,
      html: body
    })
  }
  
  async sendTemplate(to, template, data) {
    const body = await this.renderTemplate(template, data)
    await this.send(to, data.subject, body)
  }
}

async function emailPlugin(fastify, options) {
  fastify.decorate('email', new EmailService(options))
}

module.exports = fp(emailPlugin, {
  name: 'email'
})

五、配置验证 #

5.1 必需选项 #

javascript
async function myPlugin(fastify, options) {
  const required = ['apiKey', 'apiSecret']
  
  for (const key of required) {
    if (!options[key]) {
      throw new Error(`Missing required option: ${key}`)
    }
  }
  
  fastify.decorate('api', options)
}

5.2 选项Schema #

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

const optionsSchema = {
  type: 'object',
  required: ['url', 'database'],
  properties: {
    url: { type: 'string', format: 'uri' },
    database: { type: 'string', minLength: 1 },
    poolSize: { type: 'integer', minimum: 1, default: 10 },
    timeout: { type: 'integer', minimum: 1000, default: 30000 }
  }
}

async function databasePlugin(fastify, options) {
  const ajv = new Ajv({ useDefaults: true })
  const validate = ajv.compile(optionsSchema)
  
  if (!validate(options)) {
    throw new Error(`Invalid options: ${ajv.errorsText(validate.errors)}`)
  }
  
  fastify.decorate('db', await connect(options))
}

module.exports = fp(databasePlugin, {
  name: 'database'
})

5.3 默认值合并 #

javascript
async function myPlugin(fastify, options) {
  const defaults = {
    enabled: true,
    timeout: 5000,
    retries: 3,
    logLevel: 'info'
  }
  
  const config = { ...defaults, ...options }
  
  fastify.decorate('config', config)
}

六、插件依赖 #

6.1 声明依赖 #

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

async function myPlugin(fastify, options) {
  const db = fastify.db
  const redis = fastify.redis
  
  fastify.decorate('service', new Service(db, redis))
}

module.exports = fp(myPlugin, {
  name: 'my-plugin',
  dependencies: ['database', 'redis']
})

6.2 可选依赖 #

javascript
async function myPlugin(fastify, options) {
  const cache = fastify.hasDecorator('redis') 
    ? new RedisCache(fastify.redis)
    : new MemoryCache()
  
  fastify.decorate('cache', cache)
}

七、资源管理 #

7.1 连接管理 #

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

async function databasePlugin(fastify, options) {
  let connection = null
  
  const connect = async () => {
    connection = await createConnection(options)
    fastify.decorate('db', connection)
    fastify.log.info('Database connected')
  }
  
  const disconnect = async () => {
    if (connection) {
      await connection.close()
      fastify.log.info('Database disconnected')
    }
  }
  
  await connect()
  
  fastify.addHook('onClose', async () => {
    await disconnect()
  })
  
  fastify.addHook('onReady', async () => {
    if (!connection) {
      await connect()
    }
  })
}

module.exports = fp(databasePlugin, {
  name: 'database'
})

7.2 连接池 #

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

class ConnectionPool {
  constructor(options) {
    this.options = options
    this.pool = []
    this.maxSize = options.maxSize || 10
  }
  
  async acquire() {
    if (this.pool.length > 0) {
      return this.pool.pop()
    }
    return this.create()
  }
  
  async release(connection) {
    if (this.pool.length < this.maxSize) {
      this.pool.push(connection)
    } else {
      await connection.close()
    }
  }
  
  async create() {
    return createConnection(this.options)
  }
  
  async closeAll() {
    for (const conn of this.pool) {
      await conn.close()
    }
    this.pool = []
  }
}

async function poolPlugin(fastify, options) {
  const pool = new ConnectionPool(options)
  
  fastify.decorate('pool', pool)
  
  fastify.addHook('onClose', async () => {
    await pool.closeAll()
  })
}

module.exports = fp(poolPlugin, {
  name: 'pool'
})

八、插件测试 #

8.1 基本测试 #

javascript
const { test } = require('node:test')
const assert = require('node:assert')
const Fastify = require('fastify')
const myPlugin = require('./my-plugin')

test('plugin registers correctly', async (t) => {
  const fastify = Fastify()
  
  await fastify.register(myPlugin, { option: 'value' })
  await fastify.ready()
  
  assert.ok(fastify.hasDecorator('utility'))
  assert.strictEqual(fastify.utility(), 'Hello World')
  
  await fastify.close()
})

8.2 钩子测试 #

javascript
test('plugin hooks work correctly', async (t) => {
  const fastify = Fastify()
  
  await fastify.register(myPlugin)
  
  fastify.get('/test', async (request, reply) => {
    return { user: request.user }
  })
  
  await fastify.ready()
  
  const response = await fastify.inject({
    method: 'GET',
    url: '/test',
    headers: { authorization: 'valid-token' }
  })
  
  assert.strictEqual(response.statusCode, 200)
  
  await fastify.close()
})

8.3 错误测试 #

javascript
test('plugin throws on missing required option', async (t) => {
  const fastify = Fastify()
  
  await assert.rejects(
    async () => {
      await fastify.register(myPlugin, {})
      await fastify.ready()
    },
    { message: /Missing required option/ }
  )
  
  await fastify.close()
})

九、发布插件 #

9.1 package.json #

json
{
  "name": "fastify-my-plugin",
  "version": "1.0.0",
  "description": "A Fastify plugin for...",
  "main": "index.js",
  "scripts": {
    "test": "node --test",
    "lint": "eslint ."
  },
  "keywords": [
    "fastify",
    "plugin"
  ],
  "peerDependencies": {
    "fastify": "4.x"
  },
  "dependencies": {
    "fastify-plugin": "^4.0.0"
  },
  "devDependencies": {
    "fastify": "^4.0.0"
  }
}

9.2 README模板 #

markdown
# fastify-my-plugin

A Fastify plugin for...

## Installation

```bash
npm install fastify-my-plugin

Usage #

javascript
const fastify = require('fastify')()

fastify.register(require('fastify-my-plugin'), {
  option: 'value'
})

Options #

Option Type Default Description
option string - Description

License #

MIT

text

## 十、最佳实践

### 10.1 插件命名

```javascript
module.exports = fp(myPlugin, {
  name: '@myorg/fastify-mysql'
})

10.2 错误处理 #

javascript
async function myPlugin(fastify, options) {
  try {
    const resource = await createResource(options)
    fastify.decorate('resource', resource)
  } catch (err) {
    fastify.log.error('Failed to initialize plugin:', err)
    throw err
  }
}

10.3 日志记录 #

javascript
async function myPlugin(fastify, options) {
  fastify.log.info('Initializing plugin...')
  
  const resource = await createResource(options)
  fastify.decorate('resource', resource)
  
  fastify.log.info('Plugin initialized successfully')
}

十一、总结 #

本章我们学习了:

  1. 插件基础:最简插件、fastify-plugin、插件选项
  2. 装饰器模式:装饰实例、Request、Reply
  3. 钩子集成:请求钩子、认证钩子、错误处理
  4. 服务封装:数据库服务、缓存服务、邮件服务
  5. 配置验证:必需选项、选项Schema、默认值
  6. 插件依赖:声明依赖、可选依赖
  7. 资源管理:连接管理、连接池
  8. 插件测试:基本测试、钩子测试、错误测试
  9. 发布插件:package.json、README
  10. 最佳实践:命名、错误处理、日志记录

接下来让我们学习插件作用域与封装!

最后更新:2026-03-28