プラグイン入門ガイド
まず第一に、DON'T PANIC
!
Fastifyは、最初から極めてモジュール化されたシステムとして構築されました。名前空間を作成することで、Fastifyにメソッドとユーティリティを追加できる強力なAPIを構築しました。また、カプセル化モデルを作成するシステムを構築しており、アプリケーション全体をリファクタリングする必要なく、いつでもアプリケーションを複数のマイクロサービスに分割できます。
目次
登録
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
を呼び出すだけで、ユーティリティにアクセスできます(テスト内でも)。
そして、魔法が始まります。先ほどカプセル化について話していたことを覚えていますか?register
とdecorate
を組み合わせて使用することで、まさにそれが実現します。これを明確にするために例を示しましょう。
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()
})
2番目の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ではありません。decorateRequest
とdecorateReply
も使用できます。
decorateRequest
とdecorateReply
?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
キーワードを使用できないことを思い出してください。そのため、decorateReply
とdecorateReply
にrequest
とreply
インスタンスにもアクセスする必要があるユーティリティとして関数を渡す場合、アロー関数式ではなく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()
})
この方法は、次のセクションで説明するように、プラグインを配布する予定がある場合に非常に役立ちます。
今頃気づいたかもしれませんが、request
とreply
は標準のNode.jsのrequestとresponseオブジェクトではなく、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)
特定のAPIが必要な場合は、fastify-plugin
にFastifyのインストール済みバージョンを確認するように指示することもできます。
前述のように、Fastifyは.listen()
、.inject()
、または.ready()
が呼び出された**後**、つまり宣言された**後**にプラグインのロードを開始します。これは、プラグインがdecorate
を介して外部Fastifyインスタンスに変数を挿入する場合でも、デコレートされた変数には.listen()
、.inject()
、または.ready()
を呼び出す前にアクセスできないことを意味します。
先行するプラグインによって挿入された変数に依存し、それをregister
のoptions
引数に渡す必要がある場合は、オブジェクトではなく関数を使用できます。
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
の2番目の引数として渡された関数のparent
変数は、プラグインが登録された外部Fastifyインスタンスのコピーです。つまり、宣言の順序で先行するプラグインによって挿入された変数にアクセスできます。
ESMサポート
ESMはNode.js v13.3.0
以降でもサポートされています!プラグインをESMモジュールとしてエクスポートするだけで完了です!
// plugin.mjs
async function plugin (fastify, opts) {
fastify.get('/', async (req, reply) => {
return { hello: 'world' }
})
}
export default plugin
エラー処理
起動中にプラグインが失敗する可能性があります。おそらくそれを予想しており、その場合にトリガーされるカスタムロジックがあります。どのように実装できますか?after
APIが必要です。after
は、登録の直後に実行されるコールバックを登録するだけで、最大3つのパラメーターを取ることができます。
コールバックは、指定するパラメーターによって変化します。
- コールバックにパラメーターを指定せず、エラーがある場合、そのエラーは次のエラーハンドラーに渡されます。
- コールバックに1つのパラメーターを指定した場合、そのパラメーターはエラーオブジェクトになります。
- コールバックに2つのパラメーターを指定した場合、最初のものはエラーオブジェクト、2番目のものはdoneコールバックになります。
- コールバックに3つのパラメーターを指定した場合、最初のものはエラーオブジェクト、2番目のものはサーバーとオーバーライドの両方を指定していない限りトップレベルのコンテキストであり、その場合はコンテキストはオーバーライドが返すものになり、3番目のものは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
Fastify向けのテンプレートレンダリング(ejs、pug、handlebars、marko)プラグイン。@fastify/mongodb
Fastify MongoDB接続プラグイン。これを使用すると、サーバーのすべての部分で同じMongoDB接続プールを共有できます。@fastify/multipart
Fastifyのマルチパートサポート。@fastify/helmet
Fastifyの重要なセキュリティヘッダー。
何か不足していると感じますか?お知らせください! :)