跳至主要内容
版本:最新版本 (v5.0.x)

插件入门指南

首先,不要惊慌

Fastify 从一开始就被设计成一个极其模块化的系统。我们构建了一个强大的 API,允许您通过创建命名空间来向 Fastify 添加方法和实用程序。我们构建了一个创建封装模型的系统,允许您随时将应用程序拆分为多个微服务,而无需重构整个应用程序。

目录

注册

与 JavaScript 中一切皆为对象一样,在 Fastify 中一切皆为插件。

您的路由、实用程序等等都是插件。要添加一个新的插件,无论其功能是什么,在 Fastify 中您都有一个简洁独特的 API:register

fastify.register(
require('./my-plugin'),
{ options }
)

register 创建一个新的 Fastify 上下文,这意味着如果您对 Fastify 实例进行任何更改,这些更改将不会反映在上下文的祖先中。换句话说,就是封装!

为什么封装很重要?

好吧,假设您正在创建一家新的颠覆性创业公司,您会怎么做?您会创建一个包含所有内容的 API 服务器,所有内容都在同一个地方,一个单体应用!

好的,您的业务发展很快,您想更改架构并尝试微服务。通常,这需要大量的工作,因为代码库中存在交叉依赖关系和关注点分离不足。

Fastify 在这方面可以帮助您。由于封装模型,它将完全避免交叉依赖关系,并帮助您将代码构建成内聚的块。

让我们回到如何正确使用 register

如您所知,必需的插件必须公开一个具有以下签名的单个函数

module.exports = function (fastify, options, done) {}

其中 fastify 是封装的 Fastify 实例,options 是选项对象,done 是您在插件准备就绪时**必须**调用的函数。

Fastify 的插件模型是完全可重入的和基于图的,它可以毫无问题地处理异步代码,并且强制执行插件的加载和关闭顺序。如何做到? 很高兴您提出这个问题,请查看 avvio!Fastify 在调用 .listen().inject().ready() **之后**开始加载插件。

在插件内部,您可以做任何您想做的事情,注册路由、实用程序(我们稍后会看到)并进行嵌套注册,只需记住在一切设置完成后调用 done

module.exports = function (fastify, options, done) {
fastify.get('/plugin', (request, reply) => {
reply.send({ hello: 'world' })
})

done()
}

好吧,现在您知道如何使用 register API 及其工作原理了,但是我们如何向 Fastify 添加新功能,甚至更好地与其他开发人员共享它们呢?

装饰器

好的,假设您编写了一个非常棒的实用程序,您决定将其与所有代码一起提供。您将如何做到这一点?可能是以下类似方法

// your-awesome-utility.js
module.exports = function (a, b) {
return a + b
}
const util = require('./your-awesome-utility')
console.log(util('that is ', 'awesome'))

现在,您将在需要它的每个文件中导入您的实用程序。(并且不要忘记您可能还需要在测试中使用它)。

Fastify 为您提供了一种更优雅、更便捷的方法来实现这一点,即装饰器。创建装饰器非常简单,只需使用 decorate API

fastify.decorate('util', (a, b) => a + b)

现在,您只需在需要时调用 fastify.util 即可访问您的实用程序——甚至在您的测试中也是如此。

然后就开始了神奇的时刻;您还记得我们刚才谈论的封装吗?好吧,结合使用 registerdecorate 正好可以实现这一点,让我举个例子来说明这一点

fastify.register((instance, opts, done) => {
instance.decorate('util', (a, b) => a + b)
console.log(instance.util('that is ', 'awesome'))

done()
})

fastify.register((instance, opts, done) => {
console.log(instance.util('that is ', 'awesome')) // This will throw an error

done()
})

在第二个 register 调用中,instance.util 将抛出错误,因为 util 仅存在于第一个 register 上下文中。

让我们退一步,更深入地探讨一下:每次使用 register API 时,都会创建一个新的上下文,从而避免上述负面情况。

请注意,封装适用于祖先和兄弟节点,但不适用于子节点。

fastify.register((instance, opts, done) => {
instance.decorate('util', (a, b) => a + b)
console.log(instance.util('that is ', 'awesome'))

fastify.register((instance, opts, done) => {
console.log(instance.util('that is ', 'awesome')) // This will not throw an error
done()
})

done()
})

fastify.register((instance, opts, done) => {
console.log(instance.util('that is ', 'awesome')) // This will throw an error

done()
})

关键信息:如果您需要一个在应用程序的每个部分都可用的实用程序,请确保它是在应用程序的根作用域中声明的。如果这不是一种选择,您可以使用 此处所述的 fastify-plugin 实用程序。

