Terminal UI

本指南涵盖 Terminal UI 的不同方面。你将学习以下内容:

  • 以不同严重程度显示日志消息
  • 添加加载动画与操作指示器
  • 使用颜色格式化文本
  • 渲染具有自定义对齐方式的表格
  • 使用 sticker 创建带边框的内容
  • 构建带有进度更新的动画化任务运行器

概述

Ace 终端 UI 由 @poppinss/cliui 包驱动,它提供了用于显示日志、渲染表格、展示动画任务等的辅助方法。

所有终端 UI 原语都以测试为设计目标。在编写测试时,你可以启用 "raw" 模式以禁用颜色和格式化,从而轻松地在内存中收集日志并针对它们编写断言。这种设计确保你的命令在为用户提供丰富视觉体验的同时,依然保持可测试性。

显示日志消息

CLI logger 提供了以不同严重程度显示消息的方法。每个日志级别都使用不同的颜色和图标,帮助用户快速识别消息的重要性。

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

export default class DeployCommand extends BaseCommand {
  static commandName = 'deploy'
  
  async run() {
    /**
     * 调试消息 - 有助于排查问题
     */
    this.logger.debug('Loading deployment configuration')
    
    /**
     * 信息消息 - 常规信息
     */
    this.logger.info('Deploying application to production')
    
    /**
     * 成功消息 - 操作成功完成
     */
    this.logger.success('Deployment completed successfully')
    
    /**
     * 警告消息 - 潜在问题
     */
    this.logger.warning('SSL certificate expires in 30 days')
    
    /**
     * 错误和致命消息 - 写入 stderr
     */
    this.logger.error(new Error('Failed to upload assets'))
    this.logger.fatal(new Error('Deployment failed completely'))
  }
}

errorfatal 方法会写入 stderr 而非 stdout,让用户更容易将错误输出与正常输出分开重定向。

添加前缀和后缀

你可以为日志消息添加前缀和后缀文本以提供额外上下文。前缀和后缀都以较低的透明度显示,以区别于主要消息。

commands/install.ts
/**
 * 添加显示正在运行命令的后缀
 */
this.logger.info('Installing packages', {
  suffix: 'npm i --production'
})

/**
 * 添加显示进程 ID 的前缀
 */
this.logger.info('Starting worker', {
  prefix: process.pid
})

创建加载动画

加载动画会在消息之后显示动态的省略号,在长时间运行的操作期间提供视觉反馈。你可以在操作完成时更新消息文本并停止动画。

commands/build.ts
/**
 * 创建一个加载动画
 */
const animation = this.logger.await('Installing packages', {
  suffix: 'npm i'
})

/**
 * 启动动画
 */
animation.start()

/**
 * 随着进度继续更新消息
 */
setTimeout(() => {
  animation.update('Unpacking packages', {
    suffix: undefined
  })
}, 2000)

/**
 * 完成后停止动画
 */
setTimeout(() => {
  animation.stop()
  this.logger.success('Installation complete')
}, 4000)

显示操作状态

Logger actions 提供了一种一致的方式来显示操作状态,并带有自动的样式和颜色编码。这在执行多个顺序任务时尤其有用。

commands/setup.ts
/**
 * 创建一个操作指示器
 */
const createFile = this.logger.action('creating config/auth.ts')

try {
  await this.createConfigFile()
  
  /**
   * 将操作标记为成功
   * 可选:显示耗时
   */
  createFile.displayDuration().succeeded()
} catch (error) {
  /**
   * 将操作标记为失败并附带错误
   */
  createFile.failed(error)
}

操作可以标记为三种不同的状态:

commands/setup.ts
/**
 * 操作成功完成
 */
action.succeeded()

/**
 * 操作被跳过并附带原因
 */
action.skipped('File already exists')

/**
 * 操作因错误而失败
 */
action.failed(new Error('Permission denied'))

使用颜色格式化文本

Ace 使用 kleur 为文本应用 ANSI 颜色码。通过 this.colors 属性访问 kleur 的链式 API,即可使用前景色、背景色和文本样式来格式化文本。

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

export default class StatusCommand extends BaseCommand {
  static commandName = 'status'
  
  async run() {
    /**
     * 应用前景色
     */
    this.logger.info(this.colors.red('[ERROR]'))
    this.logger.info(this.colors.green('[SUCCESS]'))
    this.logger.info(this.colors.yellow('[WARNING]'))
    
    /**
     * 组合背景色和前景色
     */
    this.logger.info(this.colors.bgGreen().white(' CREATED '))
    this.logger.info(this.colors.bgRed().white(' FAILED '))
    
    /**
     * 应用文本样式
     */
    this.logger.info(this.colors.bold('Important message'))
    this.logger.info(this.colors.dim('Less important details'))
  }
}

渲染表格

表格将数据组织成行和列,让用户能够轻松浏览和比较信息。使用 this.ui.table 方法创建表格,该方法会返回一个用于定义表头与行的 Table 实例。

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

export default class ListMigrationsCommand extends BaseCommand {
  static commandName = 'migration:list'
  
  async run() {
    /**
     * 创建一个新的表格
     */
    const table = this.ui.table()
    
    /**
     * 定义表头
     */
    table.head([
      'Migration',
      'Duration',
      'Status',
    ])
    
    /**
     * 添加表格行
     */
    table.row([
      '1590591892626_tenants.ts',
      '2ms',
      'DONE'
    ])
    
    table.row([
      '1590595949171_entities.ts',
      '2ms',
      'DONE'
    ])
    
    /**
     * 将表格渲染到终端
     */
    table.render()
  }
}

