Fastify 的设计哲学与 Express 的最大区别之一,是插件系统带封装(Encapsulation)。几乎所有功能——路由、装饰器、钩子、Schema 共享——都能以插件形式组合,且默认对上层作用域隔离,避免全局污染。这一设计借鉴了 hapi 的插件模型。
前置知识:先阅读 Fastify 路由与 Handler。
一个插件就是一个「异步函数」,接收 Fastify 实例并按需扩展它:
async function myPlugin(app, opts) {
app.decorate('now', () => Date.now());
app.get('/ping', async () => ({ pong: true }));
}
const app = Fastify();
await app.register(myPlugin, { prefix: '/api' }); // opts 注入register 返回 Promise,await 后追加的内容位于插件作用域之外,更适合按顺序构建应用。
每次 app.register() 都会创建一个新的、独立的封装上下文。上下文内的状态默认不泄露到外层。
┌──────────────── Fastify 根实例 ────────────────┐
│ routes: / decorators: db │
│ ┌───── 插件 A 的封装上下文 ─────────────────┐ │
│ │ routes: /a decorators: cache(私有) │ │
│ │ ┌── 插件 A1 更深封装 ──┐ │ │
│ │ │ routes: /a/a1 │ │ │
│ │ └─────────────────────┘ │ │
│ └──────────────────────────────────────────┘ │
└─────────────────────────────────────────────────┘规律:
const app = Fastify();
app.decorate('db', createDb()); // 根作用域:全局可见
await app.register(async (scope) => {
scope.decorate('cache', createCache()); // 仅本封装及其子可见
scope.decorate('secret', 's3cr3t'); // 下层的私有物
// scope.get(...) 可读取 scope.db 和 scope.cache
});
// 根实例访问 scope.cache → TypeError(封装隔离)
app.get('/probe', async () => app.cache); // ❌ app.cache 不存在prefix前缀是封装最常见的用途,让每一组路由拥有独立路径空间:
await app.register(userRoutes, { prefix: '/users' });
await app.register(orderRoutes, { prefix: '/orders' });装饰器用于在 Fastify 实例上挂载自定义属性或方法。
| 场景 | API |
|---|---|
| 实例级属性/方法 | app.decorate(name, value) |
请求级(每个请求的 req) | app.decorateRequest(name, factory) |
响应级(每个 reply) | app.decorateReply(name, factory) |
app.decorate('config', { port: 3000 });
// 第三个参数是该装饰器依赖的其他装饰器名(须已注册,否则抛错),并非"防覆盖"
app.decorate('db', createPool(), ['config']);decorateRequest 支持延迟求值的工厂函数,适合「按需创建」:
app.decorateRequest('user', null);
app.addHook('preHandler', async (req) => {
if (req.headers.authorization) {
req.user = await loadUser(req.headers.authorization);
}
});
app.get('/me', async (req) => req.user);app.decorateReply('ok', function (payload) {
return this.code(200).send(payload);
});
app.get('/', async (req, reply) => reply.ok({ status: 'good' }));| 层级 | 对应方式 | 对封装上下文可见性 |
|---|---|---|
| 根实例 | app.decorate | 全局 |
| 插件/封装 | scope.decorate | 本封装及以下 |
| 请求级别 | decorateRequest | 每个请求的 req |
| 路由级别 | 路由选项 config/钩子 | 单条路由 |
这决定了大型应用「公共能力上移、局部能力下沉」的组织方式。
app.register() 是顺序化的:await 前面的插件注册完成,才继续后面的注册。
await app.register(configPlugin); // 先提供配置
await app.register(databasePlugin); // 依赖配置
await app.register(routesPlugin); // 依赖数据库注意:不要在封装的异步函数里直接 await 同作用域的其它 register 而未返回其 Promise,容易造成顺序错乱。推荐在主入口按依赖顺序 await 各插件。
| 插件 | 用途 |
|---|---|
@fastify/cors | 跨域配置 |
@fastify/jwt | JWT 签验(配合 decorateRequest 注入) |
@fastify/auth | 多策略鉴权 |
@fastify/helmet | 安全响应头 |
@fastify/rate-limit | 请求限流 |
@fastify/websocket | WebSocket 支持 |
@fastify/swagger + @fastify/swagger-ui | OpenAPI 文档 |
@fastify/static | 静态文件 |
fastify-plugin | 解除封装的插件辅助 |
fastify-plugin 突破封装默认装饰器只在封装内可见。若要让插件把能力暴露给所有后续封装,需用 fastify-plugin 包裹:
import fp from 'fastify-plugin';
export default fp(async function dbPlugin(app) {
app.decorate('db', createPool());
}, { name: 'db-plugin' });fastify-plugin 会将该插件提升为「全局」注册,从而 app.db 在所有后续作用域可用——这正是官方插件普遍采用的模式。
// plugins/auth.js
import fp from 'fastify-plugin';
import jwt from '@fastify/jwt';
export default fp(async function authPlugin(app) {
await app.register(jwt, { secret: app.config.jwtSecret });
app.decorate('authenticate', async (req, reply) => {
try {
await req.jwtVerify();
} catch (err) {
reply.code(401).send({ code: 'UNAUTHORIZED' });
}
});
}, { name: 'auth' });在需要保护的路由上使用:
app.get('/profile', {
preHandler: [app.authenticate],
handler: async (req) => ({ user: req.user }),
});// plugins/db.js
import fp from 'fastify-plugin';
export default fp(async function dbPlugin(app) {
const pool = createPool(app.config.db);
app.decorate('db', pool);
app.addHook('onClose', () => pool.end()); // 应用关闭时释放连接
}, { name: 'db' });Fastify 的插件模型高度统一,连 WebSocket、GraphQL、静态文件这些「框架边界能力」都作为插件接入,保持核心精简:
await app.register(import('@fastify/websocket'));
await app.register(import('mercurius')); // GraphQL
await app.register(import('@fastify/static'), { root: publicDir });默认装饰器只在「定义的封装」向下可见,子插件是新的封装,需要父级装饰器——要么用 fastify-plugin 把提供者提升为全局,要么将装饰器定义在真正的根实例上。
register 都新建了独立的 db 连接吗?不一定。用 fastify-plugin 包裹的公共插件只执行一次;普通封装插件每个上下文各自执行,可能造成重复连接。公共资源请用 fastify-plugin。
装饰器工厂里不要互相读取;把有依赖的装饰器放在同一 fastify-plugin 里按序 decorate。
fastify-plugin 组织公共插件并部署上线