Skip to main content

插件指南

¥The hitchhiker's guide to plugins

首先,DON'T PANIC

¥First of all, DON'T PANIC!

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

¥Fastify was built from the beginning to be an extremely modular system. We built a powerful API that allows you to add methods and utilities to Fastify by creating a namespace. We built a system that creates an encapsulation model, which allows you to split your application into multiple microservices at any moment, without the need to refactor the entire application.

目录

¥Table of contents

注册

¥Register

与 JavaScript 一样,一切都是对象,在 Fastify 中一切都是插件。

¥As with JavaScript, where everything is an object, in Fastify everything is a plugin.

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

¥Your routes, your utilities, and so on are all plugins. To add a new plugin, whatever its functionality may be, in Fastify you have a nice and unique API: register.

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

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

¥register creates a new Fastify context, which means that if you perform any changes on the Fastify instance, those changes will not be reflected in the context's ancestors. In other words, encapsulation!

为什么封装很重要?

¥Why is encapsulation important?

好吧,假设你正在创建一家新的颠覆性初创公司,你会做什么?你创建一个 API 服务器,其中包含你的所有内容,所有内容都位于同一个位置,是一个整体!

¥Well, let's say you are creating a new disruptive startup, what do you do? You create an API server with all your stuff, everything in the same place, a monolith!

好吧,你成长得非常快,你想改变你的架构并尝试微服务。通常,由于交叉依赖和代码库中缺乏关注点分离,这意味着大量的工作。

¥Ok, you are growing very fast and you want to change your architecture and try microservices. Usually, this implies a huge amount of work, because of cross dependencies and a lack of separation of concerns in the codebase.

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

¥Fastify helps you in that regard. Thanks to the encapsulation model, it will completely avoid cross dependencies and will help you structure your code into cohesive blocks.

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

¥Let's return to how to correctly use register.

你可能知道,所需的插件必须公开具有以下签名的单个函数

¥As you probably know, the required plugins must expose a single function with the following signature

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

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

¥Where fastify is the encapsulated Fastify instance, options is the options object, and done is the function you must call when your plugin is ready.

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

¥Fastify's plugin model is fully reentrant and graph-based, it handles asynchronous code without any problems and it enforces both the load and close order of plugins. How? Glad you asked, check out avvio! Fastify starts loading the plugin after .listen(), .inject() or .ready() are called.

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

¥Inside a plugin you can do whatever you want, register routes, utilities (we will see this in a moment) and do nested registers, just remember to call done when everything is set up!

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

done()
}

好了,现在你知道了如何使用 register API 以及它是如何工作的,但是我们如何向 Fastify 添加新功能,甚至更好地与其他开发者共享它们?

¥Well, now you know how to use the register API and how it works, but how do we add new functionality to Fastify and even better, share them with other developers?

装饰器

¥Decorators

好吧,假设你编写了一个非常好的实用程序,以至于你决定将其与所有代码一起提供。你会怎么做?大概是这样的:

¥Okay, let's say that you wrote a utility that is so good that you decided to make it available along with all your code. How would you do it? Probably something like the following:

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

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

¥Now you will import your utility in every file you need it in. (And do not forget that you will probably also need it in your tests).

Fastify 为你提供了一种更优雅、更舒适的方式来实现这一点,装饰器。创建装饰器非常简单,只需使用 decorate API:

¥Fastify offers you a more elegant and comfortable way to do this, decorators. Creating a decorator is extremely easy, just use the decorate API:

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

现在你只需在需要时调用 fastify.util 即可访问你的实用程序 - 甚至在你的测试中。

¥Now you can access your utility just by calling fastify.util whenever you need it - even inside your test.

魔法就从这里开始了;你还记得我们刚才谈论封装吗?好吧,结合使用 registerdecorate 可以实现这一点,让我向你展示一个例子来澄清这一点:

¥And here starts the magic; do you remember how just now we were talking about encapsulation? Well, using register and decorate in conjunction enable exactly that, let me show you an example to clarify this:

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()
})

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

¥Inside the second register call instance.util will throw an error because util exists only inside the first register context.

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

¥Let's step back for a moment and dig deeper into this: every time you use the register API, a new context is created which avoids the negative situations mentioned above.

请注意,封装适用于祖级和兄弟姐妹,但不适用于子级。

