# 前言
今天我们来尝试使用腾讯云 Serverless 的事件触发器函数,并且用 Nest 来做云函数执行体.
采用 Serverless 部署项目后,由于用户每次访问才会创建程序响应,响应结束之后又自动销毁
那么如果是像诸如异步队列这类需求,就可以使用事件触发函数.
# 创建腾讯云消息队列 TDMQ
腾讯云 Serverless 目前提供了几种事件触发方式
- 定时触发
- COS 触发
- CMQ 主题订阅触发
- Ckafkac 触发
- API 网关触发
- CLS 日志触发
- MPS 触发
- CLB 触发
- TDMQ 消息队列触发
之前我们上传的项目则都是通过用户访问,即 API 网关触发的
今天我们来部署一个采用 TDMQ 消息队列触发的云函数
# 创建 Pulsar 版集群,命名空间,Topic, 订阅者
创建的东西有点多,在腾讯云后台点点鼠标就可以了,在这里可以看到价格,用量少的话基本上是免费的
- 调用费: 0.02 元 / 万次 (按阶梯计费) 
- 提示:每月所有集群累计免费额度 1000 万 次 
- 存储费: 0.0025 元 / GB / 小时 
- 提示:每月所有集群累计免费额度 1024MB 
注意的地方就是 Topic 需要创建一个 持久化 的主题
在 Topic 管理中再新增一个订阅,订阅名字可以自取
一切准备好之后还需要进入 角色管理 ,针对命名空间进行授权,目的是要获取 秘钥 ,将来在提交消息到队列需要用到.
# 创建新的云函数,并设置触发器为 TDMQ
# 创建云函数
我们创建一个简单的云函数,用以监听刚才创建的 Topic, 有消息来我们就发送一封邮件
首先写一个简单的,有消息过来我们就 console 打印出来
创建文件夹 sender , 里面只有 2 个文件, index.js 和 serverless.yml
| $ mkdir sender | |
| $ touch ./sender/index.js | |
| $ touch ./sender/serverless.yml | 
index.js 内容很简单
| 'use strict'; | |
| exports.main_handler = async (event, context) => { | |
| console.log("Hello World") | |
| console.log(event) | |
| console.log(context) | |
|     return event | |
| }; | 
serverless.yml
| app: sender | |
| component: scf | |
| name: sender | |
| stage: dev | |
| inputs: | |
| description: 发送邮件 | |
| eip: false | |
| ignoreTriggers: true | |
| handler: index.main_handler | |
| initTimeout: 65 | |
| memorySize: 128 | |
| msgTTL: 21600 | |
| name: sender | |
| namespace: default | |
| publicAccess: true | |
| region: ap-shanghai | |
| retryNum: 2 | |
| runtime: Nodejs16.13 | |
| src: | |
| exclude: | |
|       - .env | |
|       - node_modules | |
| src: ./ | |
| timeout: 3 | |
| type: event | |
| vpcConfig: | |
| subnetId: subnet-asy3z93z | |
| vpcId: vpc-b5776770 | 
这里先不创建触发器 ( ignoreTriggers: true ), 因为使用 Serverless.yml 创建不了 TDMQ 触发器
创建 package.json 和 .env (之前的文章有所说明)
随后执行 npm run deploy
| $ npm run deploy | |
| > sender@1.0.0 deploy | |
| > sls deploy | |
| serverless ⚡tencent | |
| Action: "deploy" - Stage: "dev" - App: "sender" - Name: "sender" | |
| code: | |
| bucket: sls-cloudfunction-ap-shanghai-code | |
| object: /scf_component_i2594ce-1661844005.zip | |
| description: 发送邮件 | |
| functionName: sender | |
| handler: index.main_handler | |
| lastVersion:           $LATEST | |
| memorySize:            128 | |
| namespace: default | |
| runtime: Nodejs16.13 | |
| sourceCodeDownloadUrl: https://sls-app-code-prod-1300862921.cos.ap-guangzhou.myqcloud.com/1252902543%2Fdev%2Fsender%2Fsender%2Fsource.zip?sign=q-sign-algorithm%3Dsha1%26q-ak%3DAKIDfBS1NUcuMcgVSTqzRhiX6lBUbY4KeLXW%26q-sign-time%3D1661844013%3B1661847613%26q-key-time%3D1661844013%3B1661847613%26q-header-list%3Dhost%26q-url-param-list%3D%26q-signature%3D1b65218fa54d616971a3ea4a1f13ce1971e6fc18 | |
| traffic:               1 | |
| triggers: (empty array) | |
| type: event | |
| 应用控制台: https://serverless.cloud.tencent.com/apps/sender/sender/dev | |
| 13s »sender» 执行成功 | 
部署成功之后需要在后台创建触发器
# 绑定触发器
进入函数服务 - 选择 sender 云函数 - 触发管理 点击创建触发器

