Ace

未匹配的标注

Ace 是嵌入到 AdonisJS 核心的命令行框架。node ace servenode ace make:controller 等命令由 Ace CLI 提供支持。

Ace 还允许你通过将它们本地存储在项目代码库中来创建自定义命令。

为什么我们使用 Ace 而不是 npm 脚本?

大多数 Node.js 项目广泛使用 npm 脚本。 Npm 脚本很棒,因为它们允许在每个项目的基础上定义脚本,而不是在计算机上的某个地方全局定义它们。

但是,npm 脚本没有提供任何工具来创建 CLI 命令。你仍然必须手动解析 CLI 参数/标志并管理命令生命周期。

另一方面,Ace 是用于创建 CLI 接口的合适框架。

用法

每个新的 AdonisJS 应用程序都预先配置了 Ace,你可以使用存储在项目根目录中的 ace 文件运行它。

node ace

Help screen

ace 文件是一个无扩展名的 JavaScript 文件,你可以像执行任何其他 Node.js 程序一样执行它。运行此文件将启动命令行框架并执行上述命令。

你可以通过运行 node ace --help 列出所有命令,并使用 node ace <command-name> --help 查看特定命令的帮助。

命令在哪里定义?

Ace 允许你和你安装的软件包提供命令。它们在 commands 数组下的 .adonisrc.json 文件中定义。

{
  "commands": [
    "./commands",
    "@adonisjs/core/build/commands",
    "@adonisjs/repl/build/commands"
  ]
}

数组中的每个条目都必须指向一个导出 Ace 命令 的文件。或者它可以导出 附加命令数组

第一个条目 ./commands 是对项目命令目录的引用。此目录中的文件被扫描并注册为命令。

创建一个新命令

你可以通过运行以下 Ace 命令来创建新命令。

node ace make:command Greet

# CREATE: commands/Greet.ts

在运行新创建的命令之前,你必须通过运行以下命令对其进行索引。 了解为什么需要索引

node ace generate:manifest

最后,你可以按如下方式运行命令:

node ace greet

# [ info ]  Hello world!

命令结构

Ace 命令表示为类并扩展了 BaseCommand 类。你将命令名称和描述定义为类本身的静态属性。

import { BaseCommand } from '@adonisjs/core/build/standalone'

export default class Greet extends BaseCommand {
  public static commandName = 'greet'

  public static description = ''

  public static settings = {
    loadApp: false,
    stayAlive: false,
  }

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

commandName

运行命令时应键入的命令的名称。它应该始终是一个字符串。


description

命令描述显示在帮助输出中。使用此属性简要说明该命令的作用。


settings

settings 属性控制命令的运行时行为。

选项 说明
loadApp 指示 Ace 在运行方法之前启动应用程序。默认情况下,命令不会加载应用程序,而是作为独立脚本执行。
stayAlive 指示 Ace 在运行命令后不要终止进程。但是,请确保使用 await this.exit()

aliases

你还可以为命令名称定义一个别名数组。这允许其他人也可以使用别名执行命令。

export default class Greet extends BaseCommand {
  public static commandName = 'greet'
  public static aliases = ['welcome', 'hi']
}

run

每个命令都必须实现 run 方法并在其中编写处理命令的逻辑。


在命令中启动应用程序

Ace 命令在运行命令之前不会启动你的应用程序。如果你的命令依赖于应用程序代码,你必须指示命令先加载应用程序,然后执行 run 方法。

export default class Greet extends BaseCommand {
  public static commandName = 'greet'
  public static settings = {
    loadApp: true
  }
}

不允许顶级导入

不允许依赖 IoC 容器或应用程序代码库的顶级导入,你必须将它们移动到 run 方法中。例如:

❌ 不生效

import User from 'App/Models/User'

export default class CreateUser extends BaseCommand {
  public static commandName = 'create:user'
  public static settings = {
    loadApp: true
  }

  public async run() {
    await User.create({})
  }
}

✅ 在将 import 移入 run 方法内之后生效

export default class CreateUser extends BaseCommand {
  public static commandName = 'create:user'
  public static settings = {
    loadApp: true
  }

  public async run() {
    const { default: User } = await import('App/Models/User')
    await User.create()
  }
}

🤷‍♂️ 推理

让我们尝试可视化命令生命周期,以了解为什么不允许顶级导入。

