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