likes
comments
collection
share

Fastify+TS实现基础IM服务(三)搭建Fastify+TS基础项目

作者站长头像
站长
· 阅读数 5

项目结构

在node项目中,下面是一个常见的组织结构。这种清晰、易扩展、易维护,是非常推荐的代码组织方式:

当然,某交友网站上还有形形色色的各种结构。有老哥觉得哪种不错可以给个项目地址,我也去学习一下

project-root/

├── src/                      # 项目的源代码
│   ├── plugins/              # Fastify 插件(用于共享功能,如数据库连接)
│   ├── routes/               # 路由定义目录
│   │   ├── __tests__/        # 路由的单元测试
│   │   └── index.ts          # 路由入口文件,用于汇总所有路由
│   │
│   ├── services/             # 业务逻辑层,可能还包含服务的模型(MVC中的Model)
│   ├── utils/                # 工具方法和通用函数
│   ├── config.ts             # 配置文件(环境变量等)
│   ├── webserver.ts             # Fastify 实例创建和配置
│   └── index.ts              # 应用的入口点

├── test/                     # 集成测试

├── public/                   # 静态文件,如图片、样式表、JavaScript 文件等

├── views/                    # 模板文件,如果使用了模板引擎

├── dist/                     # TypeScript 编译后生成的 JavaScript 文件

├── node_modules/             # Node.js 创建的项目依赖目录

├── .env                      # 环境变量文件
├── .gitignore                # Git 忽略的文件和目录
├── package.json              # 项目的元数据和依赖关系列表
├── tsconfig.json             # TypeScript 编译器的配置文件
└── README.md                 # 项目说明文件
  • Plugins(插件): 在 plugins 目录中,可以放置 Fastify 插件,这些插件可以用来封装功能并在整个应用中复用,比如数据库连接、认证插件等。

  • Routes(路由): routes 盔里包含应用的路由定义。每个路由可以有自己的处理函数、钩子(hooks)、选项等。通常每个文件定义一组相关的路由,例如 users.js 可以包含所有用户相关的路由。

  • Services(服务): services 目录包含业务逻辑,它们可以被路由处理程序调用。这些服务可以处理数据验证、数据库交互等任务。

  • Utilities(工具): utils 目录用于存放可在多个地方使用的通用代码,比如日期时间处理函数、自定义数据验证器等。

  • Configuration(配置): config.ts 文件用于集中管理应用配置,如数据库连接信息、第三方服务的 API 密钥等。

  • Server(服务器): server.ts 文件用于创建和配置 Fastify 实例,包括插件的注册、中间件、钩子等。

  • Application Entry(应用入口): index.ts 是应用程序的主入口文件,用于启动服务器。

代码风格检查

在TypeScript项目中加入ESLint需要几个步骤,包括安装必要的包、初始化ESLint配置文件以及根据需要调整配置。下面是一个详细的指南:

1. 安装必要的包

首先,需要安装eslint本身,以及一些支持TypeScript的插件和解析器。打开终端,切换到的项目目录,然后运行以下命令:

npm install eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin --save-dev

这里的包说明如下:

  • eslint: ESLint的核心包。
  • @typescript-eslint/parser: 一个解析器,允许ESLint理解TypeScript代码。
  • @typescript-eslint/eslint-plugin: 包含了一系列特定于TypeScript的linting规则。

2. 初始化ESLint配置文件

接下来,需要创建一个ESLint的配置文件。这可以通过运行ESLint的初始化命令来完成:

npx eslint --init

You can also run this command directly using 'npm init @eslint/config'.
✔ How would you like to use ESLint? · style
✔ What type of modules does your project use? · esm
✔ Which framework does your project use? · none
✔ Does your project use TypeScript? · No / Yes
✔ Where does your code run? · node
✔ How would you like to define a style for your project? · guide
✔ Which style guide do you want to follow? · standard-with-typescript
✔ What format do you want your config file to be in? · JavaScript
Checking peerDependencies of eslint-config-standard-with-typescript@latest
The config that you've selected requires the following dependencies:

eslint-config-standard-with-typescript@latest @typescript-eslint/eslint-plugin@^6.4.0 eslint@^8.0.1 eslint-plugin-import@^2.25.2 eslint-plugin-n@^15.0.0 || ^16.0.0  eslint-plugin-promise@^6.0.0 typescript@*
✔ Would you like to install them now? · No / Yes
A config file was generated, but the config file itself may not follow your linting rules.
Successfully created .eslintrc.js file in 

