はじめに
業務で Node.js を使って対話型CLIツールを実装してみました。
その時に async、await と向き合い、自分なりに理解したことを残しておきます。
やりたいこと
対話型CLIにて得た回答からCSVを作成する
環境構築
WindowsのWSLにて動作確認しています
1. volta のインストール
curl https://get.volta.sh | bash
volta --version
Node.js のバージョン管理ツール
今回作成するアプリは、OSが混じる可能性があるため、voltaを選択
他の選択肢でいうと、nodeenv、nvm がある
2. node, typescript のインストール
volta install node
volta install typescript
3. ts-node のインストール
npm install ts-node
Node.jsはJavaScriptを実行できるが、TypeScriptを直接実行することはできない
ts-nodeを利用するとTypeScriptのファイルを指定して実行することができる
package.jsonに以下を追加することで、 npm start
コマンドにて、トランスパイルと実行を行うことができるようになる
"scripts": {
"start": "ts-node ./src/main.ts"
}
4. inquirer.js(対話型CLIライブラリ)のインストール
npm install inquirer
ドキュメントには、npm install --save inquirer
と記載されていますが、npm 5.0.0 以降からは install 時にデフォルトで save してくれるので --save や -S オプションを指定する必要はない。
require('inquirer')
がエラーになるので、ドキュメントにあるとおり、以下を実行
記事公開時には、ドキュメントのリンク先の情報がupdateされており、当時の情報とは異なっています。
npm install inquirer@^8.0.0
npm install --dev @types/inquirer@^8.0.0
実装
First Step
まずはREADMEを見ながら動かしてみる
ユーザに入力/選択を促す質問を作成
code
import inquirer, { Answers } from "inquirer";
const commonQuestions: inquirer.QuestionCollection<inquirer.Answers> = [
{
type: "input",
name: "name",
message: "What is your name or nickname?",
},
{
type: "checkbox",
name: "hobbies",
message: "Select your hobbies:",
choices: ["Reading", "Traveling", "Cookiing", "Sports", "Music", "Others"],
},
];
inquirer.prompt(commonQuestions).then((answers: inquirer.Answers) => {
console.log(answers);
return answers;
});
実行
途中経過
npm start
> start
> ts-node ./src/main.ts
? What is your name or nickname? mayumi
? Select your hobbies: (Press <space> to select, <a> to toggle all, <i> to invert selection, and <enter> to proceed)
◯ Reading
◯ Traveling
◯ Cookiing
❯◉ Sports
◯ Music
◯ Others
結果
? What is your name or nickname? mayumi
? Select your hobbies: Sports
{ name: 'mayumi', hobbies: [ 'Sports' ] }
うん、なんかよさげ。
Second Step
質問の回答によって、処理を分岐してみる
- 趣味に
スポーツ
を選択した場合には、どのスポーツが好きかの質問(選択式)をしたい - 趣味に
その他
を選択した場合には、趣味を入力(記述式)してもらいたい
とりあえず1だけ実装してみる...
code
import inquirer, { Answers } from "inquirer";
const commonQuestions: inquirer.QuestionCollection<inquirer.Answers> = [
{
type: "input",
name: "name",
message: "What is your name or nickname?",
},
{
type: "checkbox",
name: "hobbies",
message: "Select your hobbies:",
choices: ["Reading", "Traveling", "Cookiing", "Sports", "Music", "Others"],
},
];
const sportsQuestion = [
{
type: "checkbox",
name: "sports",
message: "Select your favorite sports:",
choices: [
"Baseball",
"Soccer",
"Basketball",
"Volleyball",
"Badminton",
"Tennis",
"Others",
],
},
];
const othersQuestion = [
{
type: "input",
name: "other_hobbies",
message: "What your hobbies?",
},
];
inquirer.prompt(commonQuestions).then((answers: inquirer.Answers) => {
if (answers.hobbies == "Sports") {
inquirer.prompt(sportsQuestion).then((sportsAnswers: inquirer.Answers) => {
const allAnswers = { ...answers, ...sportsAnswers };
console.log(allAnswers);
});
}
console.log(answers);
return answers;
});
やりがち:とりあえず console.log で様子見...
実行
なんか思ってるのとちょっと違う...🤔 console.logされるタイミングが変...
つぶやき
あ、そっか。2つ目のconsole.log してる場所が悪いのか...
promiseを返すってことは、inquirer.promptは非同期処理なので、そっか先にconsole.log走るよね。
頭ではわかってるつもりだったけど、実感として少し理解。
というわけで呼び出し部分を修正...
inquirer.prompt(commonQuestions).then((answers: inquirer.Answers) => {
if (answers.hobbies == "Sports") {
inquirer.prompt(sportsQuestion).then((sportsAnswers: inquirer.Answers) => {
answers = { ...answers, ...sportsAnswers };
console.log(answers);
});
} else {
console.log(answers);
}
});
実行
うん、なんかよさげ。
いやでもさ...
つぶやき(再び)
🤔 これ、分岐が増えるたびに then が増えて、入れ子がとんでもないことになるよね...
🧚 ねね、前に await つけると Promise 外せる
って typescript めっちゃできる人が言ってたよね。これってそういうことじゃ...?
よし、ちょっとやってみよう。
async/await 使ってみる
まずは、inquirer.prompt
を関数化してみる
作成したいのは、引数で指定したquestionに対するanswerを返す関数
const ask = async (
questions: inquirer.QuestionCollection<inquirer.Answers>
): Promise<inquirer.Answers> => {
const answers = await inquirer.prompt(questions);
return answers;
};
元のソースを inquirer.prompt
の直接呼び出しから、ask 関数を呼ぶように変更
inquirer.prompt(commonQuestions).then((answers: inquirer.Answers) => {
if (answers.hobbies == "Sports") {
inquirer.prompt(sportsQuestion).then((sportsAnswers: inquirer.Answers) => {
const allAnswers = { ...answers, ...sportsAnswers };
console.log(allAnswers);
});
} else {
console.log(answers);
}
});
↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
ask(commonQuestions).then((answers: inquirer.Answers) => {
if (answers.hobbies == "Sports") {
ask(sportsQuestion).then((sportsAnswers: inquirer.Answers) => {
answers = { ...answers, ...sportsAnswers };
console.log(answers);
});
} else {
console.log(answers);
}
});
次は、 .then
を外してみる ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
const askAllQuestion = async (): Promise<inquirer.Answers> => {
let answers = await ask(commonQuestions);
if (answers.hobbies == "Sports") {
const sportsAnswers = await ask(sportsQuestion);
answers = { ...answers, ...sportsAnswers };
// console.log(answers);
return answers;
} else {
// console.log(answers);
return answers;
}
};
askAllQuestion().then((answers: inquirer.Answers) => {
console.log(answers);
});
実行したら、変更前 と結果が同じになった 🙌
というわけで、Second Step 全体を実装。
- 趣味にスポーツを選択した場合には、どのスポーツが好きかの質問(選択式)をしたい
- 趣味にその他を選択した場合には、趣味を入力(記述式)してもらいたい
import inquirer, { Answers } from "inquirer";
const commonQuestions: inquirer.QuestionCollection<inquirer.Answers> = [
{
type: "input",
name: "name",
message: "What is your name or nickname?",
},
{
type: "checkbox",
name: "hobbies",
message: "Select your hobbies:",
choices: ["Reading", "Traveling", "Cookiing", "Sports", "Music", "Others"],
},
];
const sportsQuestion = [
{
type: "checkbox",
name: "sports",
message: "Select your favorite sports:",
choices: [
"Baseball",
"Soccer",
"Basketball",
"Volleyball",
"Badminton",
"Tennis",
"Others",
],
},
];
const othersQuestion = [
{
type: "input",
name: "other_hobbies",
message: "What your hobbies?",
},
];
const ask = async (
questions: inquirer.QuestionCollection<inquirer.Answers>
): Promise<inquirer.Answers> => {
const answers = await inquirer.prompt(questions);
return answers;
};
const askAllQuestion = async (): Promise<inquirer.Answers> => {
let answers = await ask(commonQuestions);
if (answers.hobbies == "Sports") {
const sportsAnswers = await ask(sportsQuestion);
answers = { ...answers, ...sportsAnswers };
return answers;
} else if (answers.hobbies == "Others") {
const othersAnswers = await ask(othersQuestion);
answers = { ...answers, ...othersAnswers };
return answers;
} else {
return answers;
}
};
askAllQuestion().then((answers: inquirer.Answers) => {
console.log(answers);
});
実行
mayumitakanoMBP:~/git/create_csv_from_cli npm start
> start
> ts-node ./src/main.ts
? What is your name or nickname? mayumi
? Select your hobbies: Sports
? Select your favorite sports: Volleyball
{ name: 'mayumi', hobbies: [ 'Sports' ], sports: [ 'Volleyball' ] }
mayumitakanoMBP:~/git/create_csv_from_cli npm start
> start
> ts-node ./src/main.ts
? What is your name or nickname? mayumi
? Select your hobbies: Others
? What your hobbies? making sweets
{
name: 'mayumi',
hobbies: [ 'Others' ],
other_hobbies: 'making sweets'
}
あとは、この結果を利用して CSV 作成する部分が残っているけど、今回は async/await のことを残しておきたかったので、いったんここまで。
まとめ
今までなんとなく動いたからとわかったつもりになっていた 非同期処理や async/await について、最終形にたどりつくまで少しずつ進めたので、自分なりに理解が深まった。
自分がやらかしがちなことなども残しておくことができたので、またつまづいた時には一から調べ直さなくてもここに戻ればよさそう。
参考にした情報