跳至主要內容
版本:最新版本 (v5.0.x)

路由 (Routes)

路由

路由方法將會設定您的應用程式的端點。您有兩種方式可以使用 Fastify 宣告路由:簡寫方法和完整宣告。

完整宣告

fastify.route(options)

路由選項

  • method:目前支援 GETHEADTRACEDELETEOPTIONSPATCHPUTPOST。若要接受更多方法,必須使用 addHttpMethod。它也可以是方法的陣列。

  • url:要比對此路由的網址路徑(別名:path)。

  • schema:一個包含請求和回應的 schema 的物件。它們需要採用 JSON Schema 格式,請查看這裡以取得更多資訊。

    • body:如果它是 POST、PUT、PATCH、TRACE、SEARCH、PROPFIND、PROPPATCH 或 LOCK 方法,則驗證請求的 body。
    • querystringquery:驗證查詢字串。這可以是一個完整的 JSON Schema 物件,其屬性 typeobject,且 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:用於儲存自訂設定的物件。

  • version:一個semver 相容字串,定義端點的版本。範例

  • constraints:根據請求屬性或值定義路由限制,使用 find-my-way 約束來啟用自訂比對。包含內建的 versionhost 約束,並支援自訂約束策略。

  • prefixTrailingSlash:用於決定如何處理將 / 作為帶有前綴的路由傳遞的字串。

    • both (預設):將同時註冊 /prefix/prefix/
    • slash:將只會註冊 /prefix/
    • no-slash:將只會註冊 /prefix

    注意:此選項不會覆寫 伺服器設定中的 ignoreTrailingSlash

  • request 定義於 請求

  • reply 定義於 回覆

注意: onRequestpreParsingpreValidationpreHandlerpreSerializationonSendonResponse 的文件在 鉤子中有更詳細的說明。此外,若要在 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 replyawait 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 valuereply.send(value) 時,第一個發生的會優先,第二個值將被丟棄,並且還會發出一個 *警告* 日誌,因為您嘗試發送兩次回應。
  • 在 promise 之外呼叫 reply.send() 是可行的,但需要特別注意。更多詳細資訊請閱讀 promise-resolution
  • 您不能回傳 undefined。更多詳細資訊請閱讀 promise-resolution

Promise 解析

如果您的處理函式是一個 async 函式或回傳一個 promise,您應該注意支援回呼和 promise 控制流程所需的特殊行為。當處理函式的 promise 被解析時,除非您在處理函式中明確地 await 或 return reply,否則回應將會自動以其值發送。

  1. 如果您想使用 async/await 或 promises,但使用 reply.send 回應值
    • return reply / await reply
    • 請勿忘記呼叫 reply.send
  2. 如果您想使用 async/await 或 promises
    • 請勿使用 reply.send
    • 回傳您要發送的值。

透過這種方式,我們可以以最小的代價支援 callback-styleasync-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 傳遞給外掛程式選項或路由選項,並使用您需要的

請注意,如果您在外掛程式層級設定 logLevelsetNotFoundHandlersetErrorHandler 也會受到影響。

// 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)
}
}
})