创建命令

本指南介绍如何使用 Ace 命令行创建命令。你将了解以下主题:

  • 创建自定义命令
  • 配置命令元数据
  • 使用生命周期方法
  • 注入依赖
  • 处理错误
  • 管理长时间运行的进程

创建你的第一个命令

你可以使用 make:command Ace 命令生成一个新的命令。这会创建一个基本的命令(位于 commands 目录中),并带有所有必要的样板代码。

另见: Make command

node ace make:command greet

# CREATE: commands/greet.ts

生成的文件包含一个继承自 BaseCommand 的命令类。一个命令至少必须定义一个 commandName 并实现 run 方法。

commands/greet.ts
import { BaseCommand } from '@adonisjs/core/ace'

export default class GreetCommand extends BaseCommand {
  static commandName = 'greet'
  static description = 'Greet a user by name'

  async run() {
    this.logger.info('Hello world!')
  }
}

现在你可以使用你定义的命令名称来执行你的命令。

node ace greet

配置命令元数据

命令元数据控制你的命令在帮助界面中的显示方式,以及它在执行期间的行为。元数据包括命令名称、描述、帮助文本、别名和执行选项。

设置命令名称

commandName 属性定义了用户将键入以执行你的命令的名称。命令名称不应包含空格,并应避免使用不熟悉的特殊字符,如 *& 或斜杠。

commands/greet.ts
export default class GreetCommand extends BaseCommand {
  static commandName = 'greet'
}

命令名称可以通过使用冒号分隔符来包含命名空间。这有助于将相关命令组织在帮助输出中。

commands/make/controller.ts
export default class MakeControllerCommand extends BaseCommand {
  /**
   * 该命令出现在 "make" 命名空间下
   */
  static commandName = 'make:controller'
}

编写命令描述

命令描述会出现在命令列表以及你的命令的帮助界面中。描述应简洁明了,较长的说明请使用帮助文本。

commands/greet.ts
export default class GreetCommand extends BaseCommand {
  static commandName = 'greet'
  static description = 'Greet a user by name'
}

添加详细的帮助文本

帮助文本允许你提供较长的描述、使用示例或适合简短描述之外的额外上下文。将帮助文本定义为字符串数组,其中每个字符串表示一行输出。

commands/greet.ts
export default class GreetCommand extends BaseCommand {
  static commandName = 'greet'
  static description = 'Greet a user by name'

  static help = [
    'The greet command is used to greet a user by name',
    '',
    'You can also send flowers to a user, if they have an updated address',
    '{{ binaryName }} greet --send-flowers',
  ]
}

{{ binaryName }} 变量替换会引用用于执行 ace 命令的二进制文件(通常是 node ace),确保你的帮助文本无论用户如何运行 Ace 都能显示正确的命令语法。

定义命令别名

别名为你的命令提供了替代名称。当你想为常用命令提供更短或更直观的名称时,这会很有用。

commands/greet.ts
export default class GreetCommand extends BaseCommand {
  static commandName = 'greet'
  static aliases = ['welcome', 'sayhi']
}

用户现在可以使用任何已定义的名称来运行你的命令。

node ace greet
node ace welcome
node ace sayhi

配置命令选项

命令选项控制你的命令的执行行为。这些选项使用静态的 options 属性定义,并影响 Ace 如何启动应用、处理标志以及管理命令的生命周期。

commands/greet.ts
import { BaseCommand } from '@adonisjs/core/ace'
import type { CommandOptions } from '@adonisjs/core/types/ace'

export default class GreetCommand extends BaseCommand {
  static commandName = 'greet'

  static options: CommandOptions = {
    startApp: false,
    allowUnknownFlags: false,
    staysAlive: false,
  }
}

启动应用

默认情况下,Ace 在运行命令时不会启动你的 AdonisJS 应用。这使得命令运行更快,并防止对不需要应用状态的简单任务进行不必要的应用初始化。

但是,如果你的命令需要访问模型、服务或其他应用资源,你必须告诉 Ace 在执行命令之前启动应用。