  • User 模型导入在内部从 IoC 容器导入 Lucid ORM。
  • 由于应用程序尚未启动,Lucid ORM 不可用。
  • 要加载应用程序,Ace 必须首先访问命令构造函数中定义的 settings.loadApp 属性。
  • 但是,它不能,因为顶级导入会导致错误。

还有其他方法可以设计此工作流程,但我们认为在 run 方法中移动导入值得将所有命令设置和元数据保存在单个文件中的麻烦。

CLI 参数

你将命令接受的参数和标志注册为类的属性。例如:

import {
  BaseCommand,
  args,
  flags
} from '@adonisjs/core/build/standalone'

export default class Greet extends BaseCommand {
  public static commandName = 'greet'

  @args.string({ description: 'Name of the person to greet' })
  public name: string

  @flags.boolean({ alias: 'i', description: 'Enable interactive mode' })
  public interactive: boolean

  public async run() {}
}

确保通过运行以下命令生成 Ace 清单文件。

node ace generate:manifest

然后查看greet命令的帮助。

node ace greet --help

参数

命令参数是位置参数,它们的接受顺序与你在类中定义它们的顺序相同。例如:

export default class Greet extends BaseCommand {
  @args.string()
  public name: string

  @args.string()
  public age: string

  @args.string()
  public height: string
}
node ace greet <name> <age> <height>

args.string

将属性标记为命令行参数。注意:命令参数始终表示为字符串。如果期望非字符串值,你将不得不自己执行类型转换。

@args.string({
  description: 'The argument description',
  name: 'username'
})
public name: string

args.spread

@args.spread 方法允许你定义一个包罗万象的参数。它就像 JavaScript 中的 rest parameters ,必须始终是最后一个参数。

import { BaseCommand, args } from '@adonisjs/core/build/standalone'

export default class FileReader extends BaseCommand {
  public static commandName = 'read'

  @args.spread()
  public files: string[]

  public async run () {
    console.log(this.files)
  }
}
node ace read foo.txt bar.txt baz.txt

输出将是

[ 'foo.txt', 'bar.txt', 'baz.txt' ]

选项

所有@args方法都接受以下选项。

选项 描述
description 参数的帮助说明
name 为参数定义一个公共名称(出现在帮助输出中的那个)

标志

你可以使用 @flags 装饰器定义标志。标志可以接受 booleanstring/string[]number/number[]值。

flags.boolean

接受一个布尔标志。

@flags.boolean()
public interactive: boolean

布尔标志的值默认为 false,除非已指定标志。但是,你也可以自己定义默认值。

@flags.boolean()
public interactive: boolean = true

要在运行时禁用该标志,你必须使用 --no 关键字来否定它。

node ace greet virk --no-interactive

flags.string

定义一个接受字符串值的标志。

@flags.string()
public email: string

@flags.string()
public password: string

flags.array

定义一个可以重复多次的标志。该值是一个字符串数组。

@flags.array()
public files: string[]
node ace read --files=foo.txt --files=bar.txt

## 或者用逗号隔开
node ace read --files=foo.txt,bar.txt
console.log(this.files)

// ['foo.txt', 'bar.txt']

flags.number

定义一个接受数字值的标志。

@flags.number({ alias: 'i' })
public iterations: number

flags.numArray

@flags.array 相同,但接受一个数字数组。

@flags.numArray()
public counters: number[]

Options

所有 @flags 装饰器都接受以下选项。

选项 描述
alias 标志的简写名称。简写名称始终使用单个破折号- 定义
description T标志的帮助说明
name 标志的公共名称(出现在帮助输出中的名称)

提示

Ace 内置支持在终端上创建交互式提示。你可以使用 this.prompt 属性访问 prompts 模块。

以下是一起使用多个提示的示例。

import { BaseCommand } from '@adonisjs/core/build/standalone'

export default class CreateUser extends BaseCommand {
  public static commandName = 'create:user'
  public static description = 'Create a new user'

