外掛程式搭便車指南
首先,不要慌張
!
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
來存取您的實用工具,甚至在您的測試中也可以。
這就是魔力開始的地方;您還記得我們剛剛在談論封裝嗎?好吧,結合使用 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()
})
在第二個 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
_ 和 _decorateRequest
_ 中傳遞函式作為也需要存取 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 使用 fastify-plugin
模組來避免此行為。
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()
之前,裝飾的變數將無法存取。
如果您依賴於先前外掛程式注入的變數,並且想要將其傳遞到 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
第二個引數傳遞的函式的 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
只是註冊一個回呼函式,該函式會在註冊後立即執行,而且最多可以接受三個參數。
回呼函式會根據您提供的參數而改變。
- 如果沒有給回呼函式任何參數,而且發生錯誤,該錯誤將會傳遞給下一個錯誤處理器。
- 如果給回呼函式一個參數,該參數將會是錯誤物件。
- 如果給回呼函式兩個參數,第一個將會是錯誤物件;第二個將會是 done 回呼函式。
- 如果給回呼函式三個參數,第一個將會是錯誤物件,第二個將會是頂層上下文,除非您同時指定了 server 和 override,在這種情況下,上下文將會是 override 回傳的內容,而第三個將會是 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 安全標頭。
您覺得這裡缺少了什麼嗎?請告訴我們!:)