Ace
Ace 是嵌入到 AdonisJS 核心的命令行框架。node ace serve
或 node ace make:controller
等命令由 Ace CLI 提供支持。
Ace 还允许你通过将它们本地存储在项目代码库中来创建自定义命令。
为什么我们使用 Ace 而不是 npm 脚本?
大多数 Node.js 项目广泛使用 npm 脚本。 Npm 脚本很棒,因为它们允许在每个项目的基础上定义脚本,而不是在计算机上的某个地方全局定义它们。
但是,npm 脚本没有提供任何工具来创建 CLI 命令。你仍然必须手动解析 CLI 参数/标志并管理命令生命周期。
另一方面,Ace 是用于创建 CLI 接口的合适框架。
用法
每个新的 AdonisJS 应用程序都预先配置了 Ace,你可以使用存储在项目根目录中的 ace
文件运行它。
node ace
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
装饰器定义标志。标志可以接受 boolean
、string/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
显示在 Yes
和 No
之间进行选择的提示。或者,你可以将配置 options 作为第二个参数传递。
await this.prompt.confirm('Want to delete files?')
prompt.toggle
类似于 confirm
提示。但是,它允许自定义 Yes
和 No
显示值。或者,你可以将配置 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 |
未提供输入时使用的默认值
|
hint |
显示提示以帮助填充输入
|
result |
修改结果。该方法在解决提示承诺 (promise) 之前被调用 注意 该值会因输入类型而异。例如:
|
format |
实时格式化用户输入(在他们输入时) 注意 该值会因输入类型而异。例如:
|
validate |
验证用户输入。返回 注意 该值会因输入类型而异。例如:
|
日志器
你可以使用内置记录器将消息记录到控制台。如果终端不支持颜色,我们会自动去除颜色和图标。
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()
}
}
所有记录器方法还会收到日志消息 prefix
和 suffix
的可选值。
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。
任务渲染器有两种输出模式,即 minimum
和 verbose
。当 shell 处于[非交互式] (github.com/poppinss/cliui/blob/dev...) 时,我们会自动切换到 verbose
模式。
const tasksManager = this.ui.tasks()
// 手动切换到详细渲染器
const tasksManager = this.ui.tasks.verbose()
创建任务渲染器后,你可以通过调用 .add
方法添加一个新任务,并在其中执行实际的任务工作。完成任务后,你必须调用 task.complete
或 task.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
本译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。