  public async run () {
    const email = await this.prompt.ask('Enter email')
    const password = await this.prompt.secure('Choose account password')
    const userType = await this.prompt.choice('Select account type', [
      {
        name: 'admin',
        message: 'Admin (Complete access)',
      },
      {
        name: 'collaborator',
        message: 'Collaborator (Can access specific resources)',
      },
      {
        name: 'user',
        message: 'User (Readonly access)',
      }
    ])

    const verifyEmail = await this.prompt.confirm('Send account verification email?')
    const accountTags = await this.prompt.enum('Type tags to associate with the account')

    console.log({
      email, password, userType, verifyEmail, accountTags
    })
  }
}

查看视频演示

prompt.ask

显示输入值的提示。可选择接受 options 作为第二个参数。

await this.prompt.ask('Choose account username', {
  validate(answer) {
    if (!answer || answer.length < 4) {
      return 'Username is required and must be over 4 characters'
    }

    return true
  },
})

prompt.secure

使用 password 提示类型。可选择接受 options 作为第二个参数。

await this.prompt.secure('Enter account password', {
  validate(answer) {
    if (!answer) {
      return 'Password is required to login'
    }

    return true
  },
})

prompt.confirm

显示在 YesNo 之间进行选择的提示。或者,你可以将配置 options 作为第二个参数传递。

await this.prompt.confirm('Want to delete files?')

prompt.toggle

类似于 confirm 提示。但是,它允许自定义 YesNo 显示值。或者,你可以将配置 options 作为第二个参数传递。

await this.prompt.toggle('Want to delete files?', ['Yep', 'Nope'])

prompt.choice

显示一个选项列表,可以只选择一个。或者,你可以将配置 options 作为第三个参数传递。

await this.prompt.choice('Select installation client', ['npm', 'yarn'])

或者将选择作为对象数组传递。

await this.prompt.choice('Select toppings', [
  {
    name: 'Jalapenos',
    hint: 'Marinated in vinegar, will taste sour',
  },
  {
    name: 'Lettuce',
    hint: 'Fresh and leafy',
  },
])

prompt.multiple

显示选项列表并允许选择多个选项。或者,你可以将配置 options 作为第三个参数传递。

await this.prompt.multiple('Select base dependencies', [
  '@adonisjs/core', '@adonisjs/bodyparser'
])

或者将选择作为对象传递。

await this.prompt.multiple('Select base dependencies', [
  {
    name: '@adonisjs/core',
    message: 'Framework core',
  },
  {
    name: '@adonisjs/bodyparser',
    message: 'Bodyparser',
  },
])

prompt.autocomplete

显示选项列表以进行一个或多个选择,并能够过滤列表项。或者,你可以将配置 options 作为第三个参数传递。

await this.prompt.autocomplete(
  'Select country',
  ['India', 'USA', 'UK', 'Ireland', 'Australia']
)

对于多选,你可以设置 options.multiple = true

await this.prompt.autocomplete(
  'Select country',
  ['India', 'USA', 'UK', 'Ireland', 'Australia'],
  { multiple: true }
)

prompt.enum

类似于 ask 提示,但允许使用逗号 (,) 分隔值。可选择接受 options 作为第二个参数。

await this.prompt.enum('Define tags', {
  hint: 'Accepts comma separated values',
})

所有提示选项

选项 描述
default

未提供输入时使用的默认值

```ts { default: 'Virk' } ```

hint

显示提示以帮助填充输入

```ts { hint: 'Email will be used for login.' } ```

result

修改结果。该方法在解决提示承诺 (promise) 之前被调用

注意 该值会因输入类型而异。例如: prompt.multiple 的值将是一个数组或选择。

```ts { result: (value) => { return value.toUppercase() } } ```

format

实时格式化用户输入(在他们输入时)

注意 该值会因输入类型而异。例如: prompt.multiple 的值将是一个数组或多选。

```ts { format: (value) => { return value.toUppercase() } } ```

validate

验证用户输入。返回 true 以通过验证或 false/错误消息

注意 该值会因输入类型而异。例如: prompt.multiple 的值将是一个数组或多选

```ts { validate: (value) => { if (!value) { return 'Enter value' }

return true } } ```

日志器

你可以使用内置记录器将消息记录到控制台。如果终端不支持颜色,我们会自动去除颜色和图标。

export default class Greet extends BaseCommand {
  public static commandName = 'greet'
  public static description = 'Greet a person by their name'

  public async run () {
    this.logger.info('This is an info message')
    this.logger.warning('Running out of disk space')
    this.logger.error(new Error('Unable to write. Disk full'))
    this.logger.fatal(new Error('Unable to write. Disk full'))
    this.logger.debug('Something just happened')
    this.logger.success('Account created')
    this.logger.info('Message with time prefix', '%time%')

    const spinner = this.logger.await(
      'installing dependencies'
      undefined,
      'npm install --production'
    )

    // 执行一些任务
    spinner.stop()
  }
}

所有记录器方法还会收到日志消息 prefixsuffix 的可选值。

this.logger.info('hello world', 'prefix', 'suffix')

Actions

除了标准日志消息,你还可以显示特定操作的日志消息。例如,创建文件的操作可以使用以下代码来显示其状态。

注:记录器操作仅用于显示用户界面。你仍然需要自己执行这个动作。

const filePath = 'app/Models/User.ts'

this.logger.action('create').succeeded(filePath)
this.logger.action('create').skipped(filePath, '文件已存在')
this.logger.action('create').failed(filePath, '出问题了')

更新现有日志行

日志记录器还允许你通过更新现有的日志线来记录消息。使用此方法,你可以绘制文本进度条和 ASCII 进度条。

每次运行 logUpdate 方法时,它都会使用新消息更新现有日志行。你可以使用 logUpdatePersist 方法保留并移动到新行。

下面是一个显示进度条的完整工作示例。

import { BaseCommand } from '@adonisjs/core/build/standalone'

export default class Greet extends BaseCommand {
  public static commandName = 'greet'

