如何使用 Promises 在循环中调用 inquirer.js 菜单?

How do I invoke inquirer.js menu in a loop using Promises?

我写了一个简单的 Node.js 程序,带有一个漂亮的菜单系统,由 inquirer.js 提供便利。然而,在 select 在菜单中选择一个选项并完成一些操作后,程序退出。我需要再次显示菜单,直到我 select 退出菜单中的 [last] 选项。我想使用 Promise 来做到这一点, 而不是 async/await 的

我尝试使用一个函数来显示菜单并在一个永久循环中调用该函数(例如 while (true) { ... }),但这使程序无法使用。我将其更改为 for 循环只是为了观察问题。下面是简单的程序和结果输出。

PROGRAM

"use strict";

const inquirer = require('inquirer');
const util = require('util')

// Clear the screen
process.stdout.write("\u001b[2J\u001b[0;0H");

const showMenu = () => {
  const questions = [
    {
      type: "list",
      name: "action",
      message: "What do you want to do?",
      choices: [
        { name: "action 1", value: "Action1" },
        { name: "action 2", value: "Action2" },
        { name: "Exit program", value: "quit"}
      ]
    }
  ];
  return inquirer.prompt(questions);
};

const main = () => {
  for (let count = 0; count < 3; count++) {
    showMenu()
    .then(answers => {
      if (answers.action === 'Action1') {
        return Promise.resolve('hello world');
      }
      else if (answers.action === 'Action2') {
        return new Promise((resolve, reject) => {
          inquirer
            .prompt([
              {
                type: 'input',
                name: 'secretCode',
                message: "Enter a secret code:"
              }
            ])
            .then(answers => {
              resolve(answers);
            })
        });
      }
      else {
        console.log('Exiting program.')
        process.exit(0);
      }
    })
    .then((data) => { console.log(util.inspect(data, { showHidden: false, depth: null })); })
    .catch((error, response) => {
      console.error('Error:', error);
    });
  }
}

main()

OUTPUT

? What do you want to do? (Use arrow keys)
❯ action 1
  action 2
  Exit program ? What do you want to do? (Use arrow keys)
❯ action 1
  action 2
  Exit program ? What do you want to do? (Use arrow keys)
❯ action 1
  action 2
  Exit program (node:983) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 keypress listeners added to [ReadStream]. Use emitter.setMaxListeners() to increase limit

如何在第一次调用生成菜单后阻塞,等待选项被 selected 和相应的操作完成,然后循环回到显示菜单的下一次迭代?

您可以使用 async/await 语法:

声明您的 main 函数 async,并 await 查询者返回的 Promise:

const main = async () => {
  for (let count = 0; count < 3; count++) {
    await showMenu()
    .then(answers => {
      [...]
  }
};

您的代码没有按预期工作,因为简而言之,解释器在 运行 任何回调(来自承诺)之前执行同步代码。因此,您的同步 for 循环会在任何 I/O 回调被解析之前执行。所有对 showMenu() returns 承诺的调用都是异步解决的,这意味着在循环之前不会打印任何内容,也不会解释任何输入。

编写 await 会阻止 async 函数内的后续同步代码,这似乎是您想要做的。

以您的代码为起点,我整合了自己的库来显示 cli 菜单。它去除了 Inquirer 的许多样板,让您可以简洁地声明菜单 graph/tree。

main.ts 文件显示了您如何使用它。您声明了一个 MenuPrompts 字典,您可以向其中添加 Menus、Actions 和 LoopActions。每个提示都有一个键,其他提示可以路由到该键。

// main.ts

import { Menu, Action, MenuPrompt, openMenuPrompt, LoopAction } from "./menus";

// Set of prompts
let prompts = {
    menu_1: new MenuPrompt("Menu 1 - This list is ordinal - What would like to do?", 20, true, [
        new Menu("Menu 2", "menu_2"),
        new LoopAction("Action", () => console.log("Menu 1 action executed")),
        new Action("Back", context => context.last),
        new Action("Exit", () => process.exit(0)),
    ]),
    menu_2: new MenuPrompt("Menu 2 - This list is NOT ordinal - What would like to do?", 20, false, [
        new Menu("Menu 1", "menu_1"),
        new LoopAction("Action", () => console.log("Menu 2 action executed")),
        new Action("Back", context => context.last),
        new Action("Exit", () => process.exit(0)),
    ]),
};

// Open the "menu_1" prompt
openMenuPrompt("menu_1", prompts);

这是 lib 文件,其中包含用于打开初始提示的类型和函数。

// menus.ts

import * as inquirer from "inquirer";

// MAIN FUNCTION
export let openMenuPrompt = async (current: string, prompts: Dict<MenuPrompt>, last?: string): Promise<any> => {
    let answer: Answer = (await inquirer.prompt([prompts[current]])).value;
    let next = answer.execute({current, last});
    if (!next) return;
    return await openMenuPrompt(next, prompts, current == next? last : current );
};

// PUBLIC TYPES
export class MenuPrompt {
    type = "list";
    name = "value";
    message: string;
    pageSize: number;
    choices: Choice[];
    constructor(message: string, pageSize: number, isOrdinalList: boolean, choices: Choice[]) {
        this.message = message;
        this.pageSize = pageSize;
        this.choices = choices;
        if (isOrdinalList) {
            this.choices.forEach((choice, i) => choice.name = `${i + 1}: ${choice.name}`)
        }
    }
}

export interface Choice {
    name: string;
    value: Answer;
}
export class Action implements Choice {
    name: string;
    value: Answer;
    constructor(name: string, execute: (context?: MenuContext) => any) {
        this.name = name;
        this.value = {execute};
    }
}
export class LoopAction implements Choice {
    name: string;
    value: Answer;
    constructor(name: string, execute: (context?: MenuContext) => any) {
        this.name = name;
        this.value = {execute: context => execute(context) ?? context.current};
    }
}
export class Menu implements Choice {
    name: string;
    value: Answer;
    constructor(name: string, menuKey: string) {
        this.name = name;
        this.value = {execute: () => menuKey};
    }
}

// INTERNAL TYPES
type Dict<T = any> = {[key: string]: T};

interface Answer {
    execute: (context: MenuContext) => any;
}
interface MenuContext {
    current: string;
    last: string;
}