这个命令会引导通过一系列问题来创建一个基本的配置文件。对于TypeScript项目,确保选择:

  • “To check syntax, find problems, and enforce code style”(检查语法,发现问题,并强制代码风格)。
  • “TypeScript”作为的项目使用的语言。
  • 选择的项目是否运行在浏览器或Node.js环境中。
  • 选择喜欢的代码风格指南(如Airbnb、Standard或其他)。

这将生成一个.eslintrc.*文件(格式可能是JSON、YAML或JS),其中包含了的基本配置。

3. 调整ESLint配置(可选)

根据的项目需求,可能需要调整生成的ESLint配置文件。例如,可以在.eslintrc.*文件中添加@typescript-eslint插件和规则。这里是一个简单的配置示例:

{
  // 指定解析器,这里使用的是"@typescript-eslint/parser",它允许ESLint解析TypeScript代码
  "parser": "@typescript-eslint/parser",
  
  // "extends"字段列出了一系列的配置,这些配置定义了一组规则,您的项目将继承和遵循这些规则
  "extends": [
    "eslint:recommended", // 继承ESLint官方推荐的规则集,这为JavaScript代码提供了一套核心的静态检查规则
    "plugin:@typescript-eslint/eslint-recommended", // 调整一些来自eslint:recommended的规则,以更好地适应TypeScript
    "plugin:@typescript-eslint/recommended" // 应用来自@typescript-eslint插件的推荐规则,这些规则专门针对TypeScript代码的静态检查
  ],
  
  // "plugins"字段列出了项目中使用的插件,"@typescript-eslint"插件提供了针对TypeScript的规则和扩展
  "plugins": [
    "@typescript-eslint"
  ],
  
  // "env"定义了代码的运行环境,每种环境都预定义了一组全局变量
  "env": {
    "node": true, // 启用Node.js全局变量和Node.js作用域
    "es6": true // 启用ES6的新特性以及相应的全局变量(例如Set和Map)
  },
  
  // "parserOptions"提供给解析器的选项
  "parserOptions": {
    "ecmaVersion": 2020, // 指定ECMAScript版本,这里是2020,允许解析最新的ES2020语法
    "sourceType": "module" // 设置代码模块化方式,默认是"script",这里使用"module"表示使用ES6模块
  },
  
  // "rules"字段允许定义自己的规则或覆盖extends中的规则
  "rules": {
    // 自定义规则
    // 例如:"no-unused-vars": "warn",表示未使用的变量会被警告
  }
}

4. 添加Lint脚本到package.json

为了方便地运行ESLint,可以在package.jsonscripts部分添加一个lint脚本:

"lint": "eslint 'src/**/*.{js,ts}' --fix",

  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "prettier": "npx prettier --write **/*.ts",
    "lint": "eslint 'src/**/*.{js,ts}' --fix",
    "start": "node dist/index.js",
    "dev": "nodemon --watch src -e ts --exec ts-node --esm src/index.ts",
    "build": "tsc && tsc-alias"
  },

这样,就可以通过运行npm run lint来检查的项目中的TypeScript文件了。

5. 运行ESLint

最后,运行以下命令来检查的代码:

npm run lint

如果有任何问题,ESLint会列出它们。也可以使用--fix选项来自动修复一些问题:

npm run lint -- --fix

这些步骤应该能帮助在TypeScript项目中成功地集成ESLint。

自定义日志

Fastify内置日志

Fastify框架内置了pino作为其日志库。Pino是一个非常快速的Node.js日志库,它提供了多种日志级别(如infoerrordebug等),以及可配置的日志输出格式。Fastify选择Pino作为内置日志库的原因之一是其性能优异,对应用程序的性能影响较小。

使用Fastify时,可以在创建Fastify实例时配置日志选项,例如:

const fastify = require('fastify')({
  logger: true
})

这将启用内置的Pino日志记录,也可以通过传递更详细的配置来自定义日志行为,如指定日志级别、自定义序列化函数等。

为什么不使用console作为日志

