yuheng/doc/fastify-docs/docs/Routes.md
2025-03-19 15:54:28 +08:00

19 KiB
Raw Blame History

Fastify

路由

路由方法设置你程序的路由。 你可以使用简写定义与完整定义两种方式来设定路由。

完整定义

fastify.route(options);

路由选项

  • method:支持的 HTTP 请求方法。目前支持 'DELETE''GET''HEAD''PATCH''POST''PUT' 以及 'OPTIONS'。它还可以是一个 HTTP 方法的数组。

  • url:路由匹配的 URL 路径 (别名:path)。

  • schema:用于验证请求与回复的 schema 对象。 必须符合 JSON Schema 格式。请看这里了解更多信息。

    • body:当为 POST 或 PUT 方法时,校验请求主体。
    • querystringquery:校验 querystring。可以是一个完整的 JSON Schema 对象,它包括了值为 objecttype 属性以及包含参数的 properties 对象,也可以仅仅是 properties 对象中的值 (见下文示例)。
    • params:校验 url 参数。
    • response:过滤并生成用于响应的 schema能帮助提升 10-20% 的吞吐量。
  • exposeHeadRoute:为任意 GET 路由创建一个对应的 HEAD 路由。默认值为服务器实例上的 exposeHeadRoutes 选项的值。如果你不想禁用该选项,又希望自定义 HEAD 处理函数,请在 GET 路由前定义该处理函数。

  • attachValidation:当 schema 校验出错时,将一个 validationError 对象添加到请求中,否则错误将被发送给错误处理函数。

  • 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):当响应发送后调用的函数。因此,在这个函数内部,不允许再向客户端发送数据。可以是一个函数数组。

  • handler(request, reply):处理请求的函数。函数被调用时,Fastify server 将会与 this 进行绑定。注意,使用箭头函数会破坏这一绑定。

  • errorHandler(error, request, reply):在请求作用域内使用的自定义错误控制函数。覆盖默认的全局错误函数,以及由 setErrorHandler 设置的请求错误函数。你可以通过 instance.errorHandler 访问默认的错误函数,在没有插件覆盖的情况下,其指向 Fastify 默认的 errorHandler

  • validatorCompiler({ schema, method, url, httpPart }):生成校验请求的 schema 的函数。详见验证与序列化

  • serializerCompiler({ { schema, method, url, httpStatus } }):生成序列化响应的 schema 的函数。详见验证与序列化

  • schemaErrorFormatter(errors, dataVar):生成一个函数,用于格式化来自 schema 校验函数的错误。详见验证与序列化。在当前路由上会覆盖全局的 schema 错误格式化函数,以及 setSchemaErrorFormatter 设置的值。

  • bodyLimit:一个以字节为单位的整形数,默认值为 1048576 (1 MiB),防止默认的 JSON 解析器解析超过此大小的请求主体。你也可以通过 fastify(options),在首次创建 Fastify 实例时全局设置该值。

  • logLevel:设置日志级别。详见下文。

  • logSerializers:设置当前路由的日志序列化器。

  • config:存放自定义配置的对象。

  • version:一个符合语义化版本控制规范 (semver) 的字符串。示例prefixTrailingSlash:一个字符串,决定如何处理带前缀的 / 路由。

    • both (默认值):同时注册 /prefix/prefix/
    • slash:只会注册 /prefix/
    • no-slash:只会注册 /prefix

    request 的相关内容请看请求一文。

    reply 请看回复一文。

注意:钩子一文中有 onRequestpreParsingpreValidationpreHandlerpreSerializationonSend 以及 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 也是支持的:
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) 会给所有支持的 HTTP 方法添加相同的处理函数。

处理函数还可以写到 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 错误。

Url 构建

Fastify 同时支持静态与动态的 URL
要注册一个参数命名的路径,请在参数名前加上冒号星号表示通配符注意,静态路由总是在参数路由和通配符之前进行匹配。

// 参数路由
fastify.get("/example/:userId", (request, reply) => {});
fastify.get("/example/:userId/:secretToken", (request, reply) => {});

// 通配符
fastify.get("/example/*", (request, reply) => {});

正则表达式路由亦被支持。但要注意,正则表达式会严重拖累性能!

