Fastify 模块化项目实战(一) — 技术选型

为什么选择 Fastify 而非 NestJS, ExpressJS, Koa?

按照我的个人经验, 之前使用 ExpressJS 来搭建项目, 项目结构基本如下所示:

txt 复制代码
src/
└───── infrastructures/
    │   ├── db.js
    │   └── server.js
    │
    ├── middlewares/
    │   └── authMiddleware.js
    │
    ├── routes/
    │   ├── authRoutes.js
    │   └── messageRoutes.js
    │
    └── services/
        ├── messagesService.js
        ├── userService.js
        └── authService.js

这次在构思模块化项目的时候, 想尝试一些新东西, 能够满足我的封装性高的需求, 首先对比了 NestJS 和 ExpressJS

对比项 NestJS ExpressJS
定位 进阶型全栈后端框架 轻量级 Web 框架
架构风格 强制 MVC + 模块化 + DI(受 Angular 启发) 无架构规范,自由度高
学习成本 🟠 中等偏高(需要理解装饰器/依赖注入) 🟢 低(API 简洁易用)
TypeScript 支持 原生支持,强类型优先 需要手动配置 TS
依赖注入(DI) 内置支持 无(通常用单例/全局或自定义 DI)
路由定义 注解式控制 手动配置路由
扩展性 ⭐⭐⭐⭐ ⭐⭐
生态和插件 模块化(Auth、Cache、Swagger、GraphQL、Jobs) 需要自行组合中间件
适合场景 企业级复杂业务、大团队合作 小项目、快速原型、灵活业务流程
性能 接近 Express 基于 Express / Fastify,性能优秀
社区 & 文档 成熟活跃 经典稳定

在这之后又了解了 Fastify 和 Koa 框架:
Fastify

  • 性能比 Express 更高
  • 插件机制
  • 原生支持超快 JSON 序列化
  • 适合高性能 API 服务
  • 学习成本略高于 Express

Koa

  • 由 Express 核心团队打造
  • Promise + async/await 原生支持
  • 更现代、更轻量
  • 中间件采用新版洋葱模型

然而 Fastify 的高性能以及插件机制吸引了我, 于是我打算基于 Fastify 开始搭建项目

开始搭建 Fastify 项目之前需要考虑的三件事

选择JS还是TS?

维度 JS TS
学习成本 低 中
开发速度(短期) 快 稍慢
维护成本(长期) 高 低
重构安全性 低 高
模块扩展能力 中 高
工程化能力 中 极高
架构演进能力 弱 强
模板复用价值 低 高
项目规模适配 小项目 中大型项目
技术资产价值 低 高

对于复用价值来说, 选择TS是必须的

选择Common JS 还是 ES Module?

我们都知道JS有两种模块系统

ES Modules (ESM) 是 ECMAScript 标准化的现代 JavaScript 模块系统,使用 import/export,支持静态分析、异步加载和单例模式,适用于浏览器和新版 Node.js。CommonJS (CJS) 是 Node.js 传统的默认系统,使用 require/module.exports,同步加载模块,主要用于服务器端。

所以它们之间的本质区别是底层机制的差异: CSJ是运行时加载

txt 复制代码
require() 是函数
→ 运行到这行才加载
→ 动态加载
→ 无法静态分析依赖关系

而ESJ(编译期静态分析)

txt 复制代码
import 是语法
→ 编译期分析依赖
→ 构建依赖图
→ 支持 Tree-shaking
→ 支持模块优化

在语法层面:

CommonJS:

js 复制代码
// 导入
const fs = require('fs')

// 导出
module.exports = {
  test() {}
}

ES Module:

js 复制代码
// 导入
import fs from 'fs'

// 导出
export function test() {}
export default {}

于是我选择了ESM来搭建项目模块, 这也是当下的工程趋势

选择Node原生测试框架还是其它?

谈到JS测试框架, 立马能想到 Jest, Vitest, Mocha, Chai, 在JS生态里, 一般在前端会测试组件渲染, 在后端是一些简单的断言, 例如测试 Rest API 的请求, 工具库的单元测试等, 接触了 Fastify 我了解到了 Node 原生测试方法.

能力 Jest Vitest node:test
Runner ✅ ✅ ✅
Mock ✅ 强 ✅ 强 ⚠️ 基础
覆盖率 ✅ ✅ ⚠️ 外接
Watch ✅ ✅ ❌
快照测试 ✅ ✅ ❌
并发执行 ⚠️ ✅ ✅
ESM 支持 ⚠️ 配置复杂 ✅ 原生 ✅ 原生
TS 支持 ⚠️ 依赖 ts-jest ✅ 原生 ⚠️ 手动
生态插件 ✅ 非常成熟 🟡 成长中 ❌ 几乎无

举几个简单的例子吧, 在 vitest 里:

前端的测试:

js 复制代码
// 使用 vitest 的方法
import { describe, expect, vi } from "vitest"
import { fireEvent, render, screen, waitFor } from "@testing-library/react"
import App from "../../src/components/App"

// 通过 vi.hoisted 来 mock 方法
const { mockGetNotes, mockDeleteNote, mockPutNote } = vi.hoisted(() => {
  return { mockGetNotes: vi.fn(), mockDeleteNote: vi.fn(), mockPutNote: vi.fn() }
})

// 使用上面的mock方法来替换后端的具体方法
vi.mock('../../src/services/notesService', () => {
  return {
    default: { getNotes: mockGetNotes, deleteNote: mockDeleteNote, putNote: mockPutNote }
  }
})

