Fastify【实用教程】含开源范例项目

简介

官网 https://fastify.dev/

Fastify 是一款高性能的 Node.js Web 框架【后端开发框架】,适合有高性能需求的项目。

框架 核心特点与设计理念 适用场景 生态与社区 性能表现 学习曲线
Express 轻量灵活,极简核心,中间件机制成熟,API 简洁直观 快速开发小型应用、API 服务,学习入门,原型验证 最丰富,插件和工具生态极完善 基础性能良好,无过多封装损耗 低
Koa Express 团队打造,采用 async/await 语法,中间件洋葱模型更优雅 追求代码简洁性和现代异步编程的中小型应用 生态良好,兼容多数 Express 中间件 性能略优于 Express,异步处理更高效 中低
NestJS 基于 TypeScript,模块化架构,支持依赖注入,借鉴 Angular 设计思想 大型企业级应用、微服务,需要强类型和严格架构规范的项目 生态快速增长,官方模块丰富,TypeScript 支持完善 性能优秀,复杂业务场景下稳定性强 高
Fastify 极致性能优化,基于 Schema 验证,插件系统高效,低开销 高并发 API 服务、需要处理大量请求的性能敏感场景 生态专注性能,插件轻量高效 性能领先,比 Express 快 2-3 倍 中
Hapi 配置驱动开发,内置输入验证、缓存等功能,安全性强 构建稳定的 API 服务、企业级后端系统 生态完善,官方插件质量高,文档详尽 性能良好,配置复杂时略有损耗 中高
Sails 全栈 MVC 框架,类似 Ruby on Rails,内置 ORM 和实时通信 快速开发 CRUD 应用、实时应用(如聊天、协作工具) 生态针对性强,适合快速迭代开发 性能中等,封装较厚重 中
Egg.js 基于 Koa,阿里团队维护,"约定优于配置",内置企业级最佳实践 中大型企业级应用、团队协作项目,尤其是需要规范开发流程的场景 国内社区活跃,阿里系场景验证,插件生态完善 性能与 Koa 接近,企业级特性加持下稳定性强 中

开发调试

热更新

但不支持 .env 文件的热更新

package.json

json 复制代码
  "scripts": {
    "dev": "chcp 65001 && set NODE_OPTIONS=--enable-source-maps --no-warnings && node --watch src/index.js"
  },

原理:添加了 -watch

同时解决了终端中文乱码的问题

环境变量配置 .env

  1. 安装依赖
dos 复制代码
npm i dotenv
  1. 导入使用

    src\index.js

    js 复制代码
    import 'dotenv/config'; // 加载.env文件
  2. 配置环境变量

    新建 .env

    env 复制代码
    PORT=3000
  3. 使用环境变量

    js 复制代码
    process.env.PORT

注册插件

src\plugins\index.js

js 复制代码
import mongodb from '@fastify/mongodb';

export const registerPlugins = async (app) => {
    // 注册 MongoDB 插件
    app.register(mongodb, {
      url: process.env.MONGODB_URI,
      database: process.env.MONGODB_DB
    });
}

src\index.js

js 复制代码
import { registerPlugins } from './plugins/index.js';
ts 复制代码
await registerPlugins(app);

此处的 app 为 Fastify 的实例

js 复制代码
const app = Fastify({
    logger: true
})

连接数据库

连接 MongoDB

  1. 安装插件 @fastify/mongodb

    dos 复制代码
    npm i @fastify/mongodb --save
  2. 注册插件

    见上文注册插件相关的代码

  3. 配置环境变量(根据自己的环境修改)

    .env

    env 复制代码
    MONGODB_URI=mongodb://localhost:27017
    MONGODB_DB=test

开发接口

新增

src\apis\user.js

js 复制代码
  app.post(
    "/add",
    {
      schema: {
        //可根据需要添加自定义的校验规则
      },
    },
    async (request, reply) => {
      try {
        const newData = {
          ...request.body,
          createdAt: new Date(),
        };

        const result = await collection.insertOne(newData);

        return reply.code(201).send({
          success: true,
          message: config.label + "新增成功",
          data: {
            id: result.insertedId,
            ...newData,
          },
        });
      } catch (error) {
        app.log.error("新增" + config.label + "失败:", error);
        return reply.code(500).send({
          success: false,
          error: "新增" + config.label + "失败",
        });
      }
    }
  );

修改

js 复制代码
  // 更新
  app.post("/update/:id", async (request, reply) => {
    try {
      const { ObjectId } = app.mongo;
      const updateData = {
        ...request.body,
        updatedAt: new Date(),
      };

      const result = await collection.updateOne(
        { _id: new ObjectId(request.params.id) },
        { $set: updateData }
      );

      if (result.modifiedCount === 0) {
        return reply.code(404).send({
          success: false,
          error: config.label + "不存在或未修改",
        });
      }

      return {
        success: true,
        message: config.label + "更新成功",
      };
    } catch (error) {
      app.log.error("更新" + config.label + "失败:", error);
      return reply.code(500).send({
        success: false,
        error: "更新" + config.label + "失败",
      });
    }
  });