// 正则表达的参数路由
fastify.get("/example/:file(^\\d+).png", (request, reply) => {});

你还可以在同一组斜杠 ("/") 里定义多个参数。就像这样:

fastify.get("/example/near/:lat-:lng/radius/:r", (request, reply) => {});

使用短横线 ("-") 来分隔参数。

最后,同时使用多参数和正则表达式也是允许的。

fastify.get(
  "/example/at/:hour(^\\d{2})h:minute(^\\d{2})m",
  (request, reply) => {},
);

在这个例子里,任何未被正则匹配的符号均可作为参数的分隔符。

多参数的路由会影响性能,所以应该尽量使用单参数,对于高频访问的路由来说更是如此。 如果你对路由的底层感兴趣,可以查看find-my-way

双冒号表示字面意义上的一个冒号,这样就不必通过参数来实现带冒号的路由了。举例如下:

fastify.post("/name::verb"); // 将被解释为 /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 向用户发送数据,只需返回消息主体就可以了!

当然,需要的话你还是可以使用 reply.send 发送数据。

fastify.get("/", options, async function (request, reply) {
  var data = await getData();
  var processed = await processData(data);
  reply.send(processed);
});

假如在路由中,reply.send() 脱离了 promise 链,在一个基于回调的 API 中被调用,你可以使用 await reply

fastify.get("/", options, async function (request, reply) {
  setImmediate(() => {
    reply.send({ hello: "world" });
  });
  await reply;
});

返回回复也是可行的:

fastify.get("/", options, async function (request, reply) {
  setImmediate(() => {
    reply.send({ hello: "world" });
  });
  return reply;
});

警告:

  • 如果你同时使用 return valuereply.send(value),那么只会发送第一次,同时还会触发警告日志,因为你试图发送两次响应。
  • 不能返回 undefined。更多细节请看 promise 取舍

Promise 取舍

假如你的处理函数是一个 async 函数,或返回了一个 promise请注意一种必须支持回调函数和 promise 控制流的特殊情况:如果 promise 被 resolve 为 undefined,请求会被挂起,并触发一个错误日志。

  1. 如果你想使用 async/await 或 promise但通过 reply.send 返回值:
    • return 任何值。
    • 忘了 reply.send
  2. 如果你想使用 async/await 或 promise
    • 使用 reply.send
    • 返回 undefined

通过这一方法,我们便可以最小代价同时支持 回调函数风格 以及 async-await。尽管这么做十分自由,我们还是强烈建议仅使用其中的一种,因为应用的错误处理方式应当保持一致。

注意:每个 async 函数各自返回一个 promise 对象。

路由前缀

有时你需要维护同一 API 的多个不同版本。一般的做法是在所有的路由之前加上版本号,例如 /v1/user。 Fastify 提供了一个快捷且智能的方法来解决上述问题,无需手动更改全部路由。这就是路由前缀。让我们来看下吧:

// server.js
const fastify = require("fastify")();

fastify.register(require("./routes/v1/users"), { prefix: "/v1" });
fastify.register(require("./routes/v2/users"), { prefix: "/v2" });

