likes
comments
collection
share

NestJS小技巧23-如何使用工作队列处理Nestjs服务器中的电子邮件发送

作者站长头像
站长
· 阅读数 8
by 雪隐 from https://juejin.cn/user/1433418895994094
本文欢迎分享与聚合,全文转载就不必了,尊重版权,圈子就这么大,若急用可联系授权

原文链接

在这个现代时代,当用户使用你的软件时,他们希望每次做出动作时都会收到通知,有时是由他或其他用户发出的,有很多方法可以通知用户,例如发送推送通知、电子邮件或短信。尽管如此,和其他的方式相比发送电子邮件是最常用的手段,经常用在新用户注册,密码重置等等。

在这篇文章中,我们将会看到怎么在Nestjs应用程序中发送邮件,为了做到这些,我们也将使用工作队列(Job Queue)。

什么是工作队列,为什么它有用?

队列是一种数据结构用来执行某些需要被组织且高效的任务,它遵循先进先出原则FIFO(First in First Out)意思是先加进这个队列的数据总是将被先执行。另一方面,工作队列是一种受队列数据结构启发的机制,用于处理常见的应用程序扩展和性能挑战。

它通常用于后台处理,在需要异步执行耗时或资源消耗任务的应用程序中,可以使用工作队列将工作转载到单独的进程或线程。这允许主应用程序在后台执行任务时保持响应。它也适用于发送邮件,想想下成千上万的用户在同一时间注册,然后您必须发送欢迎邮件给每个人,如果电子邮件发送由主进程处理,这种情况可能会减慢你的应用程序的速度。相反,你可以使用一个Job队列来存储每个电子邮件发送任务,并在不同于主进程的线程中处理它们。

NestJS小技巧23-如何使用工作队列处理Nestjs服务器中的电子邮件发送

总的来说,我们想构建上面插图一样的东西。client相当于我们的NestJS应用程序,它会将工作添加到队列中然后我们通过某些进程从storage来获取这些工作任务然后执行他们。

必要条件

为了构建我们的项目我们需要满足下面这些条件

  • SMTP服务来发送我们的邮件
  • 处理工作队列的Nodejs依赖包
  • 处理邮件发送的Nodejs依赖包

SMTP 服务

我直接用了我的QQ,大家根据自己的情况来设置邮件。

工作队列处理

和您们想的一样,我将要在这篇文章中使用NestJS,NestJS提供包去处理一些共通的概念比如ORM,工作队列,定时任务等等一些流行的包,它帮助您的应用程序让这些特性以一种友好的方式成为一体。

对于工作队列,NestJS提供了名为@nestjs/bull包,作为bull之上的抽象/包装器,bull是一个流行的、受良好支持的高性能节点。实现基于js的队列系统。

Bull使用Redis来持久化工作数据,所以您需要在您的电脑上安装Redis.

当使用工作队列的时候,有2个概念必须要先理解。它们是生产者存储,和 工人处理者

  • 生产者Producers:他们的角色是把工作加入到队列中去。当往队列加入一个工作的时候,您也必须加入可以有效的执行这个工作的数据。
  • 工人或处理者Workers or Processors:他们的角色是处理被加进队列的工作。它们在独立于主应用程序进程的进程上运行。
  • 队列存储Queue storage:存储作业的存储器,bull使用Redis作为其队列存储器。

现在让我们码一些代码:

创建一个新的NestJS项目:

nest new job-queue-example

安装包:

npm install --save @nestjs/bull bull

一旦安装完成,我们就能够把BullModule在rootAppModule中导入。