在Node.js应用程序中,虽然使用console.log()console.error()等方法进行日志记录是最简单直接的方式,但这种做法在生产环境中通常不推荐,原因包括:

  1. 性能问题console.log()会导致同步写入操作,尤其是在高负载情况下,可能会显著影响应用程序的性能。
  2. 日志管理不便:使用console进行日志记录,缺乏灵活的日志级别控制,且不易于集中管理和查询。
  3. 格式化和序列化console方法输出的日志难以自定义格式,不利于将日志输出到文件或远程日志系统,并且在处理复杂对象时可能不如专业日志库灵活。

其他Node.js日志库

除了Pino之外,Node.js生态系统中还有许多其他优秀的日志库,可以根据项目需求选择使用,例如:

  • Winston:一个功能丰富的日志库,支持多种传输方式(如文件、控制台和远程服务),并允许自定义日志格式。
  • Bunyan:另一个流行的JSON日志库,提供了简单的API和丰富的日志级别,易于集成到各种系统中。

为什么还要单独创建一个日志实例

已经内置了pino日志了,为什么还要自定义一个pino日志呢?除了方便移植之外还为了不破坏现在fastify内置的日志。总结下来说,就是以下几点:

  1. 日志分离:自定义日志实例允许将应用日志与框架日志分开,便于管理和分析。
  2. 细致控制:提供更多自定义选项,如日志级别、格式化和旋转策略,满足特定需求。
  3. 第三方集成:方便与第三方日志服务集成,如Loggly或Datadog。
  4. 模块化重用:在多模块应用或微服务中,方便日志配置和行为的重用。
  5. 场景灵活性:适用于不仅限于HTTP请求处理的日志记录场景,如后台任务、心跳

首先,需要安装 pinopino-pretty,这两个库分别用于创建日志记录器和美化日志输出。

npm install pino pino-pretty
# 应该是不用装pino,fastify依赖中有这个。但是我没试过

然后,可以创建一个 logger.ts 文件来封装日志处理逻辑:

// src/logger.ts

import pino from 'pino';

// 确定当前的运行环境
const isProduction = process.env.NODE_ENV === 'production';

// 创建Pino日志实例
const logger = pino({
  // 基本配置
  base: {
    pid: false,
  },

  // 时间戳配置
  timestamp: pino.stdTimeFunctions.isoTime,

  // 生产中异步生成日志
  transport: isProduction
    ? undefined
    : {
        target: 'pino-pretty',
        options: { colorize: true },
      },
});

export default logger;

这段代码创建了一个 pino 日志实例,它配置了以下几点:

  1. transport: 使用 pino-pretty 来美化日志输出,使其在开发过程中更易于阅读。
  2. base: 默认情况下,Pino 会记录进程 ID (pid) 和主机名 (hostname)。在这里,我们设置 pid: false 来禁用进程 ID 的记录。
  3. timestamp: Pino 允许自定义时间戳的格式。在这里,我们使用了一个函数来返回当前时间的 ISO 字符串。

现在,可以在的应用程序中导入 logger 并使用它来记录日志:

import logger from './logger/logger';

logger.info('信息日志');
logger.error('错误日志');

这样,就有了一个简单的日志处理器,可以根据需要进一步扩展和自定义。例如,可以添加更多的配置项,如日志级别或者根据不同环境配置不同的日志处理策略。

配置管理

这里我们先试用yaml文档的方式来做默认配置,如果喜欢其他的方式可以自行修改一下。总体的逻辑是不变的:先获取当前环境,然后返回相应的配置

npm i yaml 
# 这个库本身是ts写的,不用安装额外的类型库

mkdir src/config
touch src/config/index.ts
touch src/config/config.d.ts

主要逻辑:

  1. 定义了 ConfigExtendedConfig 接口,其中 ExtendedConfig 允许包含任意额外的属性。
  2. 使用 getEnv 函数来确定当前的运行环境。
  3. getConfig 函数用于加载和解析指定环境对应的配置文件。如果已经加载过配置,它会从缓存中返回配置,否则会从文件系统中读取配置文件,解析它,并将其存储在缓存中以备后续使用。
  4. 如果在读取或解析配置文件过程中发生错误,程序将输出错误信息并终止执行。

类型注解和代码

类型注解

// src/config/config.d.ts
// 定义 Config 接口,用于描述配置文件的结构
interface Config {
  database: {
    host: string
    port: number
  }
}

