大家好,我是 Yubin
透過 Decorator API,可以讓開發者自訂 Fastify 的核心物件,包括 FastifyInstance, FastifyRequest 及 FastifyReply。
這篇文章來講一下 Decorator 這個東西。
如果想在 Fastify 的各個生命週期之間共用物件或函式,定義 Decorator 是不錯的做法。
如果沒有 decorator,藉由 JavaScript 動態型別的特性,或許可以寫成這樣。
// bad example!! Don't do this
server.addHook('preHandler', (request, reply, done) => {
request.user = 'Yubin'
done()
})
server.get('/', function (request, reply) {
reply.send(`Hello, ${request.user}`)
})
這個方式是直接把想要的欄位,放進 request 物件中。
但這種作法不僅會破壞原本的形狀,也會影響 JavaScript 執行時期的最佳化,操作不慎可能會破壞整個 Fastify 的生命週期。
我們可以利用 decorate 的方式:
// Decorate request with a 'user' property
server.decorateRequest('user', '')
// Update our property
server.addHook('preHandler', (request, reply, done) => {
request.user = 'Yubin'
done()
})
server.get('/', (request, reply) => {
reply.send(`Hello, ${request.user}!`) // 'Hello, Yubin!'
})
上述程式範例中,定義 FastifyRequest 有一個 decorate 叫做 user
,給定的值為 ''
(空字串)。
然後在 preHandler hook 中,透過 request.user
指定值,
在 GET /
route 中取用 request.user
的值來做處理。
可以觀察到,透過 Decorator API,我們可以把想要的變數加進 Fastify 核心物件中。
要注意的是,decorator 的初始值要跟他的 Type 呼應,
預期是空字串的話用 ''
,預期是空物件的話可以用 null
。
decorate(name, value, [dependencies])
定義 server instance,也就是 FastifyInstance 的 decorate。
decorateRequest(name, value, [dependencies])
定義 request 物件,也就是 FastifyRequest 的 decorate。
decorateReply(name, value, [dependencies])
定義 reply 物件,也就是 FastifyReply 的 decorate。
這三個名字很像的 API 分別定義 server/request/reply 的 decorator。
第一個參數 name
,指的是那個 decorator 的名稱,型態為字串。
第二個參數 value
,指的是那個 decorator 的值或物件或方法。
第三個參數 dependencies
是 Optional 的,指的是該 decorator 相依於哪些 decorators。
若該 decorator 沒有滿足相依性的檢查,在啟動 Server 的時候會拋出 FST_ERR_DEC_MISSING_DEPENDENCY
的錯誤,讓伺服器啟動失敗。
可以定義一些實用的方法或讓整個 Fastify 生命週期共用的資訊,如下範例:
server.decorate('utility', function () {
// Something very useful
})
server.decorate('conf', {
db: 'some.db',
port: 3000
})
定義好 decorator 後,可以在任意地方透過該變數名稱存取到相應的物件或方法。
fastify.utility()
console.log(fastify.conf.db)
要特別注意的是,若要定義物件給 FastifyRequest 或 FastifyReply 的 decorator,如下:
// bad practice, don't do it
server.decorateRequest('data', { name: 'Yubin'})
上述程式中,所有的 Request 都會共享同一個物件。任何變動都會影響到其他 request,可能會導致安全性的漏洞或 memory leak。
為了讓每個 request 進來都有完整的值,可以在 onRequest
hook 中對該 decorator 賦值。
這邊使用 fastify-plugin 定義 plugin
import fastifyPlugin = from 'fastify-plugin'
async function myPlugin (app) {
app.decorateRequest('foo', null)
app.addHook('onRequest', async (req, reply) => {
req.foo = { bar: 42 }
})
}
export default fastifyPlugin(myPlugin)
同樣的 issue 在 FastifyReply 上也會發生,在處理 decorator 的時候要注意。
hasDecorator(name)
hasRequestDecorator(name)
hasReplyDecorator(name)
這三個 Decorator API,會回傳 server/request/reply 中有沒有特定的 decorator 存在,
回傳型態為 boolean。
fastify.hasDecorator('utility')
fastify.hasRequestDecorator('utility')
fastify.hasReplyDecorator('utility')
注意,在同一個 Scope 中,重複定義相同名字的 decorator 會拋出錯誤。
上面的程式大多是 JavaScript 範例,我們在使用 TypeScript 做開發的時候,只有定義 Decorators 是不夠的,因為 TypeScript Engine 無法推斷我們已經對該型態做擴展,所以無法讀取新的欄位。
以這段程式來看:
server.decorateRequest('name', 'Yubin')
server.get('/', (request, reply) => {
reply.status(200).send({ msg: `Hello ${request.name}` })
})
會發現 TypeScript 提示說,沒有 name
這個欄位。
這是因為,雖然我們知道透過 decorateRequest
方法,可以增加 decorator 在 FastifyRequest 上,讓我們可以對 request 物件拿到 name
這個欄位。但 request 的型態依然是 FastifyRequest,裡面並不存在 name
這個欄位,所以 TypeScript 會噴錯,也無法順利 Compile。
解法就是,告訴 TypeScript 我對 FastifyRequest 進行擴充了(多了 name
欄位)。
可以運用 declare module
跟 interface
,範例如下:
declare module 'fastify' {
interface FastifyRequest {
name: string
}
}
server.decorateRequest('name', 'Yubin')
server.get('/', (request, reply) => {
reply.status(200).send({ msg: `Hello ${request.name}` })
})
這樣一來,TypeScript 就知道 FastifyRequest 裡面多了一個 name 的欄位,並且型態是 string,型態正確的情況下除了可以順利 Compile,也可以讓我們對程式的信心程度更加提升。
上述程式以 FastifyRequest 為範例,若要增加的是 FastifyInstance 或 FastifyReply 的 Decorators,只要對各自的 interface 進行擴充就沒問題了。