fastify.listen(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 以及路由参数。 请注意,当使用了 fastify-plugin 时,这一选项是无效的。

处理带前缀的 / 路由

根据前缀是否以 / 结束,路径为 / 的路由的匹配模式有所不同。举例来说,前缀为 /something// 路由只会匹配 something,而前缀为 /something 则会匹配 /something/something/

要改变这一行为,请见上文 prefixTrailingSlash 选项。

自定义日志级别

在 Fastify 中为路由里设置不同的日志级别是十分容易的。
你只需在插件或路由的选项里设置 logLevel 为相应的即可。

要注意的是,如果在插件层面上设置了 logLevel,那么 setNotFoundHandlersetErrorHandler 也会受到影响。

// server.js
const fastify = require("fastify")({ logger: true });

fastify.register(require("./routes/user"), { logLevel: "warn" });
fastify.register(require("./routes/events"), { logLevel: "debug" });

fastify.listen(3000);

你也可以直接将其传给路由:

fastify.get("/", { logLevel: "warn" }, (request, reply) => {
  reply.send({ hello: "world" });
});

自定义的日志级别仅对路由生效,通过 fastify.log 访问的全局日志并不会受到影响。

自定义日志序列化器

在某些上下文里,你也许需要记录一个大型对象,但这在其他路由中是个负担。这时,你可以定义一些序列化器 (serializer),并将它们设置在正确的上下文之上!

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(3000);

你可以通过上下文来继承序列化器:

const fastify = Fastify({
  logger: {
    level: "info",
    serializers: {
      user(req) {
        return {
          method: req.method,
          url: req.url,
          headers: req.headers,
          hostname: req.hostname,
          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" });
    // 打印结果: { user: 'My serializer father - call father  serializer', key: 'another key' }
    reply.send({});
  });
}
fastify.listen(3000);

配置

注册一个新的处理函数,你可以向其传递一个配置对象,并在其中使用它。

// server.js
const fastify = require("fastify")();

function handler(req, reply) {
  reply.send(reply.context.config.output);
}

fastify.get("/en", { config: { output: "hello world!" } }, handler);
fastify.get("/it", { config: { output: "ciao mondo!" } }, handler);

fastify.listen(3000);

约束

Fastify 允许你基于请求的某些属性,例如 Host header 或 find-my-way 指定的其他值,来限制路由仅匹配特定的请求。路由选项里的 constraints 属性便是用于这一特性。Fastify 有两个内建的约束属性:versionhost。你可以自定义约束策略,来判断某路由是否处理一个请求。

版本约束

你可以在路由的 constraints 选项中提供一个 version 键。路由版本化允许你为相同路径的路由设置多个处理函数,并根据请求的 Accept-Version header 来做匹配。 Accept-Version header 的值请遵循 semver 规范,路由也应当附带对应的 semver 版本声明以便成功匹配。
对于版本化的路由Fastify 需要请求附带上 Accept-Version header。此外相同路径的请求会优先匹配带有版本的控制函数。当前尚不支持 semver 规范中的 advanced ranges 与 pre-releases 语法
请注意,这一特性会降低路由的性能。

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", // 也可以是 '1.2.0' 或 '1.2.x'
    },
  },
  (err, res) => {
    // { hello: 'world' }
  },
);

⚠ 安全提示

记得设置 Vary 响应头 为用于区分版本的值 (如 'Accept-Version') 来避免缓存污染攻击 (cache poisoning attacks)。你也可以在代理或 CDN 层设置该值。

const append = require("vary").append;
fastify.addHook("onSend", async (req, reply) => {
  if (req.headers["accept-version"]) {
    // 或其他自定义 header
    let value = reply.getHeader("Vary") || "";
    const header = Array.isArray(value) ? value.join(", ") : String(value);
    if ((value = append(header, "Accept-Version"))) {
      // 或其他自定义 header
      reply.header("Vary", value);
    }
  }
});

如果你声明了多个拥有相同主版本或次版本号的版本Fastify 总是会根据 Accept-Version header 的值选择最兼容的版本。
假如请求未带有 Accept-Version header那么将返回一个 404 错误。

新建 Fastify 实例时,可以通过设置 constraints 选项,来自定义版本匹配的逻辑。

Host 约束

你可以在路由的 constraints 选项中提供一个 host 键,使得该路由根据请求的 Host header 来做匹配。 host 约束的值可以是精确匹配的字符串,也可以是任意匹配的正则表达式

fastify.route({
  method: "GET",
  url: "/",
  constraints: { host: "auth.fastify.io" },
  handler: function (request, reply) {
    reply.send("hello world from auth.fastify.io");
  },
});

fastify.inject(
  {
    method: "GET",
    url: "/",
    headers: {
      Host: "example.com",
    },
  },
  (err, res) => {
    // 返回 404因为 host 不匹配
  },
);

fastify.inject(
  {
    method: "GET",
    url: "/",
    headers: {
      Host: "auth.fastify.io",
    },
  },
  (err, res) => {
    // => 'hello world from auth.fastify.io'
  },
);

正则形式的 host 约束也可用于匹配任意的子域 (或其他模式)

fastify.route({
  method: "GET",
  url: "/",
  constraints: { host: /.*\.fastify\.io/ }, // 匹配 fastify.io 的任意子域
  handler: function (request, reply) {
    reply.send("hello world from " + request.headers.host);
  },
});