// 定义 ExtendedConfig 接口,它继承自 Config 接口,并允许有任意的额外属性
export interface ExtendedConfig extends Config {
  // @ts-expect-error Use "@ts-expect-error" to ignore the next line error, and it will report an error itself if there's no error on the next line.
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  [key: string]: any
}

config代码

// src/config/index.ts
import { readFileSync } from "fs";
import { join } from "path";
import { parse } from "yaml";
import { ExtendedConfig } from "./config";

// 定义一个变量来缓存配置对象,初始值为 null
let configCache: ExtendedConfig | null = null;

// 定义 getEnv 函数,用于获取当前的运行环境,默认为 "dev"
export const getEnv = (): string => {
  return process.env.RUNNING_ENV || "dev";
};

// 定义 getConfig 函数,用于读取并解析配置文件,然后返回配置对象
export const getConfig = (type?: keyof ExtendedConfig) => {
  // 获取当前运行环境
  const environment = getEnv();

  // 如果配置已经被缓存,则直接返回缓存的配置
  if (configCache) {
    return type ? configCache[type] : configCache;
  }

  try {
    // 根据运行环境构造配置文件的路径
    const yamlPath = join(process.cwd(), `./src/config/.${environment}.yaml`);
    // 读取配置文件内容
    const file = readFileSync(yamlPath, "utf8");
    // 解析 YAML 文件内容为 JavaScript 对象
    const parsedConfig = parse(file) as ExtendedConfig;
    // 将解析后的配置对象缓存起来
    configCache = parsedConfig;
    // 返回请求的配置部分,如果没有指定则返回整个配置对象
    return type ? parsedConfig[type] : parsedConfig;
  } catch (error) {
    // 如果读取或解析配置文件失败,则打印错误信息并退出程序
    console.error(`Failed to read or parse the config file: ${error}`);
    process.exit(1); // 或者抛出异常,或者返回一个默认配置
  }
};

这段代码主要用于从 YAML 配置文件中读取配置信息。它首先尝试从缓存中获取配置,如果缓存不存在,则读取和解析 YAML 文件,将解析后的配置对象缓存起来,并根据需要返回整个配置对象或其特定部分。此外,它还包含了获取当前运行环境的功能,允许根据不同的环境加载不同的配置文件。

写之前还考虑过是不是要远程注册中心或者是配置热更新之类的,现在想想就好了

PS:默认异常退出这里可能不是一个好的方案,后面有机会再升级一下

http服务初始化

前面项目结构的时候提到过两个文件,index.ts和webserver.ts。

index.ts作为启动和引导整个应用的入口,而webserver.ts则专注于具体的业务逻辑处理。这种分离的模式有助于保持代码的组织性和可维护性,同时也便于测试和重用代码。

webserver.ts

这个文件专注于与HTTP服务相关的所有配置和初始化。它的主要职责包括:

  • 创建HTTP服务实例:通过fastify框架创建一个新的HTTP服务器实例,并启用内置的日志记录功能。
  • 定义路由:在这个例子中,它定义了一个简单的根路由"/",当访问这个路由时,返回{ hello: "world" }
  • 读取配置:从配置文件中获取应用程序相关的配置,如端口和主机地址。
  • 启动服务器:使用从配置文件中读取的端口和主机地址来启动HTTP服务器,并记录启动所需的时间。
  • 错误处理:在启动过程中遇到错误时,记录错误信息并退出进程。

这个文件的主要目的是封装与HTTP服务启动相关的所有细节,使得这个过程从应用程序的其它部分中解耦。

index.ts

这个文件作为应用程序的主入口点,负责协调整个应用程序的初始化过程。它的职责包括:

  • 初始化数据库连接:虽然在代码示例中没有具体实现,但通常这里会包含数据库连接的初始化代码。
  • 初始化其他服务:除了数据库之外,如果应用程序中还有其他需要在启动时初始化的服务,它们也会在这里初始化。
  • 启动HTTP服务器:调用webserver.ts中定义的startServer函数来启动HTTP服务。
  • 错误处理:如果在初始化过程中遇到任何错误,记录错误信息并退出进程。

index.ts文件的作用是作为应用程序启动的起点,它不仅仅限于启动HTTP服务器,还包括了应用程序所需的所有初始化步骤。

新增开发环境配置

# src/config/.dev.yaml
APP:
  name: "base-im"
  port: 3588 # 自己换一个也行,我3000被占了。以后会见到各种奇葩端口
  host: "0.0.0.0"