查询

js 复制代码
  // 列表
  app.get("/list", async (request, reply) => {
    try {
      const list = await collection.find().toArray();
      return {
        success: true,
        count: list.length,
        data: list,
      };
    } catch (error) {
      app.log.error("获取" + config.label + "列表失败:", error);
      return reply.code(500).send({
        success: false,
        error: "获取" + config.label + "列表失败",
      });
    }
  });

删除

js 复制代码
  app.post("/delete/:id", async (request, reply) => {
    try {
      const { ObjectId } = app.mongo;

      const result = await collection.deleteOne({
        _id: new ObjectId(request.params.id),
      });

      if (result.deletedCount === 0) {
        return reply.code(404).send({
          success: false,
          error: config.label + "不存在",
        });
      }

      return {
        success: true,
        message: config.label + "删除成功",
      };
    } catch (error) {
      app.log.error("删除" + config.label + "失败:", error);
      return reply.code(500).send({
        success: false,
        error: "删除" + config.label + "失败",
      });
    }
  });

上传图片

src\apis\upload.js

js 复制代码
import fs from "fs/promises";
import path from "path";
import { fileURLToPath } from "url";
export default async function (app) {
  const __dirname = path.dirname(fileURLToPath(import.meta.url));

  // 从 apis 目录向上两级到达项目根目录,然后进入 uploads 目录
  const uploadDir = path.join(__dirname, "..", "..", "uploads");

  // 创建上传目录(如果不存在)
  try {
    await fs.access(uploadDir);
  } catch {
    await fs.mkdir(uploadDir, { recursive: true });
  }

  // 图片上传接口
  app.post("/image", async (request, reply) => {
    try {
      // 获取上传的文件
      const data = await request.file();

      // 验证文件是否存在
      if (!data) {
        return reply.code(400).send({ error: "未上传图片" });
      }
      // 验证文件类型(仅允许图片)
      const allowedMimeTypes = ["image/jpeg", "image/png", "image/webp"];
      if (!allowedMimeTypes.includes(data.mimetype)) {
        return reply.code(400).send({
          error: "不支持的文件类型,仅允许 jpg、png、webp",
        });
      }
      // 生成唯一文件名(避免覆盖)
      const fileName = `${Date.now()}-${Math.random()
        .toString(36)
        .substring(2, 10)}.${data.filename.split(".").pop()}`;
      const filePath = path.join(uploadDir, fileName);

      // 将文件流保存到磁盘
      try {
        // 使用@fastify/multipart提供的toBuffer方法处理文件
        const buffer = await data.toBuffer();
        await fs.writeFile(filePath, buffer);
      } catch (err) {
        throw err;
      }
      // 返回图片信息
      return {
        success: true,
        data: {
          fileName,
          originalName: data.filename,
          mimetype: data.mimetype,
          url: `/uploads/${fileName}`, // 访问图片的 URL 路径
          size: await fs.stat(filePath).then((stat) => stat.size),
        },
      };
    } catch (error) {
      app.log.error("图片上传失败:", error);
      return reply.code(500).send({ error: "图片上传失败" });
    }
  });
}

删除文件

src\apis\delFile.js

js 复制代码
import path from "path";
import fs from "fs/promises";
import { fileURLToPath } from "url";

export default async function (app) {
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
  // 从 apis 目录向上两级到达项目根目录,然后进入 uploads 目录
  const uploadDir = path.join(__dirname, "..", "..", "uploads");

  // 删除文件接口
  app.delete("/:filename", async (request, reply) => {
    try {
      const { filename } = request.params;

      // 验证文件名格式,防止路径遍历攻击
      if (!/^[\w\-]+\.(jpg|jpeg|png|gif|webp)$/.test(filename)) {
        return reply.code(400).send({
          success: false,
          message: "无效的文件名格式",
        });
      }

      const filePath = path.join(uploadDir, filename);

      console.log(">>>", filePath);

      // 检查文件是否存在
      try {
        await fs.access(filePath);
      } catch {
        return reply.code(404).send({
          success: false,
          message: "图片不存在",
        });
      }

      // 执行删除操作
      await fs.unlink(filePath);

      return {
        success: true,
        message: "图片已成功删除",
        filename,
      };
    } catch (error) {
      app.log.error("删除图片失败:", error);
      return reply.code(500).send({
        success: false,
        message: "服务器错误,删除图片失败",
      });
    }
  });
}

范例项目(开源地址)

https://gitee.com/sunshine39/fastify_server