Fastify の autoload で指定範囲内のプラグインを作る
◆ /foo/bar の中でだけ使える機能を提供するプラグインを作りたい
◆ テンプレートの autoload 方法だとプラグインは全体が対象になる
◆ 特定のプレフィックスの中でカプセル化できない
◆ autoload をネストするか autohooks を使うとできるけど工夫が必要になる
◆ テンプレートの autoload 方法だとプラグインは全体が対象になる
◆ 特定のプレフィックスの中でカプセル化できない
◆ autoload をネストするか autohooks を使うとできるけど工夫が必要になる
Fastify CLI のテンプレート
Fastify CLI のテンプレートで作ると こういうフォルダ構造になりますpackage.json
app.js
plugins/
plugin1.js
routes/
route1.js
app.js は Fastify CLI からロードされるプラグインで 中の処理は plugins と routes フォルダを autoload に登録するものです
autoload の登録のところは触らないで というコメントが書かれてるくらいなのでこのまま使うのが推奨されてるようです
autoload の登録の手前ではユーザーがコードを追加してカスタムできる場所はあるようですが ここでなにかするならプラグインを作ればいいと思うので基本 app.js は触らないものという扱いで良いと思います
https://github.com/fastify/fastify-cli/blob/v6.1.1/templates/app-esm/app.js
export default async function (fastify, opts) {
// Place here your custom code!
// Do not touch the following lines
// This loads all plugins defined in plugins
// those should be support plugins that are reused
// through your application
fastify.register(AutoLoad, {
dir: path.join(__dirname, 'plugins'),
options: Object.assign({}, opts)
})
// This loads all plugins defined in routes
// define your routes in one of these
fastify.register(AutoLoad, {
dir: path.join(__dirname, 'routes'),
options: Object.assign({}, opts)
})
}
プラグインは plugins フォルダで ルート定義は routes フォルダ内に配置する感じになっています
この中に置いた .js ファイルが自動で読み込まれます
プラグインは fp (fastify-plugin) を使うことが推奨されています
fp を使うとカプセル化されません
親にカプセル化される部分がないのでグローバルなプラグインになります
反対にルートの方は fp を使わないのでカプセル化されます
困るところ
そうなると特定の範囲のルートでだけ有効なプラグインを作りたい場合に困るように思います例えば /api/v1 以下のみ fastify.db にコネクションを保持して また /api/v2 なら別のコネクションを保持したいとかあると思います
ですがこの autoload のしくみだと plugins の中で fastify.decorate すると全体に影響します
routes の routes/api/v1 で fastify.decorate すると そのファイル内で定義したルートでは有効ですが routes/api/v1/foo など別のファイルのプラグインになるとそこでは有効ではないです
プラグインの登録では ルート単位でのカプセル化ではなく register メソッドの呼び出しのネストでカプセル化されるので 独立して register するとルートが同じでも別物になるのですよね
plugins/api/v1.js を fp なしで作ったとしても ここで追加したプロパティを routes/api/v1.js 側で使えません
routes/api/v1.js で /api/v1 以下のルートをすべて定義するなら問題ないです
export default async (fastify, opts) => {
fastify.decorate("db", createConnection(opts))
// /api/v1/foo.js
fastify.get("/foo", async () => {
// ...
})
// /api/v1/bar.js
fastify.get("/bar", async () => {
// ...
})
// ...
}
ですが ルートの数が多く ほとんどのルートがこの中にある場合 routes フォルダの autoload があまり意味のないものになってしまいます
やりたいこと
autoload プラグインを使わず自分でプラグインを register していくときのように 指定範囲内でのみ有効なプラグインを使えるようにしたいですとはいえ
/
/foo/bar/baz
/baz/foo
でだけ共有とかするのは複雑ですし 実際にそれが必要なケースもあまりないと思います
なのでルートを基準にして /foo/bar/ 以下のみ有効みたいなものができればよいことにします
例)
import Fastify from "fastify"
import fp from "fastify-plugin"
const fastify = Fastify()
await fastify.register(fp(async (fastify, opts) => {
fastify.decorate("value1", "VALUE")
}))
await fastify.register(async (fastify, opts) => {
await fastify.register(async (fastify, opts) => {
await fastify.register(fp(async (fastify, opts) => {
fastify.decorate("value2", "VALUE")
}))
await fastify.register(async (fastify, opts) => {
fastify.get("/route1", async (request, reply) => {
console.log("route1", fastify.value1, fastify.value2) // VALUE VALUE
// ...
})
}, { prefix: "/baz" })
fastify.get("/route2", async (request, reply) => {
console.log("route2", fastify.value1, fastify.value2) // VALUE VALUE
// ...
})
fastify.get("/route3", async (request, reply) => {
console.log("route3", fastify.value1, fastify.value2) // VALUE VALUE
// ...
})
}, { prefix: "/bar" })
fastify.get("/route4", async (request, reply) => {
console.log("route4", fastify.value1, fastify.value2) // VALUE undefined
// ...
})
}, { prefix: "/foo" })
fastify.get("/route5", async (request, reply) => {
console.log("route5", fastify.value1, fastify.value2) // VALUE undefined
// ...
})
await fastify.inject({ method: "GET", url: "/foo/bar/baz/route1" })
await fastify.inject({ method: "GET", url: "/foo/bar/route2" })
await fastify.inject({ method: "GET", url: "/foo/bar/route3" })
await fastify.inject({ method: "GET", url: "/foo/route4" })
await fastify.inject({ method: "GET", url: "/route5" })
これと同じものを作れればおっけいとします
autoload のネスト
上でも書いたように Fastify のカプセル化は register のネストで制御されるものですautoload で別々にロードされるファイルだとどうしようもないと思うので autoload をネストするようにしようと思います
routes/foo/bar でカプセル化されたコンテキストの中で autoload の登録を行います
そうすればその中の plugins と routes は /foo/bar 以下でのみ有効になるはずです
app.js : autoload を設定
→ plugins : グローバルのプラグイン
→ routes : グローバルのルート定義
→ foo/bar : ここで autoload を設定
→ foo/bar/plugins : /foo/bar 内のプラグイン
→ foo/bar/routes : /foo/bar 内のルート定義
みたいなイメージです
ですが app.js で設定した autoload は foo/bar/plugins や foo/bar/routes もグローバルなものとしてロードしてしまいます
autoload 機能では index.js があるとそのフォルダ内の autoload は index.js だけになるらしいので routes/foo/bar/index.js を置いてみます
これで routes/foo/bar/plugins や routes/foo/bar/routes が無視されてくれればよかったのですがなぜかロードされてしまいました
index.js と同じ階層の .js ファイルはスキップされてるようなのですが サブディレクトリになるとまたロードされてしまうようです
index.js がそのサブディレクトリからモジュールを import とか普通にありそうなので バグか考慮不足ではと思ったのですがドキュメントを見るとこれが意図したものになってそうです
「In that case only the index file (and the potential sub-directories) will be loaded.」
対策(1)
app.js があるフォルダに別の plugins や routes フォルダも置いて そこを autoload に設定することもできますpackage.json
app.js
plugins/
plugin1.js
routes/
foo/
bar/
index.js
foo_bar-plugins/
plugin2.js
foo_bar-routes/
route2.js
routes/foo/bar/index.js が foo_bar-plugins と foo_bar-routes を autoload に設定する形です
でもこれって foo_bar に当たる部分がいっぱい出てくると管理が大変です
もう少し工夫もできますが ツリーが別になるのは微妙な感じがします
package.json
app.js
plugins/
plugin1.js
routes/
foo/
bar/
index.js
mount/
foo/
bar/
plugins/
plugin2.js
routes/
route2.js
対策(2)
「index.js がある場合に その階層のフォルダを autoload の対象外にする」 というのは難しそうですが 無視するパターンを設定することはできます今回は plugins と routes というフォルダ名で固定なのでこれでやってみます
ignorePattern に /^(routes|plugins)$/ をつけてこれらのフォルダは除外します
fastify.register(AutoLoad, {
dir: path.join(dirname, "routes"),
options: Object.assign({}, opts),
ignorePattern: /^(routes|plugins)$/
})
package.json
app.js
plugins/
plugin1.js
routes/
foo/
bar/
index.js
plugins/
plugin2.js
routes/
route2.js
最終的に
対策(2) の方法を使って上に書いた例と同じのを実現してみますフォルダ構造はこうなります
.
|-- app.js
|-- autoload.js
|-- plugins
| `-- value1.js
`-- routes
|-- foo
| |-- bar
| | |-- index.js
| | |-- plugins
| | | `-- value2.js
| | `-- routes
| | |-- baz
| | | `-- route1.js
| | |-- route2.js
| | `-- route3.js
| `-- route4.js
`-- route5.js
app.js と routes/foo/bar/index.js がエクスポートするものは共通でこうします
export default async function (fastify, opts) {
autoload(fastify, import.meta.url, opts)
}
この autoload 関数は autoload.js がエクスポートするもので こういうものです
export default (fastify, base_url, options) => {
const dirname = toDirName(base_url)
const { prefix: _, ...opts } = options
// This loads all plugins defined in plugins
// those should be support plugins that are reused
// through your application
fastify.register(AutoLoad, {
dir: path.join(dirname, "plugins"),
options: Object.assign({}, opts)
})
// This loads all plugins defined in routes
// define your routes in one of these
fastify.register(AutoLoad, {
dir: path.join(dirname, "routes"),
options: Object.assign({}, opts),
ignorePattern: /^(routes|plugins)$/
})
}
デフォルトのテンプレートでこういうネストすることを想定しておいてほしいですが してないということはあまり推奨されなかったりするのでしょうか
書き換えないでって言われてる autoload の設定を書き換えてますし
ただ期待する動作にはなったのでとりあえずこれでいいかなと思います
autohooks
近そうなもので autoload のプラグインには autohooks という機能がありますこれを使うとフォルダ内の autohooks.js をロードして そのカプセル化範囲がフォルダ内のファイル全体になります
cascadeHooks を true にするとサブディレクトリでも有効になります
名前的に Fastify のフックを設定する用途みたいですけど ただのプラグインなので decorate も使えそうです
index.js の代わりに autohooks.js を使って そのルート以下で有効にしたい decorate などを行えば上でやったのと似たようなことができそうです
ただ autoload のように plugins と routes できれいに分けて書けないのはイマイチに感じます
また Fastify CLI のテンプレートでは有効になってないものです
個人的には autoload をネストする考え方の方が好きです
ソースコード
1 ファイルで手動 register するケースと autoload をネストするケースと autohooks を使うケースで実際に動くソースコードと結果はこんな感じですhttps://gitlab.com/nexpr/fastify-scoped-plugin
各方法で fastify インスタンスを作って printRoutes と printPlugins をした結果と それぞれのルートへアクセスして表示されるものをまとめています
実際の各ファイルはリポジトリの方を参照です
import Fastify from "fastify"
import manual from "./manual/app.js"
import nested_autoload from "./nested-autoload/app.js"
import autohooks from "./autohooks/app.js"
const run = async (app) => {
const fastify = Fastify()
await fastify.register(app)
console.log(fastify.printRoutes())
console.log(fastify.printPlugins())
await fastify.inject({ method: "GET", url: "/foo/bar/baz/route1" })
await fastify.inject({ method: "GET", url: "/foo/bar/route2" })
await fastify.inject({ method: "GET", url: "/foo/bar/route3" })
await fastify.inject({ method: "GET", url: "/foo/route4" })
await fastify.inject({ method: "GET", url: "/route5" })
}
console.log("\n==== manual ====\n")
await run(manual)
console.log("\n==== nested autoload ====\n")
await run(nested_autoload)
console.log("\n==== autohooks ====\n")
await run(autohooks)
出力
==== manual ====
└── /
├── foo/
│ ├── bar/
│ │ ├── baz/route1 (GET, HEAD)
│ │ └── route
│ │ ├── 2 (GET, HEAD)
│ │ └── 3 (GET, HEAD)
│ └── route4 (GET, HEAD)
└── route5 (GET, HEAD)
root -1 ms
├── bound _after 5 ms
├─┬ default 11 ms
│ ├── app-auto-0 0 ms
│ ├── bound _after 1 ms
│ ├─┬ foo 9 ms
│ │ ├─┬ bar 7 ms
│ │ │ ├── app-auto-1 0 ms
│ │ │ ├── bound _after 1 ms
│ │ │ ├─┬ baz 2 ms
│ │ │ │ ├── bound _after 1 ms
│ │ │ │ └── bound _after 0 ms
│ │ │ ├── bound _after 1 ms
│ │ │ ├── bound _after 0 ms
│ │ │ ├── bound _after 0 ms
│ │ │ ├── bound _after 0 ms
│ │ │ └── bound _after 0 ms
│ │ ├── bound _after 0 ms
│ │ ├── bound _after 0 ms
│ │ └── bound _after 0 ms
│ ├── bound _after 0 ms
│ ├── bound _after 0 ms
│ └── bound _after 0 ms
└── bound _after 0 ms
route1 VALUE VALUE
route2 VALUE VALUE
route3 VALUE VALUE
route4 VALUE undefined
route5 VALUE undefined
==== nested autoload ====
└── /
├── route5 (GET, HEAD)
└── foo/
├── route4 (GET, HEAD)
└── bar/
├── route
│ ├── 2 (GET, HEAD)
│ └── 3 (GET, HEAD)
└── baz/route1 (GET, HEAD)
root -1 ms
├── bound _after 2 ms
├─┬ default 30 ms
│ ├─┬ autoload 8 ms
│ │ └── value1-auto-2 0 ms
│ └─┬ autoload 22 ms
│ ├─┬ default 1 ms
│ │ ├── bound _after 0 ms
│ │ └── bound _after 0 ms
│ ├─┬ default 1 ms
│ │ ├── bound _after 0 ms
│ │ └── bound _after 0 ms
│ └─┬ default 14 ms
│ ├─┬ autoload 4 ms
│ │ └── value2-auto-3 0 ms
│ └─┬ autoload 9 ms
│ ├─┬ default 1 ms
│ │ ├── bound _after 0 ms
│ │ └── bound _after 0 ms
│ ├─┬ default 1 ms
│ │ ├── bound _after 0 ms
│ │ └── bound _after 0 ms
│ └─┬ default 1 ms
│ ├── bound _after 1 ms
│ └── bound _after 0 ms
└── bound _after 0 ms
route1 VALUE VALUE
route2 VALUE VALUE
route3 VALUE VALUE
route4 VALUE undefined
route5 VALUE undefined
==== autohooks ====
└── /
├── route5 (GET, HEAD)
└── foo/
├── route4 (GET, HEAD)
└── bar/
├── route
│ ├── 2 (GET, HEAD)
│ └── 3 (GET, HEAD)
└── baz/route1 (GET, HEAD)
root -1 ms
├── bound _after 1 ms
├─┬ default 27 ms
│ ├─┬ autoload 5 ms
│ │ └── value1-auto-4 0 ms
│ └─┬ autoload 22 ms
│ ├─┬ default 1 ms
│ │ ├── bound _after 0 ms
│ │ └── bound _after 0 ms
│ ├─┬ default 1 ms
│ │ ├── bound _after 1 ms
│ │ └── bound _after 0 ms
│ ├─┬ composedPlugin 1 ms
│ │ ├── default 0 ms
│ │ ├─┬ default 1 ms
│ │ │ ├── bound _after 0 ms
│ │ │ └── bound _after 0 ms
│ │ └─┬ default 0 ms
│ │ ├── bound _after 0 ms
│ │ └── bound _after 0 ms
│ └─┬ composedPlugin 1 ms
│ ├── default 0 ms
│ └─┬ default 0 ms
│ ├── bound _after 0 ms
│ └── bound _after 0 ms
└── bound _after 0 ms
route1 VALUE VALUE
route2 VALUE VALUE
route3 VALUE VALUE
route4 VALUE undefined
route5 VALUE undefined