// 新的Config注解
// 定义 Config 接口,用于描述配置文件的结构
interface Config {
  database: {
    host: string
    port: number
  }
  app: {
    name: string
    port: number
    host: string
  }
}

http服务(webserver.ts)

// src/webserver.ts

import fastify from "fastify";
import { getConfig } from "./config";
import logger from "./logger";

// 创建 Fastify 应用实例,启用内置的日志记录功能
const app = fastify({
  logger: true,
});

// 定义一个异步函数来启动服务器
const startServer = async () => {
  const startTime = Date.now(); // 记录开始启动服务器的时间

  try {
    // 定义根路由,当访问 '/' 时返回 { hello: 'world' }
    app.get("/", async () => {
      return { hello: "world" };
    });

    // 从配置中获取应用程序信息
    const APP_INFO = getConfig("APP");
    // 启动服务器,监听配置中指定的端口和主机
    await app.listen({ port: APP_INFO.port, host: APP_INFO.host });

    const endTime = Date.now(); // 记录服务器启动完成的时间
    const startupTime = (endTime - startTime) / 1000; // 计算服务器启动耗时(秒)

    // 记录启动信息到日志
    logger.info(
      `Starting ${APP_INFO.name} server on ${APP_INFO.host}:${APP_INFO.port}`
    );
    // 记录启动耗时到日志
    logger.info(`Server started in ${startupTime} seconds.`);
  } catch (err) {
    // 如果启动过程中发生错误,则记录错误信息并退出进程
    app.log.error(err);
    process.exit(1);
  }
};
export { startServer };

入口文件(index.ts)

// src/index.ts

import logger from "./logger";
import { startServer } from "./webserver";

async function initializeApplication() {
  try {
    // 初始化数据库连接
    // 初始化其他服务
    // 启动Web服务器
    await startServer();
    logger.info('Server started successfully.');
  } catch (error) {
    logger.error('Failed to start the application:', error);
    process.exit(1);
  }
}

// 执行应用程序初始化
initializeApplication();

启动,提示成功后访问http://localhost:3588/

# 启动
npm run dev

> service@1.0.0 dev
> nodemon --watch src -e ts --exec ts-node --esm src/index.ts

[nodemon] 3.0.3
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): src/**/*
[nodemon] watching extensions: ts
[nodemon] starting `ts-node --esm src/index.ts`
{"level":30,"time":1706522637232,"pid":81804,"hostname":"ply***M","msg":"Server listening at http://0.0.0.0:3588"}
[18:03:57.233] INFO: Starting user-hub server on 0.0.0.0:3588
[18:03:57.233] INFO: Server started in 0.011 seconds.
[18:03:57.233] INFO: Server started successfully.

全局错误处理

全局错误处理在Web应用程序中非常重要,因为它帮助保持错误响应的一致性,减少代码重复,简化错误管理过程,提高应用的安全性,以及便于错误的调试和记录。在Fastify框架中,通过定义自定义错误类、创建和注册Fastify插件,可以实现高效且一致的全局错误处理机制。这不仅提升了开发效率,还能改善用户体验和应用的稳定性。

封装自定义错误类

这里的主要逻辑是,定义一个自定义错误类,然后封装好fastify插件函数,最后注册这个插件进行全局异常捕获。如果没有在领域中定义错误就会使用预先定义的错误类处理异常。

首先,需要安装 fastify-plugin ,这是一个官方提供的辅助库,用于创建可重用的插件模块。它的作用是简化了Fastify插件的开发流程,并确保插件可以正确地与Fastify的封装系统(encapsulation system)兼容。

npm install fastify-plugin
// src/errors/custom-error.ts

export class CustomError extends Error {
    // 状态码,用于HTTP响应
    statusCode: number;
    // 内部错误编号,可用于错误跟踪
    internalErrorNumber?: number;

    /**
     * 创建一个CustomError实例。
     * @param {string} message - 错误信息。
     * @param {number} statusCode - HTTP状态码。
     * @param {number} [internalErrorNumber] - 可选的内部错误编号。
     */
    constructor(message: string, statusCode: number, internalErrorNumber?: number) {
        // 调用基类的构造函数
        super(message);

        // 设置HTTP状态码
        this.statusCode = statusCode;
        // 设置内部错误编号(如果提供)
        this.internalErrorNumber = internalErrorNumber;

        // 恢复原型链
        Object.setPrototypeOf(this, new.target.prototype);
    }

