夜雨聆风学习资料网

ARTICLE · 1149694

插件、Schema 和高性能 JSON,不只是 Express 平替

插件、Schema 和高性能 JSON,不只是 Express 平替
它看起来像 Web 框架,实际上很像一套工程约定
仓库链接:https://github.com/fastify/fastify

第一次用 Fastify,最直观的感受是:它不是只给你一个 req、一个 res,然后祝你好运。

它把很多后端工程里绕不开的事情放到了台面上:

  • 插件怎么隔离;

  • 生命周期钩子怎么排队;

  • JSON 输入怎么校验;

  • JSON 输出怎么更快序列化;

  • 日志怎么默认带上上下文;

  • 路由怎么高效匹配。

示例很简单:

importFastifyfrom'fastify'constfastify=Fastify({ logger: true })fastify.get('/', async (request, reply) => {return { hello: 'fastify' }})awaitfastify.listen({ port: 3000 })

但如果你只把它当“另一个 Express”,就会错过它最有意思的部分。

源码地图:先认识这些文件

读 Fastify 可以先从这些文件下手:

  • fastify.js:应用初始化和主装配逻辑;

  • lib/handle-request.js:请求处理主路径;

  • lib/route.js:路由注册相关逻辑;

  • lib/request.js:请求对象封装;

  • lib/reply.js:响应对象封装;

  • lib/hooks.js:钩子定义和调度;

  • lib/plugin-override.js、lib/plugin-utils.js:插件封装相关;

  • lib/schema-controller.js、lib/validation.js:Schema 校验与序列化;

  • lib/error-handler.js:错误处理。

依赖也能说明设计方向:

  • find-my-way:高性能 HTTP 路由;

  • avvio:插件启动和封装;

  • pino:日志;

  • fast-json-stringify:基于 Schema 的 JSON 字符串生成;

  • @fastify/ajv-compiler:JSON Schema 校验;

  • secure-json-parse:更安全地解析 JSON;

  • light-my-request:注入式请求测试。

个人观点:只看文件列表,就能看出 Fastify 的性格。它不是“先极简,剩下你们自己拼”,而是把后端常见能力选好、接好,同时尽量保持插件化。

路由:不是遍历数组,而是交给路由树

Express 风格里,很多人容易把路由理解成一个线性数组:请求来了,从前往后试。

Fastify 使用 find-my-way,更接近一棵按路径结构组织的路由树。比如这些路由:

GET /usersGET /users/:idPOST /usersGET /orders/:id

树会把公共前缀和动态节点组织起来,匹配时少走冤枉路。

注册路由时,你会看到类似这样的 API:

fastify.route({method: 'GET',url: '/users/:id',schema: {params: {type: 'object',required: ['id'],properties: {id: { type: 'string' }      }    }  },handler: asyncfunction (request, reply) {return { id: request.params.id }  }})

更常见的简写是:

fastify.get('/users/:id', asyncrequest=> {return { id: request.params.id }})

源码里路由注册不只是保存函数,还会处理:

  • 方法和路径;

  • 参数、query、body 的 Schema;

  • 响应 Schema;

  • 路由级钩子;

  • 错误处理和序列化器;

  • 封装上下文。

这也是为什么 Fastify 的路由配置看起来比单纯“路径 + 函数”厚一些。它把不少运行时信息提前准备好了。

插件系统:register 不是简单地执行函数

Fastify 的插件系统是理解它的关键。看一个插件:

asyncfunctionauthPlugin(app, options) {app.decorate('checkToken', asyncfunction (request) {request.user=awaitverifyToken(request.headers.authorization)  })app.addHook('preHandler', asyncrequest=> {awaitapp.checkToken(request)  })}fastify.register(authPlugin)

如果只用 Express 思维,你可能觉得 register 就是调用函数。但 Fastify 借 avvio 管理插件启动,并提供封装语义。插件内部添加的装饰器、钩子,默认属于这个插件上下文;父子关系、注册顺序、异步启动都有规则。