decorate 不是您可以用来扩展服务器功能的唯一 API,您还可以使用 decorateRequestdecorateReply

decorateRequestdecorateReply?如果我们已经有 decorate 了,为什么还需要它们?

问得好,我们添加它们是为了使 Fastify 更易于开发者使用。让我们看一个例子

fastify.decorate('html', payload => {
return generateHtml(payload)
})

fastify.get('/html', (request, reply) => {
reply
.type('text/html')
.send(fastify.html({ hello: 'world' }))
})

它可以工作,但可以做得更好!

fastify.decorateReply('html', function (payload) {
this.type('text/html') // This is the 'Reply' object
this.send(generateHtml(payload))
})

fastify.get('/html', (request, reply) => {
reply.html({ hello: 'world' })
})

提醒一下,箭头函数上没有 this 关键字,因此,当在decorateReplydecorateRequest 中传递函数作为也需要访问 requestreply 实例的实用程序时,需要使用 function 关键字定义的函数,而不是箭头函数表达式

同样,您可以对 request 对象执行此操作

fastify.decorate('getHeader', (req, header) => {
return req.headers[header]
})

fastify.addHook('preHandler', (request, reply, done) => {
request.isHappy = fastify.getHeader(request.raw, 'happy')
done()
})

fastify.get('/happiness', (request, reply) => {
reply.send({ happy: request.isHappy })
})

同样,它可以工作,但可以做得更好!

fastify.decorateRequest('setHeader', function (header) {
this.isHappy = this.headers[header]
})

fastify.decorateRequest('isHappy', false) // This will be added to the Request object prototype, yay speed!

fastify.addHook('preHandler', (request, reply, done) => {
request.setHeader('happy')
done()
})

fastify.get('/happiness', (request, reply) => {
reply.send({ happy: request.isHappy })
})

我们已经了解了如何扩展服务器功能以及如何处理封装系统,但是如果您需要添加一个必须在服务器“发出”事件时执行的函数该怎么办?

钩子

您刚刚构建了一个很棒的实用程序,但现在您需要为每个请求执行它,这可能是您会做的事情

fastify.decorate('util', (request, key, value) => { request[key] = value })

fastify.get('/plugin1', (request, reply) => {
fastify.util(request, 'timestamp', new Date())
reply.send(request)
})

fastify.get('/plugin2', (request, reply) => {
fastify.util(request, 'timestamp', new Date())
reply.send(request)
})

我想我们都同意这很糟糕。重复的代码、糟糕的可读性,而且无法扩展。

那么,如何避免这个恼人的问题呢?是的,您是对的,使用 钩子

fastify.decorate('util', (request, key, value) => { request[key] = value })

fastify.addHook('preHandler', (request, reply, done) => {
fastify.util(request, 'timestamp', new Date())
done()
})

fastify.get('/plugin1', (request, reply) => {
reply.send(request)
})

fastify.get('/plugin2', (request, reply) => {
reply.send(request)
})

现在,对于每个请求,您都将运行您的实用程序。您可以注册任意数量的钩子。

有时您希望一个钩子仅对一部分路由执行,您如何做到这一点?是的,封装!

fastify.register((instance, opts, done) => {
instance.decorate('util', (request, key, value) => { request[key] = value })

instance.addHook('preHandler', (request, reply, done) => {
instance.util(request, 'timestamp', new Date())
done()
})

instance.get('/plugin1', (request, reply) => {
reply.send(request)
})

done()
})

fastify.get('/plugin2', (request, reply) => {
reply.send(request)
})

现在,您的钩子只会在第一个路由上运行!

另一种方法是利用 onRoute 钩子从插件内部动态自定义应用程序路由。每次注册新路由时,您都可以读取和修改路由选项。例如,基于 路由配置选项

fastify.register((instance, opts, done) => {
instance.decorate('util', (request, key, value) => { request[key] = value })

function handler(request, reply, done) {
instance.util(request, 'timestamp', new Date())
done()
}

instance.addHook('onRoute', (routeOptions) => {
if (routeOptions.config && routeOptions.config.useUtil === true) {
// set or add our handler to the route preHandler hook
if (!routeOptions.preHandler) {
routeOptions.preHandler = [handler]
return
}
if (Array.isArray(routeOptions.preHandler)) {
routeOptions.preHandler.push(handler)
return
}
routeOptions.preHandler = [routeOptions.preHandler, handler]
}
})

fastify.get('/plugin1', {config: {useUtil: true}}, (request, reply) => {
reply.send(request)
})

fastify.get('/plugin2', (request, reply) => {
reply.send(request)
})

done()
})

如果您计划分发您的插件,如下一节所述,此变体将变得非常有用。

