Fastify 最佳实践 #2: autoLoad
在任何 Web 框架的开发中,随着项目规模的增长,如何高效、清晰地组织代码,尤其是路由,都成为一个核心问题。一个良好的自动化配置流程,能极大地提升我们的开发体验和项目的可维护性。
痛点:手动注册的烦恼
在 Fastify 中,添加一个 API 路由非常直接:
// index.ts
import fastify from 'fastify';
const server = fastify();
// 新增一个 API
server.get('/ping', async (request, reply) => {
return 'pong\n';
});
// 启动服务器...
当项目只有一个文件时,这看起来很简单。但随着 API 逐渐增多,我们自然会希望将每个路由的逻辑拆分到独立的文件中,以保持代码的整洁。于是,代码结构演变成了这样:
// routes/ping.ts
import { FastifyInstance, FastifyPluginOptions } from 'fastify';
// 好的实践是把路由封装在 Fastify 插件中
export default async (fastify: FastifyInstance, opts: FastifyPluginOptions) => {
fastify.get('/ping', async (request, reply) => {
return 'pong\n';
});
};
// index.ts
import pingRoute from './routes/ping';
import fastify from 'fastify';
const server = fastify();
// 每增加一个路由文件,就需要在这里手动注册一次
server.register(pingRoute);
// server.register(userRoute);
// server.register(productRoute);
// ... 更多路由
此时,你会发现两个显而易见的问题:
- 手动维护的负担:每创建一个新的路由文件,都必须记得到主入口文件(
index.ts)中手动import和register。这不仅繁琐,还容易遗漏。 - 隐性的重复:路由文件的路径(例如
routes/ping.ts)和它所定义的 API 端点(/ping)在语义上是重复的。
我们能否让文件结构直接映射为 API 路径?为了解决这些问题,Fastify 官方生态提供了一个强大的利器:@fastify/autoload。
解法:@fastify/autoload 实现自动化
@fastify/autoload 允许我们指定一个目录,它会自动扫描该目录下的所有文件,并将它们作为插件(包括路由)注册到 Fastify 实例中。
1. 自动化加载路由
假设我们需要定义以下几个 API:
POST /management/user
DELETE /management/user/{id}
GET /user
GET /user/{id}
我们可以通过创建对应的文件和目录结构,让 autoload 自动生成这些路由。按照约定,像 {id} 这样的路径参数需要用下划线前缀的目录或文件名来表示,例如 _id。
目录结构可以设计成这样:
src
┣ routes
┃ ┣ management
┃ ┃ ┗ user
┃ ┃ ┃ ┣ _id
┃ ┃ ┃ ┃ ┗ delete.ts // -> DELETE /management/user/:id
┃ ┃ ┃ ┗ post.ts // -> POST /management/user
┃ ┗ user
┃ ┃ ┣ _id
┃ ┃ ┃ ┗ get.ts // -> GET /user/:id
┃ ┃ ┗ get.ts // -> GET /user
┣ plugins
┃ ┗ ...
┗ index.ts
接下来,我们编写路由文件。由于 autoload 会根据文件路径生成路由前缀,所以在路由文件中,我们只需定义相对于该前缀的路径。通常,对于 index.ts 或与 HTTP 方法同名的文件(如 get.ts, post.ts),路径可以留空 ”。
// src/routes/management/user/post.ts
import { FastifyInstance } from 'fastify';
// 使用默认导出,函数必须是 async 的
export default async function (fastify: FastifyInstance) {
// 此处的路径为空字符串,autoload 会自动加上 /management/user 前缀
fastify.post('', async (_request, reply) => {
// 实际的业务逻辑
return reply.status(201).send({ message: 'User created' });
});
}
最后,在主入口文件 index.ts 中配置 autoload:
// src/index.ts
import autoLoad from '@fastify/autoload';
import fastify from 'fastify';
import { join } from 'path';
const server = fastify({ logger: true });
// 注册路由自动加载
server.register(autoLoad, {
dir: join(__dirname, 'routes'), // 指定路由所在的目录
routeParams: true,
dirNameRoutePrefix: true,
});
server.listen({ port: 3000 });
autoLoad 核心选项解析
dir: 必须指定的选项,指向你的路由或插件所在的根目录。routeParams: true: 这是实现动态路由的关键。启用后,autoload会将下划线_前缀的文件或目录名(如_id)转换为 Fastify 的路径参数(如 /:id)。dirNameRoutePrefix: true: 告诉autoload使用目录结构来生成路由前缀。关闭此项,routes/management/user/post.ts将只会注册为/post而不是/management/user/post。
2. 自动化加载插件
除了路由,autoload 同样可以加载自定义插件。插件是封装可复用逻辑(如数据库连接、身份验证、装饰器等)的绝佳方式。为了更好地管理插件之间的依赖和封装,我们通常会使用 fastify-plugin。
首先,安装依赖:
npm i fastify-plugin
现在,我们来创建一个插件,它向 Fastify 实例上“装饰”一个配置对象。
// src/plugins/config.plugin.ts
import { FastifyPluginAsync } from 'fastify';
import fp from 'fastify-plugin';
// 使用 fp() 包裹插件,可以防止 Fastify 的封装机制隔离插件
// 这意味着这个插件中装饰的内容(decorate)将对外部所有子上下文可见
const configPlugin: FastifyPluginAsync = async (fastify, opts) => {
const config = {
dbUrl: process.env.DB_URL || 'default-db-url',
apiKey: process.env.API_KEY,
};
// 使用 decorate 将配置对象附加到 Fastify 实例
fastify.decorate('config', config);
};
export default fp(configPlugin);
我们可以把所有插件都放在一个专用的 plugins 目录中。然后,再次使用 autoload 来加载它们。
// 在 index.ts 中继续添加
// ...
// 注册插件自动加载
server.register(autoLoad, {
dir: join(__dirname, 'plugins'),
// 使用 matchFilter 可以更精细地控制加载哪些文件
matchFilter: (path) => path.includes('plugin'),
});
// 注册路由 (注意,插件应该在路由之前注册)
server.register(autoLoad, {
dir: join(__dirname, 'routes'),
// ...
});
matchFilter 选项非常有用,它接受一个文件路径并返回一个布尔值,允许你根据文件名、扩展名或其他规则来决定是否加载该文件。
总结
@fastify/autoload 是一个看似简单却极其强大的工具。通过拥抱“约定优于配置”的理念,它能帮助我们:
- 消除样板代码:不再需要手动
import和register每个路由和插件。 - 优化项目结构:通过文件系统的目录结构来直观地反映 API 结构。
- 提升开发效率:开发者只需关注业务逻辑的实现,其余的交给自动化工具。
在一个不断成长的项目中,尽早引入 autoLoad 是一种明智的投资。它能让你的 Fastify 应用从一开始就保持高度的组织性和可维护性。