一个常见例子:

fastify.register(asyncfunctionpluginA(app) {app.decorate('foo', 'foo')app.get('/a', asyncfunction () {return { foo: this.foo }  })})fastify.get('/b', asyncfunction () {return { foo: this.foo }})

在 /b 里未必能拿到插件内部装饰的 foo。这不是 bug,而是封装边界。想共享到父级或根实例,需要正确使用 fastify-plugin 之类方式,把封装“打开”。

个人观点:这个设计一开始可能让人觉得别扭,但大型项目里它能避免所有插件都往全局对象上乱扔东西。全局一时爽,排查火葬场,这种剧情大家都熟。

Schema:输入校验和输出序列化是两件事

Fastify 最有辨识度的能力之一是 JSON Schema。

比如:

constschema= {body: {type: 'object',required: ['name', 'age'],properties: {name: { type: 'string', minLength: 1 },age: { type: 'integer', minimum: 18 }    }  }}fastify.post('/users', { schema }, asyncrequest=> {returnrequest.body})

请求体不合法时,在进入业务 handler 前就会被拦下。业务代码里不用反复写:

if (!body.name) ...if (typeofbody.age!=='number') ...

输出也可以定义 Schema:

fastify.post('/users', {  schema: {    response: {      200: {        type: 'object',        properties: {          id: { type: 'integer' },          name: { type: 'string' }        }      }    }  }}, async request => {  return {    id: 1,    name: request.body.name,    password: 'should-not-leak'  }})

响应 Schema 配合 fast-json-stringify,既可能提升序列化速度,也能帮助约束输出。上面的 password 如果不在响应 Schema 里,就不应该指望它被正常返回。

这背后的思路可以粗略写成:

const validate = compileSchema(schema.body)const stringify = buildSerializer(schema.response[200])async function handler(request, reply) {  if (!validate(request.body)) {    throw validationError(validate.errors)  }  const data = await business(request.body)  reply.header('content-type', 'application/json')  reply.send(stringify(data))}

真实代码要处理多状态码、错误格式、引用 Schema、共享 schema storage,远比这复杂。但主干很清楚:提前编译,运行时复用。

生命周期:钩子不是摆设,而是分层的好机会

Fastify 有多种生命周期钩子,比如:

  • onRequest:请求刚进来,适合鉴权、请求 id;

  • preParsing:解析 body 前;

  • preValidation:校验前;

  • preHandler:业务处理前;

  • onSend:发送前;

  • onResponse:响应完成后;

  • onError:出错时。

示例:

fastify.addHook('onRequest', async (request, reply) => {  request.startTime = performance.now()})fastify.addHook('onResponse', async (request, reply) => {  const cost = performance.now() - request.startTime  request.log.info({ cost }, 'request completed')})

个人观点:钩子多不代表每个都要用。最舒服的方式是按职责放:

  • 鉴权不要塞进业务 handler;

  • 日志不要散落在每个路由;

  • 响应统一改写放 onSend;

  • 参数和 body 不合法交给 Schema;

  • 业务异常和系统异常分层处理。

实操踩坑:从 Express 迁过来最容易懵的地方

1. 箭头函数会丢掉 this

普通函数写法:

fastify.get('/ping', async function (request, reply) {  request.log.info('ping')  return this.someDecorator})

如果改成箭头函数:

fastify.get('/ping', async (request, reply) => {  return this.someDecorator // 这里的 this 不是 Fastify 实例})

需要访问封装实例时,用普通函数;不需要 this 时,箭头函数没问题。

2. 插件注册顺序不是装饰品

Fastify 的插件、路由和钩子都受注册顺序影响。你不能先注册业务路由,再期待它自动吃到后来注册的鉴权钩子:

fastify.register(routes)fastify.register(authPlugin) // 未必能按你想象影响前面的 routes

更稳的方式是先注册基础能力,再注册业务模块。

3. 响应 Schema 会“帮你少返回字段”

如果接口响应里突然少字段,先看 response schema,而不是立刻怀疑 ORM。很多时候是序列化器按合同办事,没列进去的字段就别出门。

4. return 和 reply.send 二选一为主

异步 handler 里可以直接返回数据:

fastify.get('/', async () => ({ ok: true }))

也可以:

fastify.get('/', async (request, reply) => {  return reply.code(201).send({ ok: true })})

但不要混用出复杂副作用,例如先 reply.send(),后面又返回另一个对象。框架会努力处理,但代码可读性会先报警。

选型对比:Fastify、Express、Koa

Fastify 更适合

  • 中大型 Node API 服务;

  • 重视 JSON Schema、日志、插件边界;

  • 需要较好的性能和可观测性;

  • 团队能接受更明确的工程约定。

Express 更适合

  • 老项目维护;

  • 依赖大量传统 Express 中间件;

  • 团队熟悉度优先;

  • 快速实现简单服务。

Koa 更适合

  • 想要轻量核心;

  • 喜欢洋葱模型;

  • 愿意自己挑选和组装组件。

个人观点:Fastify 不是“性能数字更高所以必选”。如果团队没有使用 Schema 的习惯,插件边界也不理解,直接上 Fastify 可能只是换一种方式写混乱。框架优势要配合工程习惯才能兑现。

二次开发思路:从公司级基础插件开始

可以把 Fastify 当作内部服务底座,做这些封装。

统一错误码插件

async function errorCodePlugin(app) {  app.setErrorHandler((error, request, reply) => {    if (error.validation) {      return reply.status(400).send({        code: 'INVALID_REQUEST',        message: error.message      })    }    request.log.error({ err: error })    reply.status(500).send({      code: 'INTERNAL_ERROR',      message: 'Internal Server Error'    })  })}

租户识别插件

async function tenantPlugin(app) {  app.addHook('preHandler', async request => {    const tenantId = request.headers['x-tenant-id']    if (!tenantId) {      throw app.httpErrors.unauthorized('missing tenant')    }    request.tenantId = String(tenantId)  })}

内部服务注册约定

可以按模块拆分:

plugins/  error.js  auth.js  tenant.js  logger.jsroutes/  users.js  orders.jsserver.js

二次开发还可以做:

  • 根据路由 Schema 自动生成 OpenAPI;

  • 统一给内部接口加权限标签;

  • 基于 light-my-request 做模块级集成测试;

  • 封装数据库事务上下文;

  • 给日志自动注入用户、租户、requestId。

社区背景:规则文件多,说明项目不只靠热情发电

仓库里能看到 CONTRIBUTING.md、CODE_OF_CONDUCT.md、GOVERNANCE.md、PROJECT_CHARTER.md、SECURITY.md、SPONSORS.md 等文件。它们说明 Fastify 不只是写代码,还在维护协作方式、治理结构和安全流程。

个人观点:成熟开源项目不只是“代码能跑”。谁能决策、安全问题怎么报、插件如何协作、版本如何发布,这些内容在项目变大后都很关键。Fastify 在这方面的边界感,比单纯 benchmark 更值得关注。

它的重点不是快,而是先把规则立住

读 Fastify,可以抓住三条主线:

  1. 路由用 find-my-way 做高效匹配;

  2. 插件靠 avvio 管理启动和封装;

  3. Schema 同时参与输入校验和输出序列化。

它不是 Express 的简单换皮,也不是只拿性能当招牌的网红框架。更像一个把后端工程常见规矩提前摆上桌的 Node 框架。

当然,规矩多了一开始会有点束缚。但等你维护几十个服务、上百个路由、若干团队共同写插件时就会发现:少一点“我先挂全局再说”,世界真的会清净很多。

源代码:https://github.com/fastify/fastify
END
关注我们

获取更多开源新资讯

相关学习资料