¥Do note that encapsulation applies to the ancestors and siblings, but not the children.

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 实用程序,如 此处 所述。

¥Take home message: if you need a utility that is available in every part of your application, take care that it is declared in the root scope of your application. If that is not an option, you can use the fastify-plugin utility as described here.

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

¥decorate is not the only API that you can use to extend the server functionality, you can also use decorateRequest and decorateReply.

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

¥decorateRequest and decorateReply? Why do we need them if we already have decorate?

好问题,我们添加它们是为了让 Fastify 对开发者更加友好。让我们看一个例子:

¥Good question, we added them to make Fastify more developer-friendly. Let's see an example:

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

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

它有效,但还可以更好!

¥It works, but it could be much better!

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 关键字定义的函数而不是箭头函数表达式。

¥Reminder that the this keyword is not available on arrow functions, so when passing functions in decorateReply and decorateRequest as a utility that also needs access to the request and reply instance, a function that is defined using the function keyword is needed instead of an arrow function expression.

你可以以同样的方式对 request 对象执行此操作:

¥In the same way you can do this for the request object:

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 })
})

再说一次,它有效,但还可以更好!

¥Again, it works, but it can be much better!

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 })
})

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

¥We have seen how to extend server functionality and how to handle the encapsulation system, but what if you need to add a function that must be executed whenever the server "emits" an event?

钩子

¥Hooks

你刚刚构建了一个令人惊叹的实用程序,但现在你需要为每个请求执行该实用程序,这就是你可能要做的事情:

¥You just built an amazing utility, but now you need to execute that for every request, this is what you will likely do:

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)
})

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

¥I think we all agree that this is terrible. Repeated code, awful readability and it cannot scale.

那么你可以做什么来避免这个恼人的问题呢?是的,你是对的,使用 hook

¥So what can you do to avoid this annoying issue? Yes, you are right, use a hook!

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)
})

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

¥Now for every request, you will run your utility. You can register as many hooks as you need.

有时你想要一个仅针对路由子集执行的钩子,如何才能做到这一点?是的,封装!

¥Sometimes you want a hook that should be executed for just a subset of routes, how can you do that? Yep, encapsulation!

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)
})

现在你的钩子将只运行第一条路由!

¥Now your hook will run just for the first route!

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

¥An alternative approach is to make use of the onRoute hook to customize application routes dynamically from inside the plugin. Every time a new route is registered, you can read and modify the route options. For example, based on a route config option:

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()
})

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

¥This variant becomes extremely useful if you plan to distribute your plugin, as described in the next section.

你现在可能已经注意到,requestreply 不是标准的 Node.js 请求和响应对象,而是 Fastify 的对象。

¥As you probably noticed by now, request and reply are not the standard Node.js request and response objects, but Fastify's objects.

如何处理封装和分发

¥How to handle encapsulation and distribution

太棒了,现在你(几乎)了解了可用于扩展 Fastify 的所有工具。尽管如此,你很可能遇到一个大问题:分配是如何处理的?

¥Perfect, now you know (almost) all of the tools that you can use to extend Fastify. Nevertheless, chances are that you came across one big issue: how is distribution handled?

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

¥The preferred way to distribute a utility is to wrap all your code inside a register. Using this, your plugin can support asynchronous bootstrapping (since decorate is a synchronous API), in the case of a database connection for example.

等等,什么?你不是告诉我 register 创建了一个封装,而我在里面创建的东西在外面是无法使用的吗?

¥Wait, what? Didn't you tell me that register creates an encapsulation and that the stuff I create inside will not be available outside?

是的,我就是这么说的。但是,我没有告诉你的是,你可以告诉 Fastify 使用 fastify-plugin 模块避免这种行为。

¥Yes, I said that. However, what I didn't tell you is that you can tell Fastify to avoid this behavior with the fastify-plugin module.

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。

¥You can also tell fastify-plugin to check the installed version of Fastify, in case you need a specific API.

正如我们前面提到的,Fastify 在调用 .listen().inject().ready() 之后开始加载其插件,因此,在声明它们之后。这意味着,即使插件可以通过 decorate 将变量注入外部 Fastify 实例,在调用 .listen().inject().ready() 之前,修饰的变量将无法访问。

