fastify 基础
基础
安装
pnpm add fastify
Server
定义 Server
import { fastify, FastifyInstance } from "fastify";
const initializeMiddleware = (app: FastifyInstance) => {
// 若干中间件
};
const initializeRoutes = (app: FastifyInstance) => {
// 若干路由
};
const startApp = async (port: number) => {
const app = fastify({
logger: true,
});
initializeMiddleware(app);
initializeRoutes(app);
try {
await app.listen({ port });
} catch (err) {
app.log.error(err);
process.exit(1);
}
};
startApp();
API
生命周期相关
- after():当前插件加载完成后执行,始终在 ready() 之前执行;
- ready():所有插件加载完成后执行;
- listen():ready() 后执行;
- close():关闭 fastify 实例;
fastify
.register((instance, opts, done) => {
console.log("Current plugin");
done();
})
.after((err) => {
console.log("After current plugin");
})
.register((instance, opts, done) => {
console.log("Next plugin");
done();
})
.ready((err) => {
console.log("Everything has been loaded");
});
插件相关
- decorate()/register()/hook();
- 详细见具体模块;
路由
定义路由
import { FastifyInstance } from "fastify";
export const testRoute = async (app: FastifyInstance) => {
app.route({
method: "get",
url: "/test",
schema: {
querystring: {
name: { type: "string" },
excitement: { type: "integer" },
},
response: {
200: {
type: "object",
properties: {
hello: { type: "string" },
},
},
},
},
handler: function (request, reply) {
reply.send({ hello: "world" });
},
});
};
使用路由
const fastify = Fastify({
logger: true,
});
fastify.register(testRoute);
路由参数
URL 参数
- 支持正则表达式;
fastify.get("/example/:userId/:secretToken", function (request, reply) {
const { userId, secretToken } = request.params;
// ...
});
// 正则表达式
fastify.get("/example/:file(^\\d+).png", function (request, reply) {
// curl ${app-url}/example/12345.png
// file === '12345'
const { file } = request.params;
// ...
});
异步写法
- 使用 async 函数;
- 使用 return 返回请求;
fastify.get("/", options, async function (request, reply) {
var data = await getData();
var processed = await processData(data);
return processed;
});
// 使用 reply 相关方法
fastify.get("/", options, async function (request, reply) {
var data = await getData();
var processed = await processData(data);
return reply.send(processed);
});
回调函数返回请求
- 使用 reply.send() 并 return reply;
fastify.get("/", options, async function (request, reply) {
setImmediate(() => {
reply.send({ hello: "world" });
});
return reply;
});
路由前缀
fastify.register(testRoute, { prefix: "/v1" });
request
属性
fastify.post("/:params", options, function (request, reply) {
console.log(request.body); // post body
console.log(request.query); // queryString
console.log(request.params); // url 参数
console.log(request.headers);
console.log(request.raw);
console.log(request.server);
console.log(request.id);
console.log(request.ip);
console.log(request.ips);
console.log(request.hostname);
console.log(request.protocol);
console.log(request.url);
console.log(request.routerMethod);
console.log(request.routeOptions.bodyLimit);
console.log(request.routeOptions.method);
console.log(request.routeOptions.url);
console.log(request.routeOptions.attachValidation);
console.log(request.routeOptions.logLevel);
console.log(request.routeOptions.version);
console.log(request.routeOptions.exposeHeadRoute);
console.log(request.routeOptions.prefixTrailingSlash);
console.log(request.routerPath.logLevel);
request.log.info("some info"); // pino log 对象
});
reply
状态码
// 设置状态码
fastify.get("/", options, function (request, reply) {
reply.code(200).send({ hello: "world" });
});
// 读写状态码
if (reply.statusCode >= 299) {
reply.statusCode = 500;
}
操作首部字段
// 添加首部字段
reply.header("set-cookie", "foo");
reply.header("set-cookie", "bar");
reply.headers({
"x-foo": "foo",
"x-bar": "bar",
});
// 获取首部字段
reply.getHeaders();
// 移除首部字段
reply.removeHeader("x-foo");
// 布尔判断
reply.hasHeader("x-foo");
// reply.header('Content-Type', 'the/type') 的简写
reply.type("text/html");
重定向
reply.redirect("/home");
// 设置状态码
reply.redirect(303, "/home");
发送请求
// 对象
fastify.get("/json", options, function (request, reply) {
reply.send({ hello: "world" });
});
// 字符串
fastify.get("/json", options, function (request, reply) {
reply.send("plain string");
});
// 文件流
fastify.get("/streams", async function (request, reply) {
const fs = require("node:fs");
const stream = fs.createReadStream("some-file", "utf8");
// 未设置 Content-Type, 默认为 application/octet-stream
reply.header("Content-Type", "application/octet-stream");
return reply.send(stream);
});
错误处理
自动封装
- reply 返回 Error;
- fastify 自动封装以下结构;
{
error: String; // the HTTP error message
code: String; // the Fastify error code
message: String; // the user error message
statusCode: Number; // the HTTP status code
}
自定义错误类型
- response 为指定状态码设置 schema;
- 若无对应状态码使用默认 schema;
fastify.get(
"/",
{
schema: {
response: {
501: {
type: "object",
properties: {
statusCode: { type: "number" },
code: { type: "string" },
error: { type: "string" },
message: { type: "string" },
time: { type: "string" },
},
},
},
},
},
function (request, reply) {
const error = new Error("This endpoint has not been implemented");
error.time = "it will be implemented in two weeks";
reply.code(501).send(error);
}
);
验证和序列化
Type Provider
- 个人使用 typebox;
- 用于验证数据和序列化的 typescript 支持;
// index.ts
import Fastify from "fastify";
import { TypeBoxTypeProvider } from "@fastify/type-provider-typebox";
import { registerRoutes } from "./routes";
const server = Fastify().withTypeProvider<TypeBoxTypeProvider>();
registerRoutes(server);
server.listen({ port: 3000 });
// routes.ts
import { Type } from "@sinclair/typebox";
import {
FastifyInstance,
FastifyBaseLogger,
RawReplyDefaultExpression,
RawRequestDefaultExpression,
RawServerDefault,
} from "fastify";
import { TypeBoxTypeProvider } from "@fastify/type-provider-typebox";
type FastifyTypebox = FastifyInstance<
RawServerDefault,
RawRequestDefaultExpression<RawServerDefault>,
RawReplyDefaultExpression<RawServerDefault>,
FastifyBaseLogger,
TypeBoxTypeProvider
>;
export function registerRoutes(fastify: FastifyTypebox): void {
fastify.route(
"/route",
{
method: "GET",
schema: {
querystring: queryStringJsonSchema,
},
},
(request, reply) => {
const { foo, bar } = request.query; // type safe!
}
);
}
验证数据
- 验证输入值是否符合 schema;
- 不符合报错,触发 onError handler;
- 仅用于 application-json;
// 定义 schema
export const DatasetActionBodySchema = Type.Object({
datasetID: Type.String(),
datasetName: Type.String(),
datasetAction: Type.Union([
Type.Literal("create"),
Type.Literal("rename"),
Type.Literal("delete"),
]),
});
// 使用 schema
const schema = {
body: bodyJsonSchema,
querystring: queryStringJsonSchema,
params: paramsJsonSchema,
headers: headersJsonSchema,
};
fastify.post("/the/url", { schema }, handler);
序列化数据
- schema 中定义 response,加快序列化速度;
- 使用同验证数据;
- 仅用于 application-json;
const schema = {
response: {
default: responseSchema0,
200: responseSchema1,
},
};
fastify.post("/the/url", { schema }, handler);
错误处理
错误模板
{
"statusCode": 400,
"error": "Bad Request",
"message": "body should have required property 'name'"
}
错误处理
- fastify Error 对象具有 validate 属性;
- 标识是否为验证错误;
export const globalErrorHandler = fp(async (app: FastifyTypebox) => {
app.setErrorHandler((error, _, res) => {
if (error.validation) {
res.code(200).send(generateResponse("error", error.message, null));
} else {
res.code(200).send(generateResponse("error", "server error", null));
}
console.trace(error);
});
});
hook
基础
工作机制
- 使用 fastify.addHook() 监听声明周期特定事件;
- 到达事件触发对应 hook
注册 hook
// 同步, 需要 done()
fastify.addHook("onRequest", (request, reply, done) => {
// Some code
done();
});
// 异步, 不需要 done()
fastify.addHook("onRequest", async (request, reply) => {
await asyncMethod();
});
Request/Reply hook
onRequest
fastify.addHook("onRequest", async (request, reply) => {
// Some code
await asyncMethod();
});
preParsing
fastify.addHook("preParsing", async (request, reply, payload) => {
// Some code
await asyncMethod();
return newPayload;
});
preValidation
fastify.addHook("preValidation", async (request, reply) => {
const importantKey = await generateRandomString();
request.body = { ...request.body, importantKey };
});
preHandler
fastify.addHook("preHandler", async (request, reply) => {
// Some code
await asyncMethod();
});
preSerialization
fastify.addHook("preSerialization", async (request, reply, payload) => {
return { wrapped: payload };
});
onError
fastify.addHook("onError", async (request, reply, error) => {
// Useful for custom error logging
// You should not use this hook to update the error
});
onSend
fastify.addHook("onSend", async (request, reply, payload) => {
const newPayload = payload.replace("some-text", "some-new-text");
return newPayload;
});
onResponse
fastify.addHook("onResponse", async (request, reply) => {
// Some code
await asyncMethod();
});
onTimeout
fastify.addHook("onTimeout", async (request, reply) => {
// Some code
await asyncMethod();
});
onRequestAbort
fastify.addHook("onRequestAbort", async (request) => {
// Some code
await asyncMethod();
});
错误处理
fastify.addHook("onRequest", async (request, reply) => {
throw new Error("Some error");
});
提前返回请求
- 使用 return 或 reply.send();
fastify.addHook("onRequest", (request, reply, done) => {
reply.send("Early response");
});
// Works with async functions too
fastify.addHook("preHandler", async (request, reply) => {
setTimeout(() => {
reply.send({ hello: "from prehandler" });
});
return reply; // mandatory, so the request is not executed further
// Commenting the line above will allow the hooks to continue and fail with FST_ERR_REP_ALREADY_SENT
});
路由级别 hook
- fastify.route 中使用对应 hook;
fastify hook
onListen
- fastify 实例监听之前触发;
- 调用 fastify.ready() 触发;
fastify.addHook("onReady", async function () {
// Some async code
await loadCacheFromDatabase();
});
onReady
- fastify 实例监听触发;
fastify.addHook("onListen", async function () {
// Some async code
});
onReady
- 所有请求处理完毕,且调用 fastify.close() 后触发;
fastify.addHook("onClose", async (instance) => {
// Some async code
await closeDatabaseConnections();
});
preClose
- 调用 fastify.close() 之前触发;
fastify.addHook("preClose", async () => {
// Some async code
await removeSomeServerState();
});
onRoute
- 触发路由时触发;
fastify.addHook("onRoute", (routeOptions) => {
//Some code
routeOptions.method;
routeOptions.schema;
routeOptions.url; // the complete URL of the route, it will include the prefix if any
routeOptions.path; // `url` alias
routeOptions.routePath; // the URL of the route without the prefix
routeOptions.bodyLimit;
routeOptions.logLevel;
routeOptions.logSerializers;
routeOptions.prefix;
});
onRegister
- 注册插件且创建封装上下文之前触发;
fastify.addHook("onRegister", (instance, opts) => {
// ...
});
生命周期
fastify 生命周期
Incoming Request
│
└─▶ Routing
│
└─▶ Instance Logger
│
4**/5** ◀─┴─▶ onRequest Hook
│
4**/5** ◀─┴─▶ preParsing Hook
│
4**/5** ◀─┴─▶ Parsing
│
4**/5** ◀─┴─▶ preValidation Hook
│
400 ◀─┴─▶ Validation
│
4**/5** ◀─┴─▶ preHandler Hook
│
4**/5** ◀─┴─▶ User Handler
│
└─▶ Reply
│
4**/5** ◀─┴─▶ preSerialization Hook
│
└─▶ onSend Hook
│
4**/5** ◀─┴─▶ Outgoing Response
│
└─▶ onResponse Hook
reply 生命周期
★ schema validation Error
│
└─▶ schemaErrorFormatter
│
reply sent ◀── JSON ─┴─ Error instance
│
│ ★ throw an Error
★ send or return │ │
│ │ │
│ ▼ │
reply sent ◀── JSON ─┴─ Error instance ──▶ setErrorHandler ◀─────┘
│
reply sent ◀── JSON ─┴─ Error instance ──▶ onError Hook
│
└─▶ reply sent
插件
思想
- Fastify 中一切且为插件;
- 路由/hook/中间件。。。一切通过插件定义;
const fastify = Fastify({
logger: true,
});
fastify.register(anything);
定义插件
export const testPlugin = async (fastify, opts, done) => {
fastify.decorate("utility", function () {});
fastify.get("/", handler);
fastify.register(require("./other-plugin"));
};
异步
- 异步写法下 done() 回调不必使用;
- 使用可能出现未知结果;
使用插件
fastify.register(plugin, [options]);
// esm
fastify.register(import("./plugin.mjs"));
生命周期
- 使用 after/ready/listen hook;
插件执行顺序
- 根据插件声明顺序加载插件;
- 推荐使用下列顺序;
└── plugins (from the Fastify ecosystem)
└── your plugins (your custom plugins)
└── decorators
└── hooks
└── your services
封装
封装上下文
- fastify 自顶向下分为 Root/Child/Grandchild 三个层级上下文;
- 后代上下文可访问父上下文;
"use strict";
const fastify = require("fastify")();
fastify.decorateRequest("answer", 42);
fastify.register(async function authenticatedContext(childServer) {
childServer.register(require("@fastify/bearer-auth"), { keys: ["abc123"] });
childServer.route({
path: "/one",
method: "GET",
handler(request, response) {
response.send({
answer: request.answer,
// request.foo will be undefined as it's only defined in publicContext
foo: request.foo,
// request.bar will be undefined as it's only defined in grandchildContext
bar: request.bar,
});
},
});
});
fastify.register(async function publicContext(childServer) {
childServer.decorateRequest("foo", "foo");
childServer.route({
path: "/two",
method: "GET",
handler(request, response) {
response.send({
answer: request.answer,
foo: request.foo,
// request.bar will be undefined as it's only defined in grandchildContext
bar: request.bar,
});
},
});
childServer.register(async function grandchildContext(grandchildServer) {
grandchildServer.decorateRequest("bar", "bar");
grandchildServer.route({
path: "/three",
method: "GET",
handler(request, response) {
response.send({
answer: request.answer,
foo: request.foo,
bar: request.bar,
});
},
});
});
});
fastify.listen({ port: 8000 });
获取后代上下文
- 使用 fastify-plugin 插件;
"use strict";
fastify.register(async function publicContext(childServer) {
childServer.decorateRequest("foo", "foo");
childServer.route({
// ...
});
childServer.register(fastifyPlugin(grandchildContext));
async function grandchildContext(grandchildServer) {
//...
}
});
fastify.listen({ port: 8000 });
修饰器
作用
- 自定义核心 fastify 对象;
- 可以再生命周期中的任何 hook 中获取;
初始值
- 初始值尽量接近未来动态设置的值;
添加至 fastify 实例
定义修饰器
fastify.decorate("utility", function () {
// Something very useful
});
fastify.decorate("conf", {
db: "some.db",
port: 3000,
});
使用修饰器
fastify.utility();
console.log(fastify.conf.db);
this 指向
- 修饰器函数使用普通函数形式;
- 路由中将 fastify 实例绑定到 this;
- 否则显式使用 fastify;
fastify.decorate("db", new DbConnection());
fastify.get("/", async function (request, reply) {
// using return
return { hello: await this.db.query("world") };
});
修饰器值类型
- 修饰器的值可以是任何类型;
添加至 Reply
定义装饰器
fastify.decorateReply("utility", function () {
// Something very useful
});
使用修饰器
req.utility();
this 指向
- 修饰器函数使用普通函数形式;
- 路由中将 req 实例绑定到 this;
- 否则显式使用 req;
值类型
- 修饰器值仅能为值类型和函数;
- 使用引用类型,会导致所有类型共享同一对象;
- 正确使用如下;
import fastify-plugin from "fastify-plugin"
const myPlugin = async (app) => {
app.decorateReply("foo", null);
app.addHook("onRequest", async (req, reply) => {
req.foo = { bar: 42 };
});
};
export fp(myPlugin)
添加至 Request
- 使用 decorateRequest();
- 同 decorateReply();
装饰器和封装
- 同一级上下文中不可定义同名装饰器;
- 不同级上下文可以;
错误处理
setErrorHandler
- fastify.setErrorHandler(handler(error,request,reply));
- 定义错误处理程序;
- 错误发生调用其回调函数;
// Register parent error handler
fastify.setErrorHandler((error, request, reply) => {
reply.status(500).send({ ok: false });
});
封装上下文
- setErrorHandler 会被限制于定义时的封装上下文;
- 若存在多个处理程序,优先使用最邻近处理程序;
// Register parent error handler
fastify.setErrorHandler((error, request, reply) => {
reply.status(500).send({ ok: false });
});
fastify.register((app, options, next) => {
// Register child error handler
fastify.setErrorHandler((error, request, reply) => {
throw error;
});
});
自定义中间件
- 用于 setErrorHandler 会被限制于定义时的封装上下文;
- 需使用 fastify-plugin 保留中间件定义的 setErrorHandler;
import { FastifyTypebox } from "@/type/app.type";
import { generateResponse } from "@/util/app";
import fp from "fastify-plugin";
const globalErrorHandler = fp(async (app: FastifyTypebox) => {
app.setErrorHandler((error, _, res) => {
if (error) {
res.code(500).send(generateResponse(0, "error", null));
}
});
});
app.register(globalErrorHandler);
全局错误
- 监听 uncaughtException 事件;
- 处理所有未捕获的错误;
- 最后的防线;
process.on("uncaughtException", (err) => {
console.trace(err);
console.log("unhandled error after fastify error handler");
});