Terminal UI
本指南涵盖 Terminal UI 的不同方面。你将学习以下内容:
- 以不同严重程度显示日志消息
- 添加加载动画与操作指示器
- 使用颜色格式化文本
- 渲染具有自定义对齐方式的表格
- 使用 sticker 创建带边框的内容
- 构建带有进度更新的动画化任务运行器
概述
Ace 终端 UI 由 @poppinss/cliui 包驱动,它提供了用于显示日志、渲染表格、展示动画任务等的辅助方法。
所有终端 UI 原语都以测试为设计目标。在编写测试时,你可以启用 "raw" 模式以禁用颜色和格式化,从而轻松地在内存中收集日志并针对它们编写断言。这种设计确保你的命令在为用户提供丰富视觉体验的同时,依然保持可测试性。
显示日志消息
CLI logger 提供了以不同严重程度显示消息的方法。每个日志级别都使用不同的颜色和图标,帮助用户快速识别消息的重要性。
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'))
}
}
error 和 fatal 方法会写入 stderr 而非 stdout,让用户更容易将错误输出与正常输出分开重定向。
添加前缀和后缀
你可以为日志消息添加前缀和后缀文本以提供额外上下文。前缀和后缀都以较低的透明度显示,以区别于主要消息。
/**
* 添加显示正在运行命令的后缀
*/
this.logger.info('Installing packages', {
suffix: 'npm i --production'
})
/**
* 添加显示进程 ID 的前缀
*/
this.logger.info('Starting worker', {
prefix: process.pid
})
创建加载动画
加载动画会在消息之后显示动态的省略号,在长时间运行的操作期间提供视觉反馈。你可以在操作完成时更新消息文本并停止动画。
/**
* 创建一个加载动画
*/
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 提供了一种一致的方式来显示操作状态,并带有自动的样式和颜色编码。这在执行多个顺序任务时尤其有用。
/**
* 创建一个操作指示器
*/
const createFile = this.logger.action('creating config/auth.ts')
try {
await this.createConfigFile()
/**
* 将操作标记为成功
* 可选:显示耗时
*/
createFile.displayDuration().succeeded()
} catch (error) {
/**
* 将操作标记为失败并附带错误
*/
createFile.failed(error)
}
操作可以标记为三种不同的状态:
/**
* 操作成功完成
*/
action.succeeded()
/**
* 操作被跳过并附带原因
*/
action.skipped('File already exists')
/**
* 操作因错误而失败
*/
action.failed(new Error('Permission denied'))
使用颜色格式化文本
Ace 使用
kleur
为文本应用 ANSI 颜色码。通过 this.colors 属性访问 kleur 的链式 API,即可使用前景色、背景色和文本样式来格式化文本。
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 实例。
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()
}
}
你可以通过将值用颜色方法包裹,对任意表格单元格应用颜色格式化。
table.row([
'1590595949171_entities.ts',
'2ms',
this.colors.green('DONE')
])
table.row([
'1590595949172_users.ts',
'5ms',
this.colors.red('FAILED')
])
右对齐列
默认情况下,所有列都是左对齐的。你可以通过将列定义为带有 hAlign 属性的对象来右对齐列。右对齐某列时,请务必同时右对齐对应的表头。
/**
* 右对齐状态列表头
*/
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 列。
/**
* 以终端全宽渲染表格
*/
table.fullWidth().render()
你可以使用 fluidColumnIndex 方法更改扩展以填充可用空间的列。
/**
* 让第二列(索引 1)变为 fluid 列
*/
table
.fullWidth()
.fluidColumnIndex(1)
.render()
使用 sticker 创建带边框的内容
Sticker 会在带边框的框内渲染内容,将用户的注意力吸引到重要信息上,例如服务器地址、配置说明或关键的下步操作。
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 方法。它会为每一行添加箭头符号(>),从而清楚地表明这些是待办操作项。
/**
* 显示安装后说明
*/
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 方法添加各个任务。
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.log 或 this.logger,而应使用 task.update 方法来报告进度。这能确保在 minimal 和 verbose 两种模式下进度更新都能正确显示。
/**
* 模拟异步工作的辅助函数
*/
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 标志。
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 运行你的命令,以查看详细的进度日志用于调试。