¥As we mentioned earlier, Fastify starts loading its plugins after .listen(), .inject() or .ready() are called and as such, after they have been declared. This means that, even though the plugin may inject variables to the external Fastify instance via decorate, the decorated variables will not be accessible before calling .listen(), .inject() or .ready().

如果你依赖于前面的插件注入的变量并希望将其传递给 registeroptions 参数,你可以使用函数而不是对象来实现:

¥In case you rely on a variable injected by a preceding plugin and want to pass that in the options argument of register, you can do so by using a function instead of an object:

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 实例的副本。这意味着我们可以访问前面插件按声明顺序注入的任何变量。

¥In the above example, the parent variable of the function passed in as the second argument of register is a copy of the external Fastify instance that the plugin was registered at. This means that we can access any variables that were injected by preceding plugins in the order of declaration.

无害化管理支持

¥ESM support

Node.js v13.3.0 及以上版本也支持 ESM!只需将你的插件导出为 ESM 模块即可开始!

¥ESM is supported as well from Node.js v13.3.0 and above! Just export your plugin as an ESM module and you are good to go!

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

export default plugin

处理错误

¥Handle errors

你的插件之一可能在启动过程中失败。也许你期望如此,并且你有一个在这种情况下会触发的自定义逻辑。你如何实现这一点?after API 就是你所需要的。after 只是注册一个回调,该回调将在注册后立即执行,它最多可以接受三个参数。

¥One of your plugins may fail during startup. Maybe you expect it and you have a custom logic that will be triggered in that case. How can you implement this? The after API is what you need. after simply registers a callback that will be executed just after a register, and it can take up to three parameters.

回调根据你提供的参数而变化:

¥The callback changes based on the parameters you are giving:

  1. 如果没有为回调提供任何参数并且出现错误,则该错误将被传递到下一个错误处理程序。

    ¥If no parameter is given to the callback and there is an error, that error will be passed to the next error handler.

  2. 如果为回调提供一个参数,则该参数将是错误对象。

    ¥If one parameter is given to the callback, that parameter will be the error object.

  3. 如果回调函数有两个参数,第一个参数是错误对象;第二个是完成回调。

    ¥If two parameters are given to the callback, the first will be the error object; the second will be the done callback.

  4. 如果为回调提供了三个参数,第一个将是错误对象,第二个将是顶层上下文,除非你同时指定了服务器和覆盖,在这种情况下,上下文将是覆盖返回的内容,并且 第三个完成回调。

    ¥If three parameters are given to the callback, the first will be the error object, the second will be the top-level context unless you have specified both server and override, in that case, the context will be what the override returns, and the third the done callback.

让我们看看如何使用它:

¥Let's see how to use it:

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

自定义错误

¥Custom errors

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

¥If your plugin needs to expose custom errors, you can easily generate consistent error objects across your codebase and plugins with the @fastify/error module.

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

发出警告

¥Emit Warnings

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

¥If you want to deprecate an API, or you want to warn the user about a specific use case, you can use the process-warning module.

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

开始吧!

¥Let's start!

太棒了,现在你已经了解了有关 Fastify 及其插件系统的所有信息,可以开始构建你的第一个插件了,如果你知道了,请告诉我们!我们将把它添加到我们文档的 ecosystem 部分!

¥Awesome, now you know everything you need to know about Fastify and its plugin system to start building your first plugin, and please if you do, tell us! We will add it to the ecosystem section of our documentation!

如果你想查看一些现实世界的示例,请查看:

¥If you want to see some real-world examples, check out:

  • @fastify/view 模板渲染(ejs、pug、handlebars、marko)插件支持 Fastify。

    ¥@fastify/view Templates rendering (ejs, pug, handlebars, marko) plugin support for Fastify.

  • @fastify/mongodb Fastify MongoDB 连接插件,使用它你可以在服务器的每个部分共享相同的 MongoDB 连接池。

    ¥@fastify/mongodb Fastify MongoDB connection plugin, with this you can share the same MongoDB connection pool in every part of your server.

  • @fastify/multipart Fastify 的多部分支持

    ¥@fastify/multipart Multipart support for Fastify

  • @fastify/helmet Fastify 的重要安全标头

    ¥@fastify/helmet Important security headers for Fastify

你有没有觉得这里少了点什么?让我们知道!:)

¥Do you feel like something is missing here? Let us know! :)