import { BullModule } from '@nestjs/bull';
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [
    BullModule.forRoot({
      redis: {
        host: 'localhost',
        port: 6379,
      },
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

forRoot()方法是用来注册一个bull包配置对象,这个对象在应用程序中被所有的队列注册,就像我上面提到的那样,bull需要Redis来存储队列所以我必须提供Redis链接信息。

当这个包被设定后,下一步是去注册队列。它完全可能是在一个项目中存在多个队列,每个队列将会处理不同类型的工作,比如我现在例子发送邮件,但是您的app也能够处理另外的工作比如通知和推送消息,处理文件等等。

对于队列注册,我们使用BullModule.regirsterQueue(),它应该被添加到将作业添加到队列的模块中。由于我们希望本教程尽可能简短,我们不会创建另一个模块,我们只会使用应用程序模块,所以让我们再次编辑app.module文件:

import { BullModule } from '@nestjs/bull';
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [
    BullModule.forRoot({
      redis: {
        host: 'localhost',
        port: 6379,
      },
    }),
    BullModule.registerQueue({
      name: 'emailSending',
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

每个队列在它的名称属性下都是唯一的。一个队列名称既可以用来添加也可以用来处理工作,我们将会在下一部份进行说明。

这一步之后,我们已经配置好了队列包,并且创建了一个发送邮件的队列,现在是时候来创建生产者处理者了.

生产者

上文提到过,生产者添加工作到队列,在NestJS中,这基本上是应用程序Services(一个用@Injectable装饰器修饰的类)

为了将工作加入队列,首先,像下面这样将队列注入进Service

import { InjectQueue } from '@nestjs/bull';
import { Injectable } from '@nestjs/common';
import { Queue } from 'bull';

@Injectable()
export class AppService {
  constructor(
    @InjectQueue('emailSending') private readonly emailQueue: Queue,
  ) {}
}

队列是通过名字来识别了,所以这里的名字必须和注册时候的名字一样。

创建工作数据的接口

export interface Mail {
  from: string;
  to: string;
  subject: string;
  text: string;
  [key: string]: any;
}

一旦队列被注入进了应用程序Service中,我可以创建可以添加工作的方法。

import { InjectQueue } from '@nestjs/bull';
import { Injectable } from '@nestjs/common';
import { Queue } from 'bull';
import { Mail } from './mail.interface';

@Injectable()
export class AppService {
  constructor(
    @InjectQueue('emailSending') private readonly emailQueue: Queue,
  ) {}

  async sendEmail(data: Mail) {
    const job = await this.emailQueue.add({ data });

    return { jobId: job.id };
  }
}

我创建了sendEmail方法使用emailQueue来添加工作 。这些被处理者接收参数用来发送邮件,它可以是邮件地址,邮件内容,和其他不同的信息。

被命名的工作:工作需要有一个名字,这允许我们创建特定的工作,想象一下我们的应用程序必须发送不同类型的电子邮件,例如,我们可以发送欢迎电子邮件(当新用户注册时)或重置密码电子邮件(当用户想要重置密码时),我们不希望这些工作由同一消费者处理。

import { InjectQueue } from '@nestjs/bull';
import { Injectable } from '@nestjs/common';
import { Queue } from 'bull';
import { Mail } from './mail.interface';

@Injectable()
export class AppService {
  constructor(
    @InjectQueue('emailSending') private readonly emailQueue: Queue,
  ) {}

  async sendWelcomeEmail(data: Mail) {
    const job = await this.emailQueue.add('welcome', { data });

    return { jobId: job.id };
  }

  async sendResetPasswordEmail(data: Mail) {
    const job = await this.emailQueue.add('reset-password', { data });

    return { jobId: job.id };
  }
}

处理者 或 消费者 或 工作者

上文提到,处理者处理从队列出来的工作任务,在NestJS中它们是用@nestjs/bull包中@Processer装饰器装饰的类。这个装饰器收到队列中该名字的内容并进行消费。

让我们创建命名为EmailProcesser的文件,然后在里面创建一个相同名字和装饰器前缀的类。

import { Processor } from '@nestjs/bull';

@Processor('emailSending')
export class EmailProcessor {}

现在我们有了自己的处理者类,让我们创建处理每个任务的方法。

import { Process, Processor } from '@nestjs/bull';
import { Job } from 'bull';

@Processor('emailSending')
export class EmailProcessor {
  @Process('welcome')
  async sendWelcomeEmail(job: Job<Mail>) {
    const { data } = job;

    // send the welcome email here
  }

  @Process('reset-password')
  async sendResetPasswordEmail(job: Job<Mail>) {
    const { data } = job;

    // send the reset password email here
  }
}

@Process@Nestjs/bull提供的装饰器,它可以帮助我们创建特定的处理程序,在上一节中,我们根据要发送的电子邮件类型创建了不同类型的作业,我们也必须对流程进行同样的操作。

每当有个reset-password类型的人物加入到队列中,reset-password处理者将会被调用,welcome处理也是一样的。

这就是所有的工作队列设定。

发送邮件

您也许发现在之前的内容里我没有写发送邮件的逻辑,我只是添加了可以被填写内容。那是因为发送邮件是通过设定别的包来处理的。

我将要使用@nestjs-modules/mailer,它能使NestJS具备发送邮件的能力,这个包是基于Nodemailer的。

安装依赖

npm i @nestjs-modules/mailer nodemailer 

更新app.module配置Mailer模块

import { MailerModule } from '@nestjs-modules/mailer';
import { BullModule } from '@nestjs/bull';
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [
    BullModule.forRoot({
      redis: {
        host: 'localhost',
        port: 6379,
      },
    }),
    BullModule.registerQueue({
      name: 'emailSending',
    }),
    // 导入MailerModule
    MailerModule.forRoot({
      transport: {
        host: 'smtp.example.com',
        port: 587,
        auth: {
          user: 'username',
          pass: 'password',
        },
      },
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

这里我们导入了MailerModule,您需要把您自己的SMTP服务信息写进去,包括认证信息。

当这个包被设定,它将会提供邮件服务类里面包含sendEmail方法他会接口几个参数然后发送邮件。但是首先,我们先要使用handlebars创建一个邮件模版.他允许我用动态的参数来创建一个HTML模版,如果 用户名字 或者 其他信息 也能为我们邮件写行内样式/

安装Haandlebars

npm i handlebars

之后我需要创建在src中创建一个文件夹用来存放我的模版

NestJS小技巧23-如何使用工作队列处理Nestjs服务器中的电子邮件发送

当我们做完这些,别忘了更新2个文件nestjs-cli.jsonapp.module去通知邮件模块在哪里能找到这些模版。

{
  "$schema": "https://json.schemastore.org/nest-cli",
  "collection": "@nestjs/schematics",
  "sourceRoot": "src",
  "compilerOptions": {
    // 加入资源配置
    "assets": ["templates/**"],
    "deleteOutDir": true
  }
}

然后更新app.module

import { MailerModule } from '@nestjs-modules/mailer';
import { HandlebarsAdapter } from '@nestjs-modules/mailer/dist/adapters/handlebars.adapter';
import { BullModule } from '@nestjs/bull';
import { Module } from '@nestjs/common';
import { join } from 'path';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [
    BullModule.forRoot({
      redis: {
        host: 'localhost',
        port: 6379,
      },
    }),
    BullModule.registerQueue({
      name: 'emailSending',
    }),
    MailerModule.forRoot({
      transport: {
        host: 'smtp.example.com',
        port: 587,
        auth: {
          user: 'username',
          pass: 'password',
        },
      },
      template: {
        dir: join(__dirname, 'templates'),
        adapter: new HandlebarsAdapter(),
      },
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

现在所有的设定都好了,我可以更新我的工作队列来处理发送邮件了:

import { MailerService } from '@nestjs-modules/mailer';
import { Process, Processor } from '@nestjs/bull';
import { Job } from 'bull';
import { Mail } from './mail.interface';

@Processor('emailSending')
export class EmailProcessor {
  constructor(private readonly mailService: MailerService) {}
  
  @Process('welcome')
  async sendWelcomeEmail(job: Job<Mail>) {
    const { data } = job;

    await this.mailService.sendMail({
      ...data,
      subject: 'Welcome',
      template: 'welcome',
      context: {
        user: data.user,
      },
    });
  }

  @Process('reset-password')
  async sendResetPasswordEmail(job: Job<Mail>) {
    const { data } = job;

    await this.mailService.sendMail({
      ...data,
      subject: 'Reset password',
      template: 'reset-password',
      context: {
        user: data.user,
      },
    });
  }
}

首先,我们在email.prrocessor上注入了包提供的mailService,这允许我们使用内置的方法sendEmail。

这个方法需要一些参数才能发送邮件,这些参数已经在我们的接口中被定义了。

NestJS小技巧23-如何使用工作队列处理Nestjs服务器中的电子邮件发送

设定完这些数据以后,我也需要设定模版。如果要将一些变量传递给模板进行自定义或显示特定信息,则使用其余的名为context的属性。

我们已经走了很长的路,为了测试的目的,这里最后要做的就是创建一个触发电子邮件发送的端点。

import { Body, Controller, Post } from '@nestjs/common';
import { AppService } from './app.service';
import { Mail } from './dto/mail.interface';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Post()
  async sendEmail(@Body() data: Mail) {
    await this.appService.sendWelcomeEmail(data);
  }
}

最后,每次用户用数据调用这个端点时,控制器的方法都会通过向其传递所需的数据来调用服务,然后服务会根据作业类型将作业添加到队列和我们流程的最后一个队列中,获得作业,然后执行(发送电子邮件)。

您可以在这里看到全部的代码

总结

总之,利用作业队列来处理NestJS应用程序中的电子邮件发送提供了几个好处。通过将电子邮件发送过程与请求-响应周期解耦,可以提高应用程序的响应能力和性能。作业队列支持异步处理电子邮件任务,使您的应用程序能够在后台处理电子邮件作业时快速响应用户请求。

我希望您喜欢这篇文章,就像我喜欢为您写这篇文章一样。