您可能已经注意到,requestreply 不是标准的 Node.js requestresponse 对象,而是 Fastify 的对象。

如何处理封装和分发

完美,现在您(几乎)了解了可以用来扩展 Fastify 的所有工具。但是,您很可能遇到一个大问题:如何处理分发?

分发实用程序的首选方法是将所有代码包装在 register 中。使用此方法,您的插件可以支持异步引导(因为 decorate 是同步 API),例如在数据库连接的情况下。

等等,您不是告诉我 register 会创建封装,并且我在其中创建的内容在外部不可用吗?

是的,我确实说过。但是,我没有告诉您的是,您可以使用 fastify-plugin 模块告诉 Fastify 避免此行为。

const fp = require('fastify-plugin')
const dbClient = require('db-client')

function dbPlugin (fastify, opts, done) {
dbClient.connect(opts.url, (err, conn) => {
fastify.decorate('db', conn)
done()
})
}

module.exports = fp(dbPlugin)

您还可以告诉 fastify-plugin 检查已安装的 Fastify 版本,以防您需要特定的 API。

如前所述,Fastify 在调用 .listen().inject().ready() **之后**开始加载其插件,并且因此在它们被声明**之后**。这意味着,即使插件可能通过 decorate 将变量注入到外部 Fastify 实例中,在调用 .listen().inject().ready() 之前也无法访问已装饰的变量。

如果您依赖于前面插件注入的变量,并且想要将其传递到 registeroptions 参数中,您可以使用函数而不是对象来做到这一点

const fastify = require('fastify')()
const fp = require('fastify-plugin')
const dbClient = require('db-client')

function dbPlugin (fastify, opts, done) {
dbClient.connect(opts.url, (err, conn) => {
fastify.decorate('db', conn)
done()
})
}

fastify.register(fp(dbPlugin), { url: 'https://example.com' })
fastify.register(require('your-plugin'), parent => {
return { connection: parent.db, otherOption: 'foo-bar' }
})

在上面的示例中,作为 register 的第二个参数传递的函数的 parent 变量是插件注册到的外部 Fastify 实例的副本。这意味着我们可以根据声明顺序访问前面插件注入的任何变量。

ESM 支持

Node.js v13.3.0 及更高版本开始也支持 ESM!只需将您的插件导出为 ESM 模块,就可以开始了!

// plugin.mjs
async function plugin (fastify, opts) {
fastify.get('/', async (req, reply) => {
return { hello: 'world' }
})
}

export default plugin

处理错误

您的某个插件在启动期间可能会失败。也许您预期会出现这种情况,并且您有一个将在这种情况下触发的自定义逻辑。您如何实现这一点?您需要使用 after API。after 只需注册一个将在注册后立即执行的回调,并且最多可以接受三个参数。

根据您提供的参数,回调会发生变化

  1. 如果回调没有参数,并且存在错误,则该错误将传递给下一个错误处理程序。
  2. 如果回调提供一个参数,则该参数将是错误对象。
  3. 如果回调提供两个参数,则第一个将是错误对象;第二个是 done 回调。
  4. 如果回调提供三个参数,则第一个将是错误对象,第二个将是顶级上下文,除非您同时指定了服务器和覆盖,在这种情况下,上下文将是覆盖返回的内容,第三个是 done 回调。

让我们看看如何使用它

fastify
.register(require('./database-connector'))
.after(err => {
if (err) throw err
})

自定义错误

如果您的插件需要公开自定义错误,您可以使用 @fastify/error 模块轻松地在整个代码库和插件中生成一致的错误对象。

const createError = require('@fastify/error')
const CustomError = createError('ERROR_CODE', 'message')
console.log(new CustomError())

发出警告

如果您想弃用某个 API,或者想警告用户某个特定用例,您可以使用process-warning 模块。

const warning = require('process-warning')()
warning.create('MyPluginWarning', 'MP_ERROR_CODE', 'message')
warning.emit('MP_ERROR_CODE')

让我们开始吧!

太棒了,现在您已经了解了有关 Fastify 及其插件系统的所有必要知识,可以开始构建您的第一个插件了,如果您确实构建了插件,请告诉我们!我们会将其添加到我们文档的生态系统 部分!

如果您想查看一些实际示例,请查看

  • @fastify/view 模板渲染 (ejs、pug、handlebars、marko) 插件支持 Fastify。
  • @fastify/mongodb Fastify MongoDB 连接插件,使用它,您可以在服务器的每个部分共享相同的 MongoDB 连接池。
  • @fastify/multipart Fastify 的 Multipart 支持
  • @fastify/helmet Fastify 的重要安全头信息

您觉得这里缺少什么吗?请告诉我们!:)