在这里选择刚才创建的 Topic 和订阅消费者
# 测试事件触发云函数
可以先来测试一下,进入到 Topic 管理界面,点击发送消息

进入云函数日志管理可以查看相关日志
| 日志: | |
| START RequestId: 32341b89-2836-11ed-a7ab-525400a75f9f | |
| Hello World | |
| { | |
|   data: { | |
|     msgBody: '{"user":"Mr.adam","email":"1015517471@qq.com"}', | |
| msgId: '\b���[\x10\x00\x18\x01 \x00', | |
| subscriptionName: 'sendToMy', | |
| tags: '', | |
| timestamp: 1661844875242, | |
| topic: 'persistent://pulsar-jm4bd43m2jab/sendmail/sendmail', | |
| topicType: 2 | |
| }, | |
| datacontenttype: 'application/json;charset=utf-8', | |
| id: '8efd64c4-09f5-4673-acfd-94224522638e', | |
| region: 'ap-shanghai', | |
| source: 'tdmq.cloud.tencent', | |
| specversion: '1.0', | |
| status: '', | |
| subject: 'qcs::tdmq:ap-shanghai:uin/1015517471:subscriptionName/pulsar-jm4bd43m2jab/sendmail/sendmail/sendToMy', | |
| tags: null, | |
| time: 1661844875277, | |
| type: 'connector:tdmq' | |
| } | |
| { | |
| succeed: [Function: succeed], | |
| fail: [Function: fail], | |
| callbackWaitsForEmptyEventLoop: [Getter/Setter], | |
| getRemainingTimeInMillis: [Function: getRemainingTimeInMillis], | |
| memory_limit_in_mb: 128, | |
| time_limit_in_ms: 3000, | |
| request_id: '32341b89-2836-11ed-a7ab-525400a75f9f', | |
|   environment: '{"SCF_NAMESPACE":"default"}', | |
| environ: 'SCF_NAMESPACE=default;SCF_NAMESPACE=default', | |
| function_version: '$LATEST', | |
| function_name: 'sender', | |
| namespace: 'default', | |
| tencentcloud_region: 'ap-shanghai', | |
| tencentcloud_appid: '1252902543', | |
| tencentcloud_uin: '1015517471' | |
| } | 
我们刚刚发送的内容正确地被我们接收到了!
# 部署 Nest 独立应用
如果只是单纯发送一封邮件,单个 js 文件或许可以满足需求,但是往往我们的任务会很复杂,比如连接数据库或消息队列,记录日志等操作,这样如果把所有东西都放到一个文件里执行就有点麻烦了,所以需要一个框架来帮我们处理这一类问题,该框架不用监听任何端口,只需要它执行便可,执行完自动销毁
刚好 Nest 框架可以满足这部分需求,接下来修改 sender 云函数为一个 Nest 框架,并且完成发送邮件的相关模块吧
# 安装 Nest 及相关模块
安装一个新的 Nest 项目
| $ nest new sender | 
进入 sender 目录中,安装使用 Nest 发送邮件的库
| $ npm install nestjs-mailer nodemailer -S | |
| $ npm install @types/nodemailer -D | 
# 创建 mailer 模块
| $ nest g mo mailer | |
| $ nest g s mailer | 
# 修改 main.ts
目前项目是事件驱动,所以我们的项目不需要监听任何端口,执行其中的方法即可
| import { NestFactory } from '@nestjs/core'; | |
| import { AppModule } from './app.module'; | |
| import { MailerService } from "./mailer/mailer.service"; | |
| export default async function bootstrap(body:{user:string,email:string}) { | |
|   // 这里使用 createApplicationContext 加载 AppModule | |
| const app = await NestFactory.createApplicationContext(AppModule); | |
| const mailerService = app.get(MailerService); | |
| await mailerService.sendMail(body); | |
|   // 这里进行销毁实例操作 | |
| await app.close(); | |
| } | 
# 集成 nestjs-mailer
在 MailerModule 中集成 nestjs-mailer 配置
这里使用我的 QQ 邮箱 SMTP 来发送邮件
| import { Module } from '@nestjs/common'; | |
| import { MailerModule as Mailer } from 'nestjs-mailer'; | |
| import { MailerService } from './mailer.service'; | |
| @Module({ | |
| imports:[ | |
| Mailer.forRoot({ | |
| config: { | |
| transport: { | |
| host: 'smtp.qq.com', | |
| port: 465, | |
| secure: true, | |
| auth: { | |
| user: '1015517471@qq.com', | |
| pass: 'xxxxxxx', | |
|           } | |
| }, | |
| defaults: { | |
| from: '"Mr.adam" <1015517471@qq.com>', | |
| }, | |
| }, | |
| }), | |
| ], | |
| providers: [MailerService] | |
| }) | |
| export class MailerModule {} | 
mailer.service.ts
| import { Injectable } from '@nestjs/common'; | |
| import { InjectMailer, Mailer, template } from 'nestjs-mailer'; | |
| @Injectable() | |
| export class MailerService { | |
| constructor( | |
| @InjectMailer() private readonly mailer: Mailer, | |
| ) {} | |
|     // 发送邮件 | |
| async sendMail(body){ | |
| this.mailer.sendMail({ | |
| from: '"Mr.adam" <1015517471@qq.com>', | |
| to: body.email, | |
| subject: 'Hello ✔', | |
| text: `Hello ${body.user},This is a test mail!` | |
| }).catch(e => console.log(e)); | |
| console.log("发送成功"); | |
|     } | |
| } | 
index.js 文件是事件函数的入口文件,我们也要响应做出修改,让它初始化 nest 框架,并且把 event.data.msgBody 传输过去
| 'use strict'; | |
| const bootstrap = require("./dist/main.js"); | |
| exports.main_handler = async (event, context) => { | |
| await bootstrap.default(JSON.parse(event.data.msgBody)); | |
|     return event | |
| }; | 
现在编写一个 test.js 测试一下
首先执行 npm run build 打包 nest 项目
| //test.js | |
| const {main_handler} = require("./index.js"); | |
| const data = { | |
| data: { | |
| msgBody: '{"user":"Mr.adam","email":"1015517471@qq.com"}', | |
|     } | |
| }; | |
| main_handler(data,"context"); | 
本地先执行一下测试脚本
| $ node .\test.js | |
| [Nest] 13404 - 2022/08/30 17:03:30 LOG [NestFactory] Starting Nest application... | |
| [Nest] 13404 - 2022/08/30 17:03:30 LOG [InstanceLoader] MailerModule dependencies initialized +28ms | |
| [Nest] 13404 - 2022/08/30 17:03:30 LOG [InstanceLoader] MailerCoreModule dependencies initialized +0ms | |
| [Nest] 13404 - 2022/08/30 17:03:30 LOG [InstanceLoader] MailerModule dependencies initialized +0ms | |
| [Nest] 13404 - 2022/08/30 17:03:30 LOG [InstanceLoader] AppModule dependencies initialized +1ms | |
| object | |
| 发送成功 | 
OK, 成功收到邮件!