commands/send_email.ts
import { BaseCommand } from '@adonisjs/core/ace'
import type { CommandOptions } from '@adonisjs/core/types/ace'

export default class SendEmailCommand extends BaseCommand {
  static options: CommandOptions = {
    /**
     * 启动应用以访问模型和服务
     */
    startApp: true
  }

  async run() {
    /**
     * 现在可以使用模型和等服务之类的应用资源
     */
    const users = await User.all()
  }
}

允许未知标志

默认情况下,如果你传递了一个命令未定义的标志,Ace 会显示错误。这种严格的解析有助于捕获拼写错误和错误的标志用法。

但是,某些命令需要接受任意标志并将它们传递给其他工具。你可以使用 allowUnknownFlags 选项禁用严格的标志解析。

commands/proxy.ts
import { BaseCommand } from '@adonisjs/core/ace'
import type { CommandOptions } from '@adonisjs/core/types/ace'

export default class ProxyCommand extends BaseCommand {
  static options: CommandOptions = {
    /**
     * 接受任何标志并将其传递给外部工具
     */
    allowUnknownFlags: true
  }
}

创建长时间运行的命令

Ace 会在你的命令的 run 方法完成后自动终止应用。对于执行任务后退出的大多数命令来说,这是期望的行为。

但是,如果你的命令需要无限期运行(如队列 worker 或开发服务器),你必须告诉 Ace 不要使用 staysAlive 选项终止应用。

commands/queue_worker.ts
import { BaseCommand } from '@adonisjs/core/ace'
import type { CommandOptions } from '@adonisjs/core/types/ace'

export default class QueueWorkerCommand extends BaseCommand {
  static options: CommandOptions = {
    startApp: true,
    /**
     * 保持进程存活
     */
    staysAlive: true
  }

  async run() {
    /**
     * 开始无限期处理任务
     */
    await this.startJobProcessor()
  }
}

另见: 终止应用在终止前清理

理解命令生命周期

Ace 按预定义的顺序执行命令生命周期方法,让你可以将命令逻辑组织到不同的阶段。每个生命周期方法在命令执行流程中都有特定的用途。

commands/greet.ts
import { BaseCommand } from '@adonisjs/core/ace'

export default class GreetCommand extends BaseCommand {
  /**
   * 首先运行 - 设置初始状态
   */
  async prepare() {
    console.log('preparing')
  }

  /**
   * 其次运行 - 与用户交互
   */
  async interact() {
    console.log('interacting')
  }

  /**
   * 第三运行 - 执行主要的命令逻辑
   */
  async run() {
    console.log('running')
  }

  /**
   * 最后运行 - 清理与错误处理
   */
  async completed() {
    console.log('completed')
  }
}

下表描述了每个生命周期方法及其预期用途:

方法说明
prepareAce 执行的第一个方法。用它来初始化后续方法所需的 state 或数据。
interactprepare 之后执行。用它来显示提示并收集用户输入。
run命令的主要逻辑。该方法在 interact 之后调用。
completed在所有其他生命周期方法之后调用。用它来执行清理或处理之前方法的错误。

你不需要实现所有的生命周期方法。只定义你的命令实际所需的方法即可。对于简单的命令,仅实现 run 方法就足够了。

使用依赖注入

Ace 使用 IoC 容器 构造并执行命令,使你能够将依赖注入到任何生命周期方法中。这对于访问你的命令所需的服务、仓库或其他资源特别有用。

要注入依赖,请将它们作为方法参数的类型提示,并用 @inject 装饰器装饰该方法。

commands/greet.ts
import { inject } from '@adonisjs/core'
import { BaseCommand } from '@adonisjs/core/ace'
import UserService from '#services/user_service'

export default class GreetCommand extends BaseCommand {
  /**
   * 将 UserService 注入到 prepare 方法
   */
  @inject()
  async prepare(userService: UserService) {
    // 使用注入的服务
  }