    /**
     * 将错误信息转换为JSON对象,用于HTTP响应。
     * @returns {Object} 表示错误的JSON对象。
     */
    toJSON() {
        return {
            message: this.message, // 错误信息
            statusCode: this.statusCode, // HTTP状态码
            // 如果存在内部错误编号,则添加到JSON对象中
            ...(this.internalErrorNumber && { internalErrorNumber: this.internalErrorNumber }),
        };
    }
}

使用自定义错误类封装Fastify插件(中间件)

// src/plugins/error-handler-plugin.ts
import fp from 'fastify-plugin';
import { FastifyInstance, FastifyError, FastifyReply, FastifyRequest } from 'fastify';
import logger from '../../logger';
import { CustomError } from '../../error/error-handler';

async function errorHandlerPlugin(fastify: FastifyInstance) {
    fastify.setErrorHandler((error: FastifyError, request: FastifyRequest, reply: FastifyReply) => {
        // 如果错误是我们定义的CustomError,我们使用定义的状态码和消息
        if (error instanceof CustomError) {
            logger.error(error.toJSON());
            reply.status(error.statusCode).send(error.toJSON());
        } else {
            // 对于其他类型的错误,我们使用500状态码和通用消息
            logger.error(error);
            reply.status(500).send({ message: 'Something went wrong' });
        }
    });
}

// 使用 'fp' 创建插件,以支持封装和异步注册
export default fp(errorHandlerPlugin);

对于常见错误的封装

// src/errors/index.ts

import { CustomError } from "./custom-error";

// 定义一个 BadRequestError 类,表示 HTTP 400 错误。通常用于表示客户端请求错误。
export class BadRequestError extends CustomError {
  constructor(message = "Bad Request", internalErrorNumber?: number) {
    super(message, 400, internalErrorNumber);
  }
}

// 定义一个 UnauthorizedError 类,表示 HTTP 401 错误。用于需要用户认证的情况。
export class UnauthorizedError extends CustomError {
  constructor(message = "Unauthorized", internalErrorNumber?: number) {
    super(message, 401, internalErrorNumber);
  }
}

// 定义一个 ForbiddenError 类,表示 HTTP 403 错误。用于表示服务器拒绝执行此请求。
export class ForbiddenError extends CustomError {
  constructor(message = "Forbidden", internalErrorNumber?: number) {
    super(message, 403, internalErrorNumber);
  }
}

// 定义一个 NotFoundError 类,表示 HTTP 404 错误。用于请求的资源不存在。
export class NotFoundError extends CustomError {
  constructor(message = "Not Found", internalErrorNumber?: number) {
    super(message, 404, internalErrorNumber);
  }
}

// 定义一个 InternalServerError 类,表示 HTTP 500 错误。用于服务器内部错误。
export class InternalServerError extends CustomError {
  constructor(message = "Internal Server Error", internalErrorNumber?: number) {
    super(message, 500, internalErrorNumber);
  }
}

// 定义一个 BadGatewayError 类,表示 HTTP 502 错误。用于作为网关或代理工作的服务器收到无效响应。
export class BadGatewayError extends CustomError {
  constructor(message = "Bad Gateway", internalErrorNumber?: number) {
    super(message, 502, internalErrorNumber);
  }
}

// 定义一个 ServiceUnavailableError 类,表示 HTTP 503 错误。用于服务器暂不可用。
export class ServiceUnavailableError extends CustomError {
  constructor(message = "Service Unavailable", internalErrorNumber?: number) {
    super(message, 503, internalErrorNumber);
  }
}

// 定义一个 GatewayTimeoutError 类,表示 HTTP 504 错误。用于网关超时。
export class GatewayTimeoutError extends CustomError {
  constructor(message = "Gateway Timeout", internalErrorNumber?: number) {
    super(message, 504, internalErrorNumber);
  }
}

// 定义一个 NotImplementedError 类,表示 HTTP 501 错误。用于服务器不支持请求的功能。
export class NotImplementedError extends CustomError {
  constructor(message = "Not Implemented", internalErrorNumber?: number) {
    super(message, 501, internalErrorNumber);
  }
}

