路由 (Routes)
路由
路由方法將會設定您的應用程式的端點。您有兩種方式可以使用 Fastify 宣告路由:簡寫方法和完整宣告。
完整宣告
fastify.route(options)
路由選項
method
:目前支援GET
、HEAD
、TRACE
、DELETE
、OPTIONS
、PATCH
、PUT
和POST
。若要接受更多方法,必須使用addHttpMethod
。它也可以是方法的陣列。url
:要比對此路由的網址路徑(別名:path
)。schema
:一個包含請求和回應的 schema 的物件。它們需要採用 JSON Schema 格式,請查看這裡以取得更多資訊。body
:如果它是 POST、PUT、PATCH、TRACE、SEARCH、PROPFIND、PROPPATCH 或 LOCK 方法,則驗證請求的 body。querystring
或query
:驗證查詢字串。這可以是一個完整的 JSON Schema 物件,其屬性type
為object
,且properties
參數物件,或者簡而言之,是將包含在properties
物件中的值,如下所示。params
:驗證參數。response
:篩選並為回應產生 schema,設定 schema 可以使我們的吞吐量提高 10-20%。
exposeHeadRoute
:為任何GET
路由建立同級HEAD
路由。預設為exposeHeadRoutes
實例選項的值。如果您想要一個自訂的HEAD
處理常式,而不想停用此選項,請務必在定義GET
路由之前定義它。attachValidation
:將validationError
附加到請求,如果存在 schema 驗證錯誤,則不將錯誤傳送到錯誤處理常式。預設的 錯誤格式是 Ajv 格式。onRequest(request, reply, done)
:一個 函式,一旦收到請求就會呼叫,它也可以是一個函式陣列。preParsing(request, reply, done)
:在解析請求之前呼叫的函式,它也可以是一個函式陣列。preValidation(request, reply, done)
:在共用的preValidation
鉤子之後呼叫的函式,如果您需要在路由層級執行驗證,例如,它也可以是一個函式陣列。preHandler(request, reply, done)
:在請求處理常式之前呼叫的函式,它也可以是一個函式陣列。preSerialization(request, reply, payload, done)
:在序列化之前呼叫的函式,它也可以是一個函式陣列。onSend(request, reply, payload, done)
:在傳送回應之前呼叫的函式,它也可以是一個函式陣列。onResponse(request, reply, done)
:當回應已傳送時呼叫的函式,因此您將無法再向客戶端傳送更多資料。它也可以是一個函式陣列。onTimeout(request, reply, done)
:當請求逾時且 HTTP socket 已掛斷時呼叫的函式。onError(request, reply, error, done)
:當路由處理常式擲回或傳送錯誤到客戶端時呼叫的函式。handler(request, reply)
:將處理此請求的函式。當呼叫處理常式時,Fastify 伺服器會繫結到this
。注意:使用箭頭函式會破壞this
的繫結。errorHandler(error, request, reply)
:請求範圍的自訂錯誤處理常式。覆寫預設的錯誤全域處理常式,以及針對路由請求的setErrorHandler
設定的任何內容。若要存取預設處理常式,您可以存取instance.errorHandler
。請注意,只有在外掛程式尚未覆寫的情況下,這才會指向 fastify 的預設errorHandler
。childLoggerFactory(logger, binding, opts, rawReq)
:自訂的工廠函式,將會呼叫它來為每個請求產生子記錄器實例。如需更多資訊,請參閱childLoggerFactory
。覆寫預設的記錄器工廠,以及針對路由請求的setChildLoggerFactory
設定的任何內容。若要存取預設工廠,您可以存取instance.childLoggerFactory
。請注意,只有在外掛程式尚未覆寫的情況下,這才會指向 Fastify 的預設childLoggerFactory
。validatorCompiler({ schema, method, url, httpPart })
:為請求驗證建構 schema 的函式。請參閱驗證和序列化文件。serializerCompiler({ { schema, method, url, httpStatus, contentType } })
:為回應序列化建構 schema 的函式。請參閱驗證和序列化文件。schemaErrorFormatter(errors, dataVar)
:格式化來自驗證編譯器的錯誤的函式。請參閱驗證和序列化文件。覆寫全域 schema 錯誤格式化處理常式,以及針對路由請求的setSchemaErrorFormatter
設定的任何內容。bodyLimit
:防止預設的 JSON body 解析器解析大於此位元組數的請求主體。必須是整數。您也可以在第一次使用fastify(options)
建立 Fastify 實例時全域設定此選項。預設值為1048576
(1 MiB)。logLevel
:為此路由設定日誌等級。請參閱下方。logSerializers
:設定要為此路由記錄的序列化器。config
:用於儲存自訂設定的物件。constraints
:根據請求屬性或值定義路由限制,使用 find-my-way 約束來啟用自訂比對。包含內建的version
和host
約束,並支援自訂約束策略。prefixTrailingSlash
:用於決定如何處理將/
作為帶有前綴的路由傳遞的字串。both
(預設):將同時註冊/prefix
和/prefix/
。slash
:將只會註冊/prefix/
。no-slash
:將只會註冊/prefix
。
注意:此選項不會覆寫 伺服器設定中的
ignoreTrailingSlash
。request
定義於 請求。reply
定義於 回覆。
注意: onRequest
、preParsing
、preValidation
、preHandler
、preSerialization
、onSend
和 onResponse
的文件在 鉤子中有更詳細的說明。此外,若要在 handler
處理請求之前傳送回應,請參閱 從鉤子回應請求。
範例
fastify.route({
method: 'GET',
url: '/',
schema: {
querystring: {
name: { type: 'string' },
excitement: { type: 'integer' }
},
response: {
200: {
type: 'object',
properties: {
hello: { type: 'string' }
}
}
}
},
handler: function (request, reply) {
reply.send({ hello: 'world' })
}
})
簡寫宣告
上面的路由宣告更像是 Hapi 風格,但如果您偏好 Express/Restify 方法,我們也支援它
fastify.get(path, [options], handler)
fastify.head(path, [options], handler)
fastify.post(path, [options], handler)
fastify.put(path, [options], handler)
fastify.delete(path, [options], handler)
fastify.options(path, [options], handler)
fastify.patch(path, [options], handler)
範例
const opts = {
schema: {
response: {
200: {
type: 'object',
properties: {
hello: { type: 'string' }
}
}
}
}
}
fastify.get('/', opts, (request, reply) => {
reply.send({ hello: 'world' })
})
fastify.all(path, [options], handler)
會將相同的處理常式新增至所有支援的方法。
也可以透過 options
物件提供處理常式
const opts = {
schema: {
response: {
200: {
type: 'object',
properties: {
hello: { type: 'string' }
}
}
}
},
handler: function (request, reply) {
reply.send({ hello: 'world' })
}
}
fastify.get('/', opts)
注意:如果處理常式在
options
中指定,並且作為捷徑方法的第三個參數,則會擲回重複的handler
錯誤。
網址建構
Fastify 支援靜態和動態網址。
若要註冊參數式路徑,請在參數名稱前使用冒號。對於萬用字元,請使用星號。請記住,靜態路由始終在參數式和萬用字元之前檢查。
// parametric
fastify.get('/example/:userId', function (request, reply) {
// curl ${app-url}/example/12345
// userId === '12345'
const { userId } = request.params;
// your code here
})
fastify.get('/example/:userId/:secretToken', function (request, reply) {
// curl ${app-url}/example/12345/abc.zHi
// userId === '12345'
// secretToken === 'abc.zHi'
const { userId, secretToken } = request.params;
// your code here
})
// wildcard
fastify.get('/example/*', function (request, reply) {})
也支援正規表示式路由,但請注意,您必須逸出斜線。請注意,正規表示式在效能方面也非常耗費成本!
// parametric with regexp
fastify.get('/example/:file(^\\d+).png', function (request, reply) {
// curl ${app-url}/example/12345.png
// file === '12345'
const { file } = request.params;
// your code here
})
可以在相同的斜線("/")中定義多個參數。例如
fastify.get('/example/near/:lat-:lng/radius/:r', function (request, reply) {
// curl ${app-url}/example/near/15°N-30°E/radius/20
// lat === "15°N"
// lng === "30°E"
// r ==="20"
const { lat, lng, r } = request.params;
// your code here
})
請記住,在這種情況下,使用破折號 ("-") 作為參數分隔符號。
最後,可以使用正規表示式來建立多個參數
fastify.get('/example/at/:hour(^\\d{2})h:minute(^\\d{2})m', function (request, reply) {
// curl ${app-url}/example/at/08h24m
// hour === "08"
// minute === "24"
const { hour, minute } = request.params;
// your code here
})
在這種情況下,可以使用正規表示式不比對的任何字元作為參數分隔符號。
如果將問號 ("?") 新增到參數名稱的結尾,則可以將最後一個參數設為選用。
fastify.get('/example/posts/:id?', function (request, reply) {
const { id } = request.params;
// your code here
})
在這種情況下,您可以請求 /example/posts
以及 /example/posts/1
。如果未指定,則選用參數將為未定義。
使用多個參數的路由可能會對效能造成負面影響,因此請盡可能偏好單一參數方法,尤其是在應用程式熱門路徑上的路由。如果您有興趣了解我們如何處理路由,請查看 find-my-way。
如果您想要包含冒號的路徑而不宣告參數,請使用雙冒號。例如
fastify.post('/name::verb') // will be interpreted as /name:verb
Async Await
您是 async/await
的使用者嗎?我們為您準備好了!
fastify.get('/', options, async function (request, reply) {
var data = await getData()
var processed = await processData(data)
return processed
})
如您所見,我們沒有呼叫 reply.send
來將資料回傳給使用者。您只需要回傳 body 即可完成!
如果需要,您也可以使用 reply.send
將資料回傳給使用者。在這種情況下,請不要忘記在您的 async
處理函式中 return reply
或 await reply
,否則在某些情況下會引入競爭條件。
fastify.get('/', options, async function (request, reply) {
var data = await getData()
var processed = await processData(data)
return reply.send(processed)
})
如果路由包裹一個基於回呼的 API,該 API 會在 promise 鏈之外呼叫 reply.send()
,則可以 await reply
。
fastify.get('/', options, async function (request, reply) {
setImmediate(() => {
reply.send({ hello: 'world' })
})
await reply
})
回傳 reply 也有效。
fastify.get('/', options, async function (request, reply) {
setImmediate(() => {
reply.send({ hello: 'world' })
})
return reply
})
警告
- 當同時使用
return value
和reply.send(value)
時,第一個發生的會優先,第二個值將被丟棄,並且還會發出一個 *警告* 日誌,因為您嘗試發送兩次回應。 - 在 promise 之外呼叫
reply.send()
是可行的,但需要特別注意。更多詳細資訊請閱讀 promise-resolution。 - 您不能回傳
undefined
。更多詳細資訊請閱讀 promise-resolution。
Promise 解析
如果您的處理函式是一個 async
函式或回傳一個 promise,您應該注意支援回呼和 promise 控制流程所需的特殊行為。當處理函式的 promise 被解析時,除非您在處理函式中明確地 await 或 return reply
,否則回應將會自動以其值發送。
- 如果您想使用
async/await
或 promises,但使用reply.send
回應值- 請
return reply
/await reply
。 - 請勿忘記呼叫
reply.send
。
- 請
- 如果您想使用
async/await
或 promises- 請勿使用
reply.send
。 - 請回傳您要發送的值。
- 請勿使用
透過這種方式,我們可以以最小的代價支援 callback-style
和 async-await
。儘管有這麼多的自由度,我們仍然強烈建議只使用一種風格,因為錯誤處理應該在您的應用程式中以一致的方式處理。
注意:每個 async 函式本身都會回傳一個 promise。
路由前綴
有時您需要維護同一個 API 的兩個或多個不同版本;一個經典的方法是用 API 版本號作為所有路由的前綴,例如 /v1/user
。Fastify 提供一種快速且智慧的方式來建立同一個 API 的不同版本,而無需手動更改所有路由名稱,即路由前綴。讓我們看看它是如何運作的
// server.js
const fastify = require('fastify')()
fastify.register(require('./routes/v1/users'), { prefix: '/v1' })
fastify.register(require('./routes/v2/users'), { prefix: '/v2' })
fastify.listen({ port: 3000 })
// routes/v1/users.js
module.exports = function (fastify, opts, done) {
fastify.get('/user', handler_v1)
done()
}
// routes/v2/users.js
module.exports = function (fastify, opts, done) {
fastify.get('/user', handler_v2)
done()
}
Fastify 不會抱怨您對兩個不同的路由使用相同的名稱,因為在編譯時它會自動處理前綴(這也意味著效能完全不受影響!)。
現在您的客戶端將可以存取以下路由
/v1/user
/v2/user
您可以重複執行此操作多次,它也適用於巢狀的 register
,並且也支援路由參數。
如果您想為所有路由使用前綴,您可以將它們放在一個外掛程式中
const fastify = require('fastify')()
const route = {
method: 'POST',
url: '/login',
handler: () => {},
schema: {},
}
fastify.register(function (app, _, done) {
app.get('/users', () => {})
app.route(route)
done()
}, { prefix: '/v1' }) // global route prefix
await fastify.listen({ port: 3000 })
路由前綴和 fastify-plugin
請注意,如果您使用 fastify-plugin
來包裝您的路由,此選項將無法運作。您仍然可以透過在外掛程式中包裝一個外掛程式來使其運作,例如:
const fp = require('fastify-plugin')
const routes = require('./lib/routes')
module.exports = fp(async function (app, opts) {
app.register(routes, {
prefix: '/v1',
})
}, {
name: 'my-routes'
})
處理具有前綴的外掛程式內的 / 路由
/
路由的行為會根據前綴是否以 /
結尾而有所不同。例如,如果我們考慮前綴 /something/
,則新增 /
路由將只會匹配 /something/
。如果我們考慮前綴 /something
,則新增 /
路由將會匹配 /something
和 /something/
。
請參閱上面的 prefixTrailingSlash
路由選項來變更此行為。
自訂日誌級別
您可能需要在您的路由中使用不同的日誌級別;Fastify 以非常直接的方式實現這一點。
您只需要將選項 logLevel
傳遞給外掛程式選項或路由選項,並使用您需要的值。
請注意,如果您在外掛程式層級設定 logLevel
,setNotFoundHandler
和 setErrorHandler
也會受到影響。
// server.js
const fastify = require('fastify')({ logger: true })
fastify.register(require('./routes/user'), { logLevel: 'warn' })
fastify.register(require('./routes/events'), { logLevel: 'debug' })
fastify.listen({ port: 3000 })
或者您可以直接將其傳遞給路由
fastify.get('/', { logLevel: 'warn' }, (request, reply) => {
reply.send({ hello: 'world' })
})
請記住,自訂日誌級別僅適用於路由,而不適用於可透過 fastify.log
存取的全域 Fastify Logger。
自訂日誌序列化程式
在某些情況下,您可能需要記錄一個大型物件,但對於某些路由來說,這可能是一種資源浪費。在這種情況下,您可以定義自訂的 serializers
,並將它們附加在正確的上下文中!
const fastify = require('fastify')({ logger: true })
fastify.register(require('./routes/user'), {
logSerializers: {
user: (value) => `My serializer one - ${value.name}`
}
})
fastify.register(require('./routes/events'), {
logSerializers: {
user: (value) => `My serializer two - ${value.name} ${value.surname}`
}
})
fastify.listen({ port: 3000 })
您可以透過上下文繼承序列化程式
const fastify = Fastify({
logger: {
level: 'info',
serializers: {
user (req) {
return {
method: req.method,
url: req.url,
headers: req.headers,
host: req.host,
remoteAddress: req.ip,
remotePort: req.socket.remotePort
}
}
}
}
})
fastify.register(context1, {
logSerializers: {
user: value => `My serializer father - ${value}`
}
})
async function context1 (fastify, opts) {
fastify.get('/', (req, reply) => {
req.log.info({ user: 'call father serializer', key: 'another key' })
// shows: { user: 'My serializer father - call father serializer', key: 'another key' }
reply.send({})
})
}
fastify.listen({ port: 3000 })
配置
註冊新的處理函式時,您可以將配置物件傳遞給它,並在處理函式中擷取它。
// server.js
const fastify = require('fastify')()
function handler (req, reply) {
reply.send(reply.routeOptions.config.output)
}
fastify.get('/en', { config: { output: 'hello world!' } }, handler)
fastify.get('/it', { config: { output: 'ciao mondo!' } }, handler)
fastify.listen({ port: 3000 })
約束
Fastify 支援根據請求的某些屬性(例如 Host
標頭),或透過 find-my-way
約束的其他任何值來約束路由以僅匹配某些請求。約束是在路由選項的 constraints
屬性中指定的。Fastify 有兩個內建約束可供使用:version
約束和 host
約束,您可以新增自己的自訂約束策略來檢查請求的其他部分,以決定是否應該為請求執行路由。
版本約束
您可以在路由的 constraints
選項中提供 version
鍵。版本化的路由允許您為同一個 HTTP 路由路徑宣告多個處理函式,然後根據每個請求的 Accept-Version
標頭進行匹配。Accept-Version
標頭值應遵循 semver 規範,並且應該使用精確的 semver 版本宣告路由以進行匹配。
如果路由設定了版本,Fastify 將要求設定請求 Accept-Version
標頭,並且對於相同的路徑,將優先選擇版本化的路由而非非版本化的路由。目前不支援進階的版本範圍和預先發佈版本。
請注意,使用此功能會導致路由器整體效能下降。
fastify.route({
method: 'GET',
url: '/',
constraints: { version: '1.2.0' },
handler: function (request, reply) {
reply.send({ hello: 'world' })
}
})
fastify.inject({
method: 'GET',
url: '/',
headers: {
'Accept-Version': '1.x' // it could also be '1.2.0' or '1.2.x'
}
}, (err, res) => {
// { hello: 'world' }
})
⚠ 安全注意事項
請記住在您的回應中設定
Vary
標頭,其中包含您用於定義版本控制的值(例如:'Accept-Version'
),以防止快取中毒攻擊。您也可以將其設定為 Proxy/CDN 的一部分。const append = require('vary').append
fastify.addHook('onSend', (req, reply, payload, done) => {
if (req.headers['accept-version']) { // or the custom header you are using
let value = reply.getHeader('Vary') || ''
const header = Array.isArray(value) ? value.join(', ') : String(value)
if ((value = append(header, 'Accept-Version'))) { // or the custom header you are using
reply.header('Vary', value)
}
}
done()
})
如果您宣告多個具有相同主要或次要版本的版本,Fastify 將始終選擇與 Accept-Version
標頭值相容的最高版本。
如果請求沒有 Accept-Version
標頭,將會回傳 404 錯誤。
可以定義自訂版本匹配邏輯。這可以透過建立 Fastify 伺服器執行個體時的 constraints
配置來完成。
主機約束
您可以在 constraints
路由選項中提供 host
鍵,以限制該路由僅在請求的 Host
標頭的特定值匹配時才匹配。host
約束值可以指定為字串以進行精確匹配,或者指定為 RegExp 以進行任意主機匹配。
fastify.route({
method: 'GET',
url: '/',
constraints: { host: 'auth.fastify.dev' },
handler: function (request, reply) {
reply.send('hello world from auth.fastify.dev')
}
})
fastify.inject({
method: 'GET',
url: '/',
headers: {
'Host': 'example.com'
}
}, (err, res) => {
// 404 because the host doesn't match the constraint
})
fastify.inject({
method: 'GET',
url: '/',
headers: {
'Host': 'auth.fastify.dev'
}
}, (err, res) => {
// => 'hello world from auth.fastify.dev'
})
也可以指定 RegExp host
約束,允許約束匹配萬用字元子網域(或任何其他模式)的主機
fastify.route({
method: 'GET',
url: '/',
constraints: { host: /.*\.fastify\.dev/ }, // will match any subdomain of fastify.dev
handler: function (request, reply) {
reply.send('hello world from ' + request.headers.host)
}
})
非同步自訂約束
可以提供自訂約束,並且可以從另一個來源(例如 database
)提取 constraint
標準。使用非同步自訂約束應作為最後手段,因為它會影響路由器效能。
function databaseOperation(field, done) {
done(null, field)
}
const secret = {
// strategy name for referencing in the route handler `constraints` options
name: 'secret',
// storage factory for storing routes in the find-my-way route tree
storage: function () {
let handlers = {}
return {
get: (type) => { return handlers[type] || null },
set: (type, store) => { handlers[type] = store }
}
},
// function to get the value of the constraint from each incoming request
deriveConstraint: (req, ctx, done) => {
databaseOperation(req.headers['secret'], done)
},
// optional flag marking if handlers without constraints can match requests that have a value for this constraint
mustMatchWhenDerived: true
}
⚠ 安全注意事項
當與非同步約束一起使用時。強烈建議永遠不要在回呼中回傳錯誤。如果錯誤是無法避免的,建議提供自訂的
frameworkErrors
處理函式來處理它。否則,您的路由選擇可能會中斷或向攻擊者洩漏敏感資訊。const Fastify = require('fastify')
const fastify = Fastify({
frameworkErrors: function (err, res, res) {
if (err instanceof Fastify.errorCodes.FST_ERR_ASYNC_CONSTRAINT) {
res.code(400)
return res.send("Invalid header provided")
} else {
res.send(err)
}
}
})