  /**
   * 依赖可以注入到任何生命周期方法
   */
  @inject()
  async interact(userService: UserService) {
  }

  @inject()
  async run(userService: UserService) {
  }

  @inject()
  async completed(userService: UserService) {
  }
}

容器会自动解析依赖,包括嵌套依赖,让你无需手动实例化即可轻松访问应用的服务。

处理错误和退出码

当你的命令抛出异常时,Ace 会使用 CLI logger 显示错误,并将命令的退出码设置为 1,表示失败。非零退出码会向 shell 或 CI/CD 系统发出命令失败的信号。

但是,你也可以使用 try/catch 块或 completed 生命周期方法来显式处理错误。当你自己处理错误时,必须更新命令的 exitCodeerror 属性,以确保命令正确报告其状态。

使用 try/catch 处理错误

使用 try/catch 块直接在可能发生错误的处理方法中处理错误。这让你对错误处理有精细的控制。

commands/greet.ts
import { BaseCommand } from '@adonisjs/core/ace'

export default class GreetCommand extends BaseCommand {
  async run() {
    try {
      await runSomeOperation()
    } catch (error) {
      /**
       * 记录错误消息
       */
      this.logger.error(error.message)

      /**
       * 更新命令状态以表示失败
       */
      this.error = error
      this.exitCode = 1
    }
  }
}

在 completed 方法中处理错误

completed 生命周期方法提供了一个集中的位置,用于处理来自任何先前生命周期方法的错误。当你想要跨所有命令阶段保持一致的错误处理时,这会很有用。

commands/greet.ts
import { BaseCommand } from '@adonisjs/core/ace'

export default class GreetCommand extends BaseCommand {
  async run() {
    /**
     * 如果这里抛出异常,错误将在 completed() 中可用
     */
    await runSomeOperation()
  }

  async completed() {
    if (this.error) {
      /**
       * 处理来自任何生命周期方法的错误
       */
      this.logger.error(this.error.message)

      /**
       * 返回 true 以通知 Ace 你已经处理了错误
       * 这会防止 Ace 再次记录该错误
       */
      return true
    }
  }
}

终止应用

Ace 会在执行完你的命令后自动终止应用。但是,当你为长时间运行的命令启用 staysAlive 选项时,你必须在命令完成或发生错误时显式终止应用。

使用 this.terminate 方法优雅地关闭应用。这常用于需要基于特定条件退出的长时间运行进程。

commands/monitor_redis.ts
import { BaseCommand } from '@adonisjs/core/ace'
import type { CommandOptions } from '@adonisjs/core/types/ace'

export default class MonitorRedisCommand extends BaseCommand {
  static options: CommandOptions = {
    startApp: true,
    staysAlive: true
  }

  async run() {
    const redis = createRedisConnection()

    /**
     * 当连接失败时终止应用
     */
    redis.on('error', (error) => {
      this.logger.error(error)
      this.terminate()
    })

    /**
     * 开始监控 Redis
     */
    redis.monitor()
  }
}

在应用终止前清理

多个事件都可能触发应用终止,包括进程管理器发送的 SIGTERM 信号 ,或用户按下 Ctrl+C 时。为确保你的命令在关闭前执行必要的清理,请监听 terminating 钩子。

terminating 钩子应在 prepare 生命周期方法中注册,该方法在你的命令主要逻辑之前运行。这确保清理处理器在任何工作开始之前就已就绪。

commands/queue_worker.ts
import { BaseCommand } from '@adonisjs/core/ace'
import type { CommandOptions } from '@adonisjs/core/types/ace'

export default class QueueWorkerCommand extends BaseCommand {
  static options: CommandOptions = {
    startApp: true,
    staysAlive: true
  }

  prepare() {
    /**
     * 注册在终止前运行的清理逻辑
     */
    this.app.terminating(() => {
      /**
       * 关闭数据库连接、刷新日志等。
       */
      this.logger.info('Shutting down gracefully...')
    })
  }

  async run() {
    /**
     * 开始长时间运行的工作
     */
    await this.processJobs()
  }
}