你可以通过将值用颜色方法包裹,对任意表格单元格应用颜色格式化。

commands/list_migrations.ts
table.row([
  '1590595949171_entities.ts',
  '2ms',
  this.colors.green('DONE')
])

table.row([
  '1590595949172_users.ts',
  '5ms',
  this.colors.red('FAILED')
])

右对齐列

默认情况下,所有列都是左对齐的。你可以通过将列定义为带有 hAlign 属性的对象来右对齐列。右对齐某列时,请务必同时右对齐对应的表头。

commands/list_migrations.ts
/**
 * 右对齐状态列表头
 */
table.head([
  'Migration',
  'Batch',
  {
    content: 'Status',
    hAlign: 'right'
  },
])

/**
 * 右对齐状态列数据
 */
table.row([
  '1590595949171_entities.ts',
  '2',
  {
    content: this.colors.green('DONE'),
    hAlign: 'right'
  }
])

渲染全宽表格

默认情况下,表格会自动调整列宽以适应其内容。不过,你可以使用 fullWidth 方法以终端全宽渲染表格。

在 full-width 模式下,除一列之外的所有列都使用其内容宽度,而指定的 "fluid" 列会扩展以填充剩余空间。默认情况下,第一列为 fluid 列。

commands/list_files.ts
/**
 * 以终端全宽渲染表格
 */
table.fullWidth().render()

你可以使用 fluidColumnIndex 方法更改扩展以填充可用空间的列。

commands/list_files.ts
/**
 * 让第二列(索引 1)变为 fluid 列
 */
table
  .fullWidth()
  .fluidColumnIndex(1)
  .render()

使用 sticker 创建带边框的内容

Sticker 会在带边框的框内渲染内容,将用户的注意力吸引到重要信息上,例如服务器地址、配置说明或关键的下步操作。

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

export default class ServeCommand extends BaseCommand {
  static commandName = 'serve'
  
  async run() {
    /**
     * 创建一个用于显示服务器信息的 sticker
     */
    const sticker = this.ui.sticker()

    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.2:3333')}`)
      .render()
  }
}

若要显示分步说明,请改用 this.ui.instructions 方法。它会为每一行添加箭头符号(>),从而清楚地表明这些是待办操作项。

commands/init.ts
/**
 * 显示安装后说明
 */
const instructions = this.ui.instructions()

instructions
  .add('Run npm install to install dependencies')
  .add('Copy .env.example to .env and configure your environment')
  .add('Run node ace migrate to set up the database')
  .render()

构建动画化任务运行器

Tasks 组件为多耗时操作的执行与进度显示提供了精致的 UI。它支持两种渲染模式:minimal(用于生产环境)和 verbose(用于调试)。

在 minimal 模式下,只有当前正在运行的任务会展开以显示进度更新。在 verbose 模式下,每条进度消息都会记录在自己的行上,从而更容易调试问题。

创建基础任务

使用 this.ui.tasks 方法创建 tasks 组件,然后使用 add 方法添加各个任务。

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

export default class SetupCommand extends BaseCommand {
  static commandName = 'setup'
  
  async run() {
    /**
     * 创建一个 tasks 组件
     */
    const tasks = this.ui.tasks()

    /**
     * 添加任务并执行它们
     */
    await tasks
      .add('clone repo', async (task) => {
        await this.cloneRepository()
        return 'Completed'
      })
      .add('update package file', async (task) => {
        try {
          await this.updatePackageFile()
          return 'Updated'
        } catch (error) {
          return task.error('Unable to update package file')
        }
      })
      .add('install dependencies', async (task) => {
        await this.installDependencies()
        return 'Installed'
      })
      .run()
  }
}

每个任务回调都必须返回一个状态消息。返回普通字符串表示成功,而将返回值包裹在 task.error() 中则表示失败。你也可以抛出异常将任务标记为失败。

报告任务进度

不要在任务回调中使用 console.logthis.logger,而应使用 task.update 方法来报告进度。这能确保在 minimal 和 verbose 两种模式下进度更新都能正确显示。

commands/build.ts
/**
 * 模拟异步工作的辅助函数
 */
const sleep = () => new Promise<void>((resolve) => setTimeout(resolve, 50))

const tasks = this.ui.tasks()

await tasks
  .add('clone repo', async (task) => {
    /**
     * 在任务执行时报告进度
     */
    for (let i = 0; i <= 100; i = i + 2) {
      await sleep()
      task.update(`Downloaded ${i}%`)
    }

    return 'Completed'
  })
  .run()

在 minimal 模式下,仅显示最新的进度消息。在 verbose 模式下,所有消息都会按其发生的顺序被记录。

启用 verbose 模式

你可能希望允许用户启用 verbose 输出来进行调试。通常的做法是接受一个 --verbose 标志。

commands/deploy.ts
import { BaseCommand, flags } from '@adonisjs/core/ace'

export default class DeployCommand extends BaseCommand {
  static commandName = 'deploy'
  
  /**
   * 接受一个 verbose 标志
   */
  @flags.boolean({
    description: 'Enable verbose output'
  })
  declare verbose: boolean

  async run() {
    /**
     * 根据该标志启用 verbose 模式
     */
    const tasks = this.ui.tasks({
      verbose: this.verbose
    })
    
    await tasks
      .add('build assets', async (task) => {
        // 任务实现
      })
      .run()
  }
}

现在用户可以使用 --verbose 运行你的命令,以查看详细的进度日志用于调试。