# 部署云函数
# 部署 layer
之前的文章有详细讲过,这段就略过了...
### 修改 serverless.yml
| app: sender | |
| component: scf | |
| name: sender | |
| stage: dev | |
| inputs: | |
| description: 发送邮件 | |
| eip: false | |
| ignoreTriggers: true | |
| handler: index.main_handler | |
| initTimeout: 65 | |
| memorySize: 128 | |
| msgTTL: 21600 | |
| name: sender | |
| namespace: default | |
| publicAccess: true | |
| region: ap-shanghai | |
| retryNum: 2 | |
| runtime: Nodejs16.13 | |
| layers: | |
| - name: sender-layer | |
| version: 1 | |
| src: | |
| dist: ./ | |
| exclude: | |
|       - .env | |
|       - node_modules | |
|       - layer | |
| src: ./ | |
| timeout: 3 | |
| type: event | |
| vpcConfig: | |
| subnetId: subnet-asy3z93z | |
| vpcId: vpc-b5776770 | 
# 执行部署
部署之前修改下 package.json
| $ npm run deploy | |
| serverless ⚡tencent | |
| Action: "deploy" - Stage: "dev" - App: "sender" - Name: "sender" | |
| code: | |
| bucket: sls-cloudfunction-ap-shanghai-code | |
| object: /scf_component_9syxhj-1661851211.zip | |
| description: 发送邮件 | |
| functionName: sender | |
| handler: index.main_handler | |
| lastVersion:           $LATEST | |
| layers: | |
| - | |
| name: sender-layer | |
|     version: 1 | |
| memorySize:            128 | |
| namespace: default | |
| runtime: Nodejs16.13 | |
| sourceCodeDownloadUrl: https://sls-app-code-prod-1300862921.cos.ap-guangzhou.myqcloud.com/1252902543%2Fdev%2Fsender%2Fsender%2Fsource.zip?sign=q-sign-algorithm%3Dsha1%26q-ak%3DAKIDfBS1NUcuMcgVSTqzRhiX6lBUbY4KeLXW%26q-sign-time%3D1661851223%3B1661854823%26q-key-time%3D1661851223%3B1661854823%26q-header-list%3Dhost%26q-url-param-list%3D%26q-signature%3D1b65289dfdde661673d9a010e54b8e90b229657c | |
| traffic:               1 | |
| triggers: (empty array) | |
| type: event | |
| 应用控制台: https://serverless.cloud.tencent.com/apps/sender/sender/dev | |
| 15s »sender» 执行成功 | 
部署完成我们可以再到 Topic 管理发送一条消息测试一下,结果也是成功收到邮件!
至此,我们使用 Nest 独立应用接收到了来自 TDMQ 的通知消息
如何通过 TDMQ 发送消息则见下篇博文!
