## 定时任务
定时任务允许你按照指定的日期/时间、一定时间间隔或者一定时间后单次执行来调度(`scheduling`)任意代码(方法/函数)。在`Linux`世界中,这经常通过操作系统层面的`cron`包等执行。在`Node.js`应用中,有几个不同的包可以模拟 cron 包的功能。Nest 提供了`@nestjs/schedule`包,其集成了流行的 Node.js 的`node-cron`包,我们将在本章中应用该包。
### 安装
我们首先从安装需要的依赖开始。
~~~bash
$ npm install --save @nestjs/schedule
$ npm install --save-dev @types/cron
~~~
要激活工作调度,从根`AppModule`中导入`ScheduleModule`并运行`forRoot()`静态方法,如下:
> app.module.ts
```typescript
import { Module } from '@nestjs/common';
import { ScheduleModule } from '@nestjs/schedule';
@Module({
imports: [ScheduleModule.forRoot()],
})
export class AppModule {}
```
`.forRoot()`调用初始化调度器并且注册在你应用中任何声明的`cron jobs`,`timeouts`和`intervals`。注册开始于`onApplicationBootstrap`生命周期钩子发生时,保证所有模块都已经载入,任何计划工作已经声明。
### 声明计时工作(cron job)
一个计时工作调度任何函数(方法调用)以自动运行, 计时工作可以:
- 单次,在指定日期/时间
- 重复循环:重复工作可以在指定周期中指定执行(例如,每小时,每周,或者每 5 分钟)
在包含要运行代码的方法定义前使用`@Cron()`装饰器声明一个计时工作,如下:
```typescript
import { Injectable, Logger } from '@nestjs/common';
import { Cron } from '@nestjs/schedule';
@Injectable()
export class TasksService {
private readonly logger = new Logger(TasksService.name);
@Cron('45 * * * * *')
handleCron() {
this.logger.debug('Called when the current second is 45');
}
}
```
在这个例子中,`handleCron()`方法将在当前时间为`45秒`时定期执行。换句话说,该方法每分钟执行一次,在第 45 秒执行。
`@Cron()`装饰器支持标准的[cron patterns](http://crontab.org/):
- 星号通配符 (也就是 \*)
- 范围(也就是 1-3,5)
- 步长(也就是 \*/2)
在上述例子中,我们给装饰器传递了`45 * * * * *`,下列键展示了每个位置的计时模式字符串的意义:
```bash
* * * * * *
| | | | | |
| | | | | day of week
| | | | month
| | | day of month
| | hour
| minute
second (optional)
```
一些示例的计时模式包括:
| 名称 | 含义 |
| ------------------- | ---------------------------------- |
| \* \* \* \* \* \* | 每秒 |
| 45 \* \* \* \* \* | 每分钟第 45 秒 |
| _ 10 _ \* \* \* | 每小时,从第 10 分钟开始 |
| 0 _/30 9-17 _ \* \* | 上午 9 点到下午 5 点之间每 30 分钟 |
| 0 30 11 \* \* 1-5 | 周一至周五上午 11:30 |
`@nestjs/schedule`包提供一个方便的枚举
```typescript
import { Injectable, Logger } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';
@Injectable()
export class TasksService {
private readonly logger = new Logger(TasksService.name);
@Cron(CronExpression.EVERY_45_SECONDS)
handleCron() {
this.logger.debug('Called every 45 seconds');
}
}
```
在本例中,`handleCron()`方法每`45`秒执行一次。
可选地,你可以为将一个`JavaScript`的`Date`对象传递给`@Cron()`装饰器。这样做可以让工作在指定日期执行一次。
> 使用`JavaScript`日期算法来关联当前日期和计划工作。`@Cron(new Date(Date.now()+10*1000))`用于在应用启动 10 秒后运行。
你可以在声明后访问并控制一个定时任务,或者使用[动态 API](https://docs.nestjs.com/techniques/task-scheduling#dynamic-schedule-module-api)动态创建一个定时任务(其定时模式在运行时定义)。要通过 API 声明定时任务,你必须通过将选项对象中的`name`属性作为可选的第二个参数传递给装饰器,从而将工作和名称联系起来。
```typescript
@Cron('* * 8 * * *', {
name: 'notifications',
})
triggerNotifications() {}
```
### 声明间隔
要声明一个以一定间隔运行的方法,使用`@Interval()`装饰器前缀。以毫秒单位的`number`传递间隔值,如下:
```typescript
@Interval(10000)
handleInterval() {
this.logger.debug('Called every 10 seconds');
}
```
> 本机制在底层使用`JavaScript`的`setInterval()`函数。你也可以使用定期调度工作来应用一个定时任务。
如果你希望在声明类之外通过[动态 API](https://docs.nestjs.com/techniques/task-scheduling#dynamic-schedule-module-api)控制你声明的时间间隔。使用下列结构将名称与间隔关联起来。
```typescript
@Interval('notifications', 2500)
handleInterval() {}
```
动态 API 也支持动态创建时间间隔,间隔属性在运行时定义,可以列出和删除他们。
### 声明延时任务
要声明一个在指定时间后运行(一次)的方法,使用`@Timeout()`装饰器前缀。将从应用启动的相关时间偏移量(毫秒)传递给装饰器,如下:
```typescript
@Timeout(5000)
handleTimeout() {
this.logger.debug('Called once after 5 seconds');
}
```
> 本机制在底层使用 JavaScript 的`setTimeout()`方法
如果你想要在声明类之外通过动态 API 控制你声明的超时时间,将超时时间和一个名称以如下结构关联:
```typescript
@Timeout('notifications', 2500)
handleTimeout() {}
```
动态 API 同时支持创建动态超时时间,超时时间在运行时定义,可以列举和删除他们。
### 动态规划模块 API
`@nestjs/schedule`模块提供了一个支持管理声明定时、超时和间隔任务的动态 API。该 API 也支持创建和管理动态定时、超时和间隔,这些属性在运行时定义。
### 动态定时任务
使用`SchedulerRegistry`API 从你代码的任何地方获取一个`CronJob`实例的引用。首先,使用标准构造器注入`ScheduleRegistry`。
```typescript
constructor(private schedulerRegistry: SchedulerRegistry) {}
```
> 从`@nestjs/schedule`包导入`SchedulerRegistry`。
使用下列类,假设通过下列定义声明一个定时任务:
```typescript
@Cron('* * 8 * * *', {
name: 'notifications',
})
triggerNotifications() {}
```
如下获取本工作:
```typescript
const job = this.schedulerRegistry.getCronJob('notifications');
job.stop();
console.log(job.lastDate());
```
`getCronJob()`方法返回一个命名的定时任务。然后返回一个包含下列方法的`CronJob`对象:
- stop()-停止一个按调度运行的任务
- start()-重启一个停止的任务
- setTime(time:CronTime)-停止一个任务,为它设置一个新的时间,然后再启动它
- lastDate()-返回一个表示工作最后执行日期的字符串
- nextDates(count:number)-返回一个`moment`对象的数组(大小`count`),代表即将执行的任务日期
> 在`moment`对象中使用`toDate()`来渲染成易读的形式。
使用`SchedulerRegistry.addCronJob()`动态创建一个新的定时任务,如下:
```typescript
addCronJob(name: string, seconds: string) {
const job = new CronJob(`${seconds} * * * * *`, () => {
this.logger.warn(`time (${seconds}) for job ${name} to run!`);
});
this.scheduler.addCronJob(name, job);
job.start();
this.logger.warn(
`job ${name} added for each minute at ${seconds} seconds!`,
);
}
```
在这个代码中,我们使用`cron`包中的`CronJob`对象来创建定时任务。`CronJob`构造器采用一个定时模式(类似`@Cron()`[装饰器](https://docs.nestjs.com/techniques/task-scheduling#declarative-cron-jobs)作为其第一个参数,以及一个将执行的回调函数作为其第二个参数。`SchedulerRegistry.addCronJob()`方法有两个参数:一个`CronJob`名称,以及一个`CronJob`对象自身。
> 记得在使用前注入`SchedulerRegistry`,从`cron`包中导入 `CronJob`。
使用`SchedulerRegistry.deleteCronJob()`方法删除一个命名的定时任务,如下:
```typescript
deleteCron(name: string) {
this.scheduler.deleteCronJob(name);
this.logger.warn(`job ${name} deleted!`);
}
```
使用`SchedulerRegistry.getCronJobs()`方法列出所有定时任务,如下:
```typescript
getCrons() {
const jobs = this.scheduler.getCronJobs();
jobs.forEach((value, key, map) => {
let next;
try {
next = value.nextDates().toDate();
} catch (e) {
next = 'error: next fire date is in the past!';
}
this.logger.log(`job: ${key} -> next: ${next}`);
});
}
```
`getCronJobs()`方法返回一个`map`。在这个代码中,我们遍历该`map`并且尝试获取每个`CronJob`的`nextDates()`方法。在`CronJob`API 中,如果一个工作已经执行了并且没有下一次执行的日期,将抛出异常。
### 动态间隔
使用`SchedulerRegistry.getInterval()`方法获取一个时间间隔的引用。如上,使用标准构造注入`SchedulerRegistry`。
```typescript
constructor(private schedulerRegistry: SchedulerRegistry) {}
```
如下使用:
```typescript
const interval = this.schedulerRegistry.getInterval('notifications');
clearInterval(interval);
```
使用`SchedulerRegistry.addInterval() `方法创建一个新的动态间隔,如下:
```typescript
addInterval(name: string, seconds: string) {
const callback = () => {
this.logger.warn(`Interval ${name} executing at time (${seconds})!`);
};
const interval = setInterval(callback, seconds);
this.scheduler.addInterval(name, interval);
}
```
在该代码中,我们创建了一个标准的 JavaScript 间隔,然后将其传递给`ScheduleRegistry.addInterval()`方法。该方法包括两个参数:一个时间间隔的名称,和时间间隔本身。
如下使用`SchedulerRegistry.deleteInterval()`删除一个命名的时间间隔:
```typescript
deleteInterval(name: string) {
this.scheduler.deleteInterval(name);
this.logger.warn(`Interval ${name} deleted!`);
}
```
使用`SchedulerRegistry.getIntervals()`方法如下列出所有的时间间隔:
```typescript
getIntervals() {
const intervals = this.scheduler.getIntervals();
intervals.forEach(key => this.logger.log(`Interval: ${key}`));
}
```
### 动态超时
使用`SchedulerRegistry.getTimeout()`方法获取一个超时引用,如上,使用标准构造注入`SchedulerRegistry`:
```typescript
constructor(private schedulerRegistry: SchedulerRegistry) {}
```
并如下使用:
```typescript
const timeout = this.schedulerRegistry.getTimeout('notifications');
clearTimeout(timeout);
```
使用`SchedulerRegistry.addTimeout()`方法创建一个新的动态超时,如下:
```typescript
addTimeout(name: string, seconds: string) {
const callback = () => {
this.logger.warn(`Timeout ${name} executing after (${seconds})!`);
});
const timeout = setTimeout(callback, seconds);
this.scheduler.addTimeout(name, timeout);
}
```
在该代码中,我们创建了个一个标准的 JavaScript 超时任务,然后将其传递给`ScheduleRegistry.addTimeout()`方法,该方法包含两个参数:一个超时的名称,以及超时对象自身。
使用`SchedulerRegistry.deleteTimeout()`方法删除一个命名的超时,如下:
```typescript
deleteTimeout(name: string) {
this.scheduler.deleteTimeout(name);
this.logger.warn(`Timeout ${name} deleted!`);
}
```
使用`SchedulerRegistry.getTimeouts()`方法列出所有超时任务:
```typescript
getTimeouts() {
const timeouts = this.scheduler.getTimeouts();
timeouts.forEach(key => this.logger.log(`Timeout: ${key}`));
}
```
### 示例
一个可用的例子见[这里](https://github.com/nestjs/nest/tree/master/sample/27-scheduling)。
- 介绍
- 概述
- 第一步
- 控制器
- 提供者
- 模块
- 中间件
- 异常过滤器
- 管道
- 守卫
- 拦截器
- 自定义装饰器
- 基础知识
- 自定义提供者
- 异步提供者
- 动态模块
- 注入作用域
- 循环依赖
- 模块参考
- 懒加载模块
- 应用上下文
- 生命周期事件
- 跨平台
- 测试
- 技术
- 数据库
- Mongo
- 配置
- 验证
- 缓存
- 序列化
- 版本控制
- 定时任务
- 队列
- 日志
- Cookies
- 事件
- 压缩
- 文件上传
- 流式处理文件
- HTTP模块
- Session(会话)
- MVC
- 性能(Fastify)
- 服务器端事件发送
- 安全
- 认证(Authentication)
- 授权(Authorization)
- 加密和散列
- Helmet
- CORS(跨域请求)
- CSRF保护
- 限速
- GraphQL
- 快速开始
- 解析器(resolvers)
- 变更(Mutations)
- 订阅(Subscriptions)
- 标量(Scalars)
- 指令(directives)
- 接口(Interfaces)
- 联合类型
- 枚举(Enums)
- 字段中间件
- 映射类型
- 插件
- 复杂性
- 扩展
- CLI插件
- 生成SDL
- 其他功能
- 联合服务
- 迁移指南
- Websocket
- 网关
- 异常过滤器
- 管道
- 守卫
- 拦截器
- 适配器
- 微服务
- 概述
- Redis
- MQTT
- NATS
- RabbitMQ
- Kafka
- gRPC
- 自定义传输器
- 异常过滤器
- 管道
- 守卫
- 拦截器
- 独立应用
- Cli
- 概述
- 工作空间
- 库
- 用法
- 脚本
- Openapi
- 介绍
- 类型和参数
- 操作
- 安全
- 映射类型
- 装饰器
- CLI插件
- 其他特性
- 迁移指南
- 秘籍
- CRUD 生成器
- 热重载
- MikroORM
- TypeORM
- Mongoose
- 序列化
- 路由模块
- Swagger
- 健康检查
- CQRS
- 文档
- Prisma
- 静态服务
- Nest Commander
- 问答
- Serverless
- HTTP 适配器
- 全局路由前缀
- 混合应用
- HTTPS 和多服务器
- 请求生命周期
- 常见错误
- 实例
- 迁移指南
- 发现
- 谁在使用Nest?