  private getProgressBar(currentPercentage: number) {
    /**
     * 几乎每 3% 绘制一个单元格。
     * 这是为了确保进度条在较小的终端宽度上呈现良好
     */
    const completed = Math.ceil(currentPercentage / 3)
    const incomplete = Math.ceil((100 - currentPercentage) / 3)
    return `[${new Array(completed).join('=')}${new Array(incomplete).join(' ')}]`
  }

  public async run () {
    for (let i = 0; i <= 100; i = i + 2) {
      await new Promise((resolve) => setTimeout(resolve, 50))
      this.logger.logUpdate(`downloading ${this.getProgressBar(i)} ${i}%`)
    }

    this.logger.logUpdatePersist()
  }
}

查看视频演示

命令行界面

命令行界面将 API 公开给绘制表格在框内渲染指令动画进度的任务

你可以使用 this.ui.table 属性绘制表格。下面是一个类似的例子。

const table = this.ui.table()
table.head(['Name', 'Email', 'Score'])

// 可选地定义列宽
table.columnWidths([15, 30, 10])

// 添加新行
table.row(['Virk', 'virk@adonisjs.com', '67'])
table.row(['Romain', 'romain@adonisjs.com', '82'])
table.row(['Nikk', 'nikk@adonisjs.com', '41'])

// 渲染表格
table.render()
  • 你可以使用 this.ui.table() 方法创建一个新的表实例。
  • 使用 .head() 方法创建表头并传递要创建的列数组。
  • 使用 .row() 方法添加新行。
  • 最后,使用 .render() 方法渲染表格。

显示说明

你可以通过将指令绘制在有界框内来显示给定操作的指令。例如:

this.ui
  .instructions()
  .add(`cd ${this.colors.cyan('hello-world')}`)
  .add(`Run ${this.colors.cyan('node ace serve --watch')} to start the server`)
  .render()
  • 调用 this.ui.instructions() 方法开始一个新的指令块。
  • 接下来,你可以使用 .add() 方法添加新行。
  • 最后,调用 .render() 方法在控制台上渲染。

贴纸

贴纸(sticker)类似于 instructions 块。但是,它不会在行前加上指针 >,其余都是一样的。

this.ui
  .sticker()
  .add('Started HTTP server')
  .add('')
  .add(`Local address:    ${this.colors.cyan('http://localhost:3333')}`)
  .add(`Network address:  ${this.colors.cyan('http://192.168.1.4:3333')}`)
  .render()

任务渲染器

你可以使用任务渲染器来显示多个操作的输出。 AdonisJS 本身在构建新应用程序时使用它来显示 UI。

任务渲染器有两种输出模式,即 minimumverbose。当 shell 处于[非交互式] (github.com/poppinss/cliui/blob/dev...) 时,我们会自动切换到 verbose 模式。

const tasksManager = this.ui.tasks()

// 手动切换到详细渲染器
const tasksManager = this.ui.tasks.verbose()

创建任务渲染器后,你可以通过调用 .add 方法添加一个新任务,并在其中执行实际的任务工作。完成任务后,你必须调用 task.completetask.fail 以移动到队列中的下一个任务。

tasksManager
  .add('clone repo', async (logger, task) => {
    // 使用日志器记录进度
    await task.complete()
  })
  .add('install package', async (logger, task) => {
    await task.fail(new Error('Cannot install packages'))
  })

定义完所有任务后调用 run 方法。

await tasksManager.run()

模板生成器

Ace 有一个内置的轻量级模板生成器。你可以使用它从预先存在的存根生成文件。例如:

import { join } from 'path'
import { BaseCommand } from '@adonisjs/core/build/standalone'

export default class Greet extends BaseCommand {
  public static commandName = 'greet'