describe("render app", () => {
  it("should show notes successfully", async () => {
    //mock实现
    mockGetNotes.mockResolvedValue(["fake note1"])
    render(<App />)
    await waitFor(() => screen.getByText("fake note1"))

    expect(screen.getByText("fake note1")).toBeInTheDocument()
  })

  it("should delete note successfully when click delete note button", async () => {
    mockGetNotes.mockResolvedValue(["fake note1"])
    render(<App />)
    await waitFor(() => screen.getByText("fake note1"))

    const deleteNoteBtn = screen.getByTestId("delete-note")
    fireEvent.click(deleteNoteBtn)

    expect(mockDeleteNote).toHaveBeenCalled(1)
  })
})

后端的测试例子:

js 复制代码
// vitest 的方法
import { beforeEach, describe, it, vi, expect } from "vitest"
import notesService from "../../src/services/notesService"

const mockSelect = vi.fn()
const mockInsert = vi.fn()
const mockDelete = vi.fn()
const mockEq = vi.fn()

// mock 基础设施(DB)的 client 的相关方法
const { supabaseMockClient } = vi.hoisted(() => {
  return {
    supabaseMockClient: {
      from: vi.fn(() => ({
        select: mockSelect,
        insert: mockInsert,
        delete: vi.fn(() => ({
          eq: mockEq
        })),
      }))
    }
  }
})

// 替换外部依赖的具体方法为mock方法
vi.mock('@supabase/supabase-js', () => ({
  createClient: vi.fn(() => supabaseMockClient),
}))

describe("notes service layer tests", () => {
  beforeEach(() => {
    vi.clearAllMocks()
  })

  it("should return notes array", async () => {
    mockSelect.mockResolvedValue({ data: [{ note: 'note1' }, { note: 'note2' }], error: null })

    const result = await notesService.getNotes()

    expect(supabaseMockClient.from).toHaveBeenCalledWith('notes-online')
    expect(supabaseMockClient.from().select).toHaveBeenCalledWith('note')
    expect(result).toEqual(['note1', 'note2'])
  })

  it("should return empty array when return error", async () => {
    mockSelect.mockResolvedValue({ data: null, error: "some errors" })

    const result = await notesService.getNotes()

    expect(result).toEqual([])
  })

  it("should save note successfully", async () => {
    mockInsert.mockResolvedValue()

    await notesService.putNote("new fake note")

    expect(supabaseMockClient.from).toHaveBeenCalledWith('notes-online')
    expect(supabaseMockClient.from().insert).toHaveBeenCalledWith({ note: "new fake note", creator: "anonymous" }, { returning: 'minimal' })
  })

  it("should delete note successfully", async () => {
    mockDelete.mockResolvedValue()
    mockEq.mockResolvedValue()

    await notesService.deleteNote("fake note")

    expect(supabaseMockClient.from().delete().eq).toHaveBeenCalledWith('note', 'fake note')
  })
})

而最近使用了 node:test, 例如:

ts 复制代码
import { describe, it } from 'node:test'
import assert from 'node:assert'
import { build, expectValidationError } from '../../../../helper.js'

describe('Security api', () => {
  describe('POST /api/v1/auth/login', () => {
    it('Transaction should rollback on error', async (t) => {
      const app = await build(t)

      const { mock: mockCompare } = t.mock.method(app.passwordManager, 'compare')
      mockCompare.mockImplementationOnce((value: string, hash: string) => {
        throw new Error('Kaboom!')
      })

      const { mock: mockLogError } = t.mock.method(app.log, 'error')

      const res = await app.inject({
        method: 'POST',
        url: '/api/v1/auth/login',
        payload: {
          email: 'basic@example.com',
          password: 'Password123$'
        }
      })

      assert.strictEqual(mockCompare.callCount(), 1)

      const arg = mockLogError.calls[0].arguments[0] as unknown as {
        err: Error;
      }

      assert.strictEqual(res.statusCode, 500)
      assert.deepStrictEqual(arg.err.message, 'Kaboom!')
    })
})

使用之后也很顺手, 一个是没有外部依赖, 另外也能满足需求, 所以目前我也并未使用其它测试框架, 选择了node:test 框架.

原文地址: sddtc.florist

相关推荐
默_笙8 小时前
🚓 分诊台与拆题术:让 RAG 学会判断和规划
前端·javascript
呃呃呃呃ex9 小时前
10. 现代前端工程化:ES6 React 项目实战与原理解析
前端·javascript
浪遏9 小时前
D2C 系统架构复盘:从设计稿到代码,我们踩过的坑和最终方案
前端·javascript·后端
Blanche15009 小时前
Webpack 构建流程解析
javascript·面试
Flynt9 小时前
官方说一个破折号拖慢整段高亮,我在 Node 里验证了一遍:机制是真的,2.8 倍没跑出来
javascript·性能优化·v8
默_笙9 小时前
🚗 把小说装进数据库了:我的第一个 RAG,和它的五个硬伤
前端·javascript
像我这样帅的人丶你还9 小时前
📱iPhone Duo 开屏动画咋实现的?
javascript·webgl·three.js
Leslie1659 小时前
把 SQL 性能诊断接入模型 API:从谓词分析到可验证优化
javascript
gnip9 小时前
UniApp 内嵌 H5 通信全攻略
前端·javascript
用户6802659051199 小时前
企业电脑统一管理怎么做?2026企业终端统一管理方法与工具推荐
javascript·后端·面试