// 定义一个 TooManyRequestsError 类,表示 HTTP 429 错误。用于客户端发送的请求过多。
export class TooManyRequestsError extends CustomError {
  constructor(message = "Too Many Requests", internalErrorNumber?: number) {
    super(message, 429, internalErrorNumber);
  }
}

更新webserver

日志和配置完成之后,就可以启动一个hello word程序来体验一下

新增开发环境配置

# src/config/.dev.yaml
APP:
  name: "base-im"
  port: 3588 # 自己换一个也行,我3000被占了。以后会见到各种奇葩端口
  host: "0.0.0.0"

http服务

import fastify from "fastify";
import { getConfig } from "./config";
import logger from "./logger";
import errorHandlerPlugin from "./plugins/error-handler-plugin"; // 新增引入

// 创建 Fastify 应用实例,启用内置的日志记录功能
const app = fastify({
  logger: true,
});

// 定义一个异步函数来启动服务器
const startServer = async () => {
  const startTime = Date.now(); // 记录开始启动服务器的时间

  try {
    // 定义根路由,当访问 '/' 时返回 { hello: 'world' }
    app.get("/", async () => {
      return { hello: "world" };
    });

    // 注册自定义的错误处理插件
    await errorHandlerPlugin(app); // 新增注册插件

    // 从配置中获取应用程序信息
    const APP_INFO = getConfig("APP");
    // 启动服务器,监听配置中指定的端口和主机
    await app.listen({ port: APP_INFO.port, host: APP_INFO.host });

    const endTime = Date.now(); // 记录服务器启动完成的时间
    const startupTime = (endTime - startTime) / 1000; // 计算服务器启动耗时(秒)

    // 记录启动信息到日志
    logger.info(
      `Starting ${APP_INFO.name} server on ${APP_INFO.host}:${APP_INFO.port}`
    );
    // 记录启动耗时到日志
    logger.info(`Server started in ${startupTime} seconds.`);
  } catch (err) {
    // 如果启动过程中发生错误,则记录错误信息并退出进程
    app.log.error(err);
    process.exit(1);
  }
};
export { startServer };

总结一下

虽然内容质量和数量都比较一般,但是目前这篇文章涵盖了项目的组织、代码风格、日志记录、配置管理、HTTP服务初始化和全局错误处理六大核心要素。后续

当前目录结构

project-root/

├── src/                      # 项目的源代码
│   ├── config/               # 配置文件目录
│   │   ├── config.d.ts       # 配置的TypeScript声明文件,用于类型安全
│   │   └── index.ts          # 配置文件的实现,负责加载和导出配置
│   │
│   ├── errors/               # 错误处理相关的目录
│   │   ├── custom-error.ts   # 自定义错误类,用于创建统一的错误响应
│   │   └── index.ts          # 错误处理的入口文件,可能用于汇总和导出错误类
│   │
│   ├── plugins/              # Fastify插件目录
│   │   └── error-handler-plugin.ts # 错误处理插件,用于全局错误处理
│   │
│   ├── logger.ts             # 日志配置文件,定义日志记录方式和配置
│   ├── webserver.ts          # Fastify服务器设置和启动逻辑
│   └── index.ts              # 应用入口文件,用于启动服务器和其他初始化设置

├── package.json              # 定义项目依赖和脚本的npm配置文件
├── package-lock.json         # 锁定安装时的包的版本,确保一致性
├── tsconfig.json             # TypeScript的编译配置文件

项目结构详细说明:

  • config/: 包含应用的配置逻辑和类型声明。这里的配置可能包括数据库连接信息、应用密钥等,利用TypeScript以确保类型安全。

  • errors/: 提供一个中心化的错误处理机制。custom-error.ts 定义了一个或多个自定义错误类,而 index.ts 可能用于导出所有错误类,以便在其他文件中使用。

  • plugins/: 存放Fastify插件。error-handler-plugin.ts 是一个专门用于错误处理的插件,它可能会捕获应用中的异步错误,并返回统一的错误响应。

  • logger.ts: 定义应用的日志记录策略。这可能包括配置不同的日志级别、日志格式以及日志的存储位置等。

  • webserver.ts: 包含Fastify服务器的配置和启动逻辑。这里设置了服务器的各种选项,如端口号、插件注册、路由以及启动服务器等。

  • index.ts: 作为应用的主入口点,负责初始化和启动整个应用。这里可能包括调用 webserver.ts 来启动服务器,以及其他全局初始化逻辑。