  public async run() {
    const name = 'UsersController'

    this.generator
      .addFile(name)
      .appRoot(this.application.appRoot)
      .destinationDir('app/Controllers/Http')
      .useMustache()
      .stub(join(__dirname, './templates/controller.txt'))
      .apply({ name })

    await this.generator.run()
  }
}
  • generator.addFile 方法启动创建新文件的过程。
  • 使用其流畅的 API,你可以定义文件目标、存根和要传递给存根的数据
  • 最后执行 this.generator.run 创建所有使用 .addFile 方法添加的文件。

addFile

该方法创建 GeneratorFile 类的新实例。它接受两个参数;首先,文件名(带或不带扩展名),第二个是选项对象。

this.generator.addFile(
  'UserController',
  {
    // 强制文件名是复数
    form: 'plural',

    // 未定义时则定义「.ts」扩展名
    extname: '.ts',

    // 将名称重新格式化为「camelCase」
    pattern: 'camelcase',

    // 未定义时添加「控制器」后缀
    suffix: 'Controller',

    // 当控制器名称与以下之一匹配时不要复数
    formIgnoreList: ['Home', 'Auth', 'Login']
  }
)

destinationDir

定义要在其中创建文件的目标目录。你还可以从 .adonisrc.json 文件中提取目录名称,如下所示:

// 获取配置目录的路径
file.destinationDir(
  this.application.directoriesMap.get('config')!
)

// 获取控制器命名空间的路径
file.destinationDir(
  this.application.resolveNamespaceDirectory('httpControllers')!
)

appRoot

定义应用程序的根。这是 destinationDir 的前缀以创建绝对路径。

file.appRoot(this.application.appRoot)

stub

定义存根模板的绝对路径。你可以使用 ES6 模板文字编写模板,也可以通过首先调用 useMustache 方法来使用 mustache

file
  .useMustache() // 使用 mustache 作为模板引擎
  .stub(join(__dirname, 'templates/controller.txt'))

apply

与 mustache 模板共享数据。当前文件名(应用所有转换后)作为 filename 属性与模板共享。

file.apply({
  resourceful: true
})

run

generator.run 方法开始创建使用 .addFile 方法定义的文件。如果目标路径已经存在,生成器会跳过该文件。

await this.generator.run()

生命周期钩子

可以定义以下生命周期钩子。

prepare 方法在运行 run 方法之前执行,而 completed 方法在 run 方法之后执行。

export default class Greet extends BaseCommand {
  public async prepare() {
    console.log('before run')
  }

  public async run() {
    console.log('run')
  }

  public async completed() {
    console.log('after run')
  }
}

如果出现错误,你可以使用 completed 方法中的 this.error 属性访问错误。

以编程方式执行命令

在同一进程中执行其他命令不是一个好习惯。命令不应该被代码的不同部分使用,因为它们导出用户界面并且不是编码界面。例如:

  • 你可以从进程退出代码中找到命令的状态,而不是某个返回值。
  • 命令直接将其状态转储到终端,并且不将其存储在某些属性中,以便以编程方式访问。

综上所述,有几种方法可以以编程方式执行命令。

作为子进程执行命令

推荐的方法是在单独的子进程中执行命令。你可以使用 Node.js child_process 模块或使用 execa npm 模块。

import execa from 'execa'
import { BaseCommand } from '@adonisjs/core/build/standalone'

export default class Greet extends BaseCommand {
  public static commandName = 'greet'

  public async run() {
    const { exitCode } = await execa.node('ace', ['make:controller', 'User'], {
      stdio: 'inherit',
    })
  }
}

在同一个进程内执行命令

另一种选择是利用 Ace 内核在同一进程中执行命令。在以下示例中,无法知道命令的退出代码。

import { BaseCommand } from '@adonisjs/core/build/standalone'

export default class Greet extends BaseCommand {
  public static commandName = 'greet'

  public async run() {
    await this.kernel.exec('make:controller', ['User'])
  }
}

生成 Ace 清单文件

Ace manifest 是所有已注册命令的 JSON 索引。它允许 Ace 在不加载所有命令文件的情况下查找命令、它接受的参数/标志。

生成索引对于性能至关重要。否则,导入所有命令,使用内存中的 TypeScript 编译器编译它们将花费大量时间,尤其是打印帮助屏幕。

AdonisJS 在以下事件期间自动更新 ace-manifest.json 文件。

  • 每次使用 node ace configure 命令安装和配置软件包时。
  • 当文件观察程序运行时,你更改了存储在 commands 目录中的命令文件。

仅这两个事件就涵盖了大多数用例。但是,你也可以通过运行以下命令手动更新清单文件。

node ace generate:manifest

prepare方法在运行 run 方法之前执行。
并且completed方法是 execu

本文章首发在 LearnKu.com 网站上。

本译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

原文地址:https://learnku.com/docs/adonisjs/5.x/di...

译文地址:https://learnku.com/docs/adonisjs/5.x/di...

上一篇 下一篇
贡献者:2
讨论数量: 0
发起讨论 只看当前版本


暂无话题~