- 投稿日:2020-03-26T21:50:52+09:00
Node.jsをインストール(nodebrewでバージョン管理)
本来の目的:ローカル環境でES6を使用
node.jsのバージョン管理ツールnodebrewをインストール
※メリット:複数のnode.jsのバージョンを簡単に切り替えられる。
brewがインストールされているか確認
$ brew -v【実行結果】インストールされているHomebrew 2.2.6 Homebrew/homebrew-core (git revision ba38; last commit 2020-02-28)nodebrewのインストール
$ brew install nodebrew【実行結果】Updating Homebrew... ==> Auto-updated Homebrew! Updated 1 tap (homebrew/core). ==> New Formulae abseil hdf5@1.10 publish archiver hdt qp azcopy hsd seal bnfc killswitch swift-format ccheck mockolo swift-sh cdk8s mtoc tlx container-structure-test nef vpn-slice dhall-yaml newrelic-cli xclogparser forcecli oil zim git-trim openlibm ==> Updated Formulae node ✔ less * * (省略) * * Removing: /Users/lancai/Library/Logs/Homebrew/yarn... (100B) Removing: /Users/lancai/Library/Logs/Homebrew/autoconf... (64B) Removing: /Users/lancai/Library/Logs/Homebrew/node... (64B) Removing: /Users/lancai/Library/Logs/Homebrew/rbenv... (64B) Pruned 1 symbolic links and 2 directories from /usr/local lancai@oja Desktop %2分ほどでインストールが完了。
※10分以上かかる場合もあるとのことnodebrewのバージョン確認
$ nodebrew【実行結果】nodebrew 1.0.1 Usage: nodebrew help Show this message nodebrew install <version> Download and install <version> (from binary) nodebrew compile <version> Download and install <version> (from source) nodebrew install-binary <version> Alias of `install` (For backword compatibility) nodebrew uninstall <version> Uninstall <version> nodebrew use <version> Use <version> nodebrew list List installed versions nodebrew ls Alias for `list` nodebrew ls-remote List remote versions nodebrew ls-all List remote and installed versions nodebrew alias <key> <value> Set alias nodebrew unalias <key> Remove alias nodebrew clean <version> | all Remove source file nodebrew selfupdate Update nodebrew nodebrew migrate-package <version> Install global NPM packages contained in <version> to current version nodebrew exec <version> -- <command> Execute <command> using specified <version> Example: # install nodebrew install v8.9.4 # use a specific version number nodebrew use v8.9.4私のバージョンは
8.9.4
みたいですNode.jsのインストール
実行環境の確認$ echo $SHELL【実行結果】/bin/zsh
/bin/zsh
の場合は以下を入力$ echo 'export PATH=$HOME/.nodebrew/current/bin:$PATH' >> ~/.zshrc〜ここまでインストール準備完了〜
以下を一つずつ実行
$ source ~/.zshrc$ nodebrew setup実行結果Fetching nodebrew... Fetching nodebrew... Installed nodebrew in $HOME/.nodebrew ======================================== Export a path to nodebrew: export PATH=$HOME/.nodebrew/current/bin:$PATH ========================================引き続き以下を実行
$ nodebrew use stable実行結果use v12.16.1$ node -v実行結果v12.16.0Node.jsのバージョンが表示されたので無事にインストール出来たようです。
- 投稿日:2020-03-26T14:11:34+09:00
[Node.js] express + nodemon + BrowserSync でゲボ楽コーディング
はじめに
expressを使ってBrowserSyncを使う記事が少なかったので書きます。
nodemon + BrowserSyncでブラウザノータッチの楽々コーディングを目指そう。方法
- expres側ファイルをnodemonで監視(3000portで起動)
- BrowserSync を 4000 番ポートで起動、nodemonに接続
これによりクライアント側のファイルはBrowserSyncでブラウザをリロード
nodemonによるサーバ再起動は、再起動後にBrowserSync に通知してもらえるようにする。前提
- node + expressは導入済み
- Nodeの側のポートは3000に指定
導入
browser-sync
のインストール$ npm install --save-dev browser-syncインストールが終わってからbrowser-syncのコンフィグファイルを生成
$ npx browser-sync init
bs-config.js
が作られるので、以下のように一部を編集
プロキシをhttp://localhost:3000
に指定することで連携可能。
今回はカスタムテンプレートにEJSを使用するために.ejsを指定しているが、適宜対応してください。bs-config.js"files": [ "**/*.js", "**/*.ejs", "**/*.css" ], ~中略~ "proxy": "http://localhost:3000", "port": 4000,次に
nodemon
のインストール$ npm install -g nodemonここまでくればあとはどちらも起動するだけで終わりだが、どうせならまとめて起動したい。
ここでひと手間を加える。
npm-run-all
のインストール$ npm install --save-dev npm-run-all
package.json
の編集(抜粋)package.json"scripts": { "start": "npm-run-all --parallel start:*", "start:nodemon": "nodemon ./bin/www", "start:sync": "browser-sync start --config bs-config.js" },あとは
npm start
で楽々開発。package.json の解説
動きますね!終わり!でもいいがnpm-run-allを使った記述が多少わかりずらいので解説
npm-run-allの詳しい挙動に関してはggってもらって・・・
npm-run-allはシーケンシャルとパラレルの二つの指定方法があります。今回は
parallel
を指定しなければ、nodemon
orbrowser-sync
どちらかしか動きません。
nodemon
もbrowser-sync
も一度呼び出すと永続するので・・・"start": "npm-run-all --parallel←コレ start:*",
start:*
に関しては後ろ2行の:以下を表しています。"start:nodemon←コレ": "nodemon ./bin/www", "start:sync←コレモ": "browser-sync start --config bs-config.js"他にもいろいろ応用できそう。
参考記事
- 投稿日:2020-03-26T09:49:15+09:00
話題のanalyzeコマンドを実装してみた
今、イケてるエンジニア界隈で話題沸騰中の
analyze
コマンドをご存知でしょうか。こういうやつですね。
Yet another
analyze
commandコレCLIっぽい見た目をしていますが、実はWebブラウザ上1でしか動作しません。不便ですね。
Shellでも使いたい!という声にお答えして、実装してみました!!Source code: https://github.com/kaz/qiita-analyze
使い方
インストールは以下のコマンドを実行するだけです。nodejs/npmは別途インストールしてください。
$ npm i -g qiita-analyzeそしたらこうやって実行
$ analyze @sobaya007すごい!!!! (実際は色もついてるよ)
$ analyze @sobaya007 投稿した記事 読んだ記事 LGTMした記事 D言語: 20% Docker: 7% dlang: 19% D言語くん: 20% Python: 5% D言語: 7% プログラミング: 10% JavaScript: 3% Vim: 4% GPGPU: 10% docker-compose: 3% C: 4% dlang: 10% Node.js: 2% C++: 4%解説
NodeJSでのWebスクレイピングにはcheerioが便利です。(申し訳程度の技術要素)
- 投稿日:2020-03-26T01:18:44+09:00
JavaScriptでちょっと複雑なCLIを作るのに便利なEnquirer
この記事は
最近Node.jsでCLIを作る機会があり、その時に触ったEnquirerというライブラリが便利だったので、軽く紹介してみようというものです。ツールそのものについて軽くふれつつ、制作過程で出てきた「こんなことしたいけど、どう実現すれば良いんだろう」と試行錯誤して分かった使い方などを共有できればなと思います。
Enquirerとは
Enquirerは CLIアプリケーションにおける対話的インターフェイスの実装を楽にしてくれるライブラリです。単純なテキスト入力の受付はもちろん、リストからの選択、チェックボックス、パスワード、入力補完、など、様々な入力方式を手軽に組み込むことができます。Node.js製です。JavaScript(TypeScript)万歳!
公式サイトによるとこんなのもできるそうです(凄い!!
いつ使うんだろう)
類似のツールとしては先発の Inquirer.js などがあります。公式サイトに「Inquirerより速いぜ!」とあったり、Inquirerとほぼ同じような記述方式をサポートしていることから、かなり意識して作っているように見えます。どちらも利用者が多く、メンテも続いているのでどちらを使っても良いでしょう。あえて比較をするならば、Enquirerの方が多少全体や中身を把握しやすかったのと、少し凝ったことをやろうとしたときに素直そうな印象でした(個人の感想です)。
基本的な使い方
const Enquirer = require('enquirer'); (async ()=> { const question = { type: 'select', name: 'favorite', message: '好きな乗り物は?', choices: ['パトカー', '救急車', '消防車'], }; const answer = await Enquirer.prompt(question); console.log(`僕も${answer.favorite}が好きだよ`); })();このように
prompt
メソッドにオプションを渡してやると、それに応じた方式で入力を受け付ける描画をしたのち、ユーザの入力が完了したときにPromise
をresolve
してくれます。結果はオプションname
に指定したキーにぶら下がってきます。この要領で公式ドキュメントを見ながら使えば、大抵のことは実現できると思います(少し違う使い方もありますが後で触れます)
少し複雑なケースの実装
ここからは少しだけ複雑な要件に対応してみます。
複雑な入力と言われて最初に思い浮かぶのはポケモンバトルの選出画面です。故にここからは「ポケモンバトルの選出画面をCLIで実装するとしたら」をテーマに少しづつ進めていきたいと思います。リストから要素を複数選択(これは単純
ポケモンの対戦は、基本的に以下のような流れで進行します。
1. ポケモン6匹でパーティを構築する 2. 対戦前にお互いにパーティを見せ合う 3. 6匹のうち3匹を選出し、3vs3で対戦今回の対応範囲である「選出」というのはこの3番めにある「6匹のうち3匹を選ぶ」作業のことを指します。
以上を踏まえると、選出画面のCLI版は以下のようなものになりそうです。
実装は以下のようになります。const Enquirer = require('enquirer'); const myPokemonNames = [ 'フシギバナ', 'リザードン', 'カメックス', 'ゴリランダー', 'エースバーン', 'インテレオン', ]; (async ()=> { const question = { name: 'selections', type: 'select', multiple: true, message: '誰を出す?', choices: myPokemonNames, validate: (selectedItems) => { if(selectedItems.length === 3){ // true/falseを返すとOK/NGのみを表現 return true; } // 文字列を返すとエラーメッセージになる return '3匹選んでください'; }, }; const answer = await Enquirer.prompt(question); console.log(`${answer.selections.join(',')} を選出しました`); })();オプション
multiple: true
を渡すことで、複数選択可能なチェックボックス式の入力を受け付けます。また、
validate
にバリデーション用の関数を渡すことで、ユーザ入力を検証して、不適合な入力を弾き、入力画面をキープすることができます。エラーメッセージを独自のものにしたい場合は、true/false
ではなく文字列を返すようにすることで、判定をNGとしたうえで、関数が返した文字列をエラーメッセージとして表示してくれます(errorMessageってオプションあったほうがわかりやすいきがしますが)。タイマーで入力をキャンセルする
ここまでは公式Readmeにもしっかり書いてあるので難なく対応できました。が、要件を一つ忘れていたのでここで追加します。
ポケモンの対戦では遅延行為を防ぐため、あらゆる行動に制限時間が設けられています。もちろん選出も例外ではなく、すべてのプレイヤーは1分30秒(記事執筆時点)以内で3匹のポケモンを選び出す必要があります。これに間に合わない場合は、強制的に上から順に3匹のポケモンが選出されます。
この要件をCLIに反映させてみます。
const Enquirer = require('enquirer'); // (信じられないことに)配列 myPokemonNamesをchoicesオプションとして渡すと破壊的に配列が変更されるので、 // 違うArrayインスタンスを返すようにFunctionに包んでいる const myPokemonNames = () =>{ return [ 'フシギバナ', 'リザードン', 'カメックス', 'ゴリランダー', 'エースバーン', 'インテレオン', ]; }; (async ()=> { const prompt = new Enquirer.MultiSelect({ name: 'selections', message: '誰を出す?', choices: myPokemonNames(), validate: (selectedItems) => { if(selectedItems.length === 3){ return true; } return '3匹選んでください'; }, }) let timer; prompt.once('run', ()=>{ timer = setTimeout(()=>{ prompt.cancel() }, 5000) }) prompt.once('close', ()=>{ clearTimeout(timer); }); const answer = await prompt.run().catch(() => { // 時間切れです return myPokemonNames().slice(0,3); }); console.log(`${answer.join(',')} を選出しました`); })();これまでとは少し実装方法を変えています。これまではInquirer.js風の実装をしていましたが、ここではコチラのように、ライブラリにビルトインで入っている
Prompt
の子クラスを用いた実装にしています。コンストラクタの形式はこれまで
prompt
メソッドに渡していたものに似ていますが、各クラスごとに自明なものMultipleSelect
クラスにおけるtype
やmultiple:true
など)が不要になっています(TypeScriptなら型チェックも効いて快適)。この方式では
Prompt
クラスのrun()
メソッドを実行したときにユーザの入力を受け付けるようになり、同cansel()
メソッドを呼んでやることで、強制的に入力を終了させることが可能です。
Prompt
クラスはEventEmitter
を継承しており、上記コードでは入力受付開始時のrun
イベントに反応してタイマーを作動させ、一定時間経過後にキャンセルするようにしています。入力終了時(キャンセル/タイムアウト含む)に発生するclose
イベントが発生したタイミングでは、タイマーを止める処理を実行するようにしています。最後に、プロンプトをキャンセルしたときには
Promise
がreject
されるので、catch節でエラーを拾って、「時間切れの場合は先頭の3匹強制選出」を示す結果を返すようにしています(ちなみにcatch節のコールバックには何も引数が入って来ません)カウントダウンを表示する
制限時間を超過すると時間切れになるようにはできましたが、選出中に「後何秒?」が分からないのは辛いものがあります。これも対応しましょう。
(async ()=> { let timeRemaining = 10; const prompt = new Enquirer.MultiSelect({ name: 'selections', message: () => { return `誰を出す? 残り ${timeRemaining} 秒` }, choices: myPokemonNames(), validate: (selectedItems) => { if(selectedItems.length === 3){ return true; } return '3匹選んでください'; }, }) let interval; prompt.once('run', ()=>{ interval = setInterval(()=>{ timeRemaining -= 1; if(timeRemaining <= 0){ prompt.cancel() } else { prompt.render() } }, 1000) }) prompt.once('close', ()=>{ clearInterval(interval); }); const answer = await prompt.run().catch(()=>{ console.log('時間切れです'); return myPokemonNames().slice(0,3); }); console.log(`${answer.join(',')} を選出しました`); })();タイマー処理を
setTimeout
をsetInterval
に変えて、1秒毎にカウントダウンするように変更しつつ、メッセージに「残り n 秒」を表示させるために、以下の修正を施しています。
message
オプションに残り秒数を表示する文字列を返す関数を渡す- 1秒毎に
prompt.render()
メソッドを実行するこれにより、プロンプトの内容が毎秒再描画され、残り時間がカウントダウンされていく様子を表示することができました。カウントダウン部分のみを他のライブラリや独自実装などで代替しようとすると、カーソルの状態が衝突して表示がおかしくなったりするので、このあたりをサポートしてくれているのは有り難い限りです。
複数の質問をする & 前の回答を考慮して選択肢を変更する
これで完成かと思いましたが、そうはいきません。確かに選出は6匹から3匹を選び出す作業ですが、同時に「誰を先発させるか」を決める作業でもあることを忘れていました。
最初に選択する要素には特別な意味をもたせる必要がありそうなので、最初に先発を聞いて選んでもらった後に、控えの二匹を選出してもらうようにしてみます。
(async ()=> { let timeRemaining = 10; let currentPrompt; let interval; // Enquirerインスタンスの参照が欲しいのでstaticメソッドのpromptではなく // new Enquirer()して、そいつのpromptメソッドを呼ぶようにする const enquirer = new Enquirer(); enquirer.on('prompt', (prompt) => { currentPrompt = prompt; prompt.once('run', ()=>{ interval = setInterval(()=>{ timeRemaining -= 1; if(timeRemaining <= 0){ currentPrompt.cancel() } else { currentPrompt.render() } }, 1000) }); prompt.once('close', ()=>{ clearInterval(interval); }); }) const answer = await enquirer.prompt([ { type: 'select', name: 'starter', message: () => { return `先発は誰にする? 残り ${timeRemaining} 秒` }, choices: myPokemonNames(), }, { type: 'select', multiple: true, name: 'reserves', message: () => { return `控えは誰にする? 残り ${timeRemaining} 秒` }, choices() { return myPokemonNames().filter((name) => { return this.state.answers.starter !== name; }) }, validate: (selectedItems) => { if(selectedItems.length === 2){ return true; } return '2匹選んでください'; }, } ]).catch(console.error); if (answer) { const selected = [answer.starter].concat(answer.reserves); console.log(`${selected.join(',')} を選出しました`); } else { console.log('時間切れです') console.log(`${myPokemonNames().slice(0,3).join(',')} を選出しました`); } })();公式ドキュメント によると、Enquirerは複数の質問を連続して表示することに対応しているようですが、
Enquirer.prompt
メソッドにオプションの配列を渡してあげる形式にする必要があります。このままだとタイマー処理によってキャンセルすることができないので、どうにかしてPrompt
クラスのインスタンスを参照する必要があります。ということをやろうとしているのが上の方にあるこの処理です。
// Enquirerインスタンスの参照が欲しいのでstaticメソッドのpromptではなく // new Enquirer()して、そいつのpromptメソッドを呼ぶようにする const enquirer = new Enquirer(); enquirer.on('prompt', (prompt) => {Enquirerクラスは内部的に保持している
Prompt
インスタンスを処理するタイミングでprompt
イベントを発火しつつPrompt
インスタンスを渡してくれるので、そこでこれまでのケースと同じようにイベントハンドルを仕掛けています。先発で選んだポケモンを控えの選択肢に出さない
message
オプションなどと同様にchoices
オプションにも関数を指定することが可能です。そして、この関数内部でthis.state.answers
を参照することで前の質問に対する入力の値を得ることができます。コレを利用して、控えポケモンの選択肢から、先発に選んだポケモンを除外しています。choices() { return myPokemonNames().filter((name) => { return this.state.answers.starter !== name; }) },その他
今回の例では
choices
にはString配列を渡していましたが、{ name: '興梠', value: 'rocky' }
のようなオブジェクトの配列を渡すことで、見た目上はname
に指定した値を表示しつつ、実際にanswer
で得られるのはvalue
に指定した値にする、ということも可能です(多分大体そうする)。加えて、このようにオブジェクトを渡す方式にしている場合は、最後の例を実現するにあたって
choices
をフィルタする代わりに、各choice
のオブジェクトにdisabled: true
などを渡すことで選択不能な状態にすることができるみたいです。(というか複数聞きたいなら複数回
prompt
呼んでしまえばいいじゃないのって思ったけど違うのかな)まとめ
無事、ポケモン選出画面の要件を満たすことができました。
技術的には以下のあたりがリポジトリ検索したり調べたりコード見てみたりしないと見えてこなかった印象があるので、実現したい方は参考にしてみると良いでしょう。(PullRequestチャンスでもある)
- タイマーでプロンプトを終了させるためには
Prompt#cancel
- Promptクラスの参照は、最初から
Prompt
クラスを直接使った方法で実装するか、Enquirer
クラスのprompt
イベントをリスニングして降ってくるのを拾う最後に
この謎チュートリアルはノンフィクションです(実際に勢いでポケモン対戦できるCLIを書いているときの展開をほぼそのまま再現しました)
- 投稿日:2020-03-26T01:18:44+09:00
JavaScriptでちょっと複雑なcliを作るのに便利なEnquirer
この記事は
LAPRAS アウトプットリレー の...何日目だっけ?3/25の記事です!
こんにちは!LAPRAS エンジニアの @rockymanobi です!最近Node.jsでCLIを作る機会があり、その時に触ったEnquirerというライブラリが便利だったので、軽く紹介してみようというものです。ツールそのものについて軽くふれつつ、制作過程で出てきた「こんなことしたいけど、どう実現すれば良いんだろう」と試行錯誤して分かった使い方などを共有できればなと思います。
Enquirerとは
Enquirerは CLIアプリケーションにおける対話的インターフェイスの実装を楽にしてくれるライブラリです。単純なテキスト入力の受付はもちろん、リストからの選択、チェックボックス、パスワード、入力補完、など、様々な入力方式を手軽に組み込むことができます。Node.js製です。JavaScript(TypeScript)万歳!
公式サイトによるとこんなのもできるそうです(凄い!!
いつ使うんだろう)
類似のツールとしては先発の Inquirer.js などがあります。公式サイトに「Inquirerより速いぜ!」とあったり、Inquirerとほぼ同じような記述方式をサポートしていることから、かなり意識して作っているように見えます。どちらも利用者が多く、メンテも続いているのでどちらを使っても良いでしょう。あえて比較をするならば、Enquirerの方が多少全体や中身を把握しやすかったのと、少し凝ったことをやろうとしたときに素直そうな印象でした(個人の感想です)。
基本的な使い方
const Enquirer = require('enquirer'); (async ()=> { const question = { type: 'select', name: 'favorite', message: '好きな乗り物は?', choices: ['パトカー', '救急車', '消防車'], }; const answer = await Enquirer.prompt(question); console.log(`僕も${answer.favorite}が好きだよ`); })();このように
prompt
メソッドにオプションを渡してやると、それに応じた方式で入力を受け付ける描画をしたのち、ユーザの入力が完了したときにPromise
をresolve
してくれます。結果はオプションname
に指定したキーにぶら下がってきます。この要領で公式ドキュメントを見ながら使えば、大抵のことは実現できると思います(少し違う使い方もありますが後で触れます)
少し複雑なケースの実装
ここからは少しだけ複雑な要件に対応してみます。
複雑な入力と言われて最初に思い浮かぶのはポケモンバトルの選出画面です。故にここからは「ポケモンバトルの選出画面をCLIで実装するとしたら」をテーマに少しづつ進めていきたいと思います。リストから要素を複数選択(これは単純
ポケモンの対戦は、基本的に以下のような流れで進行します。
1. ポケモン6匹でパーティを構築する 2. 対戦前にお互いにパーティを見せ合う 3. 6匹のうち3匹を選出し、3vs3で対戦今回の対応範囲である「選出」というのはこの3番めにある「6匹のうち3匹を選ぶ」作業のことを指します。
以上を踏まえると、選出画面のCLI版は以下のようなものになりそうです。
実装は以下のようになります。const Enquirer = require('enquirer'); const myPokemonNames = [ 'フシギバナ', 'リザードン', 'カメックス', 'ゴリランダー', 'エースバーン', 'インテレオン', ]; (async ()=> { const question = { name: 'selections', type: 'select', multiple: true, message: '誰を出す?', choices: myPokemonNames, validate: (selectedItems) => { if(selectedItems.length === 3){ // true/falseを返すとOK/NGのみを表現 return true; } // 文字列を返すとエラーメッセージになる return '3匹選んでください'; }, }; const answer = await Enquirer.prompt(question); console.log(`${answer.selections.join(',')} を選出しました`); })();オプション
multiple: true
を渡すことで、複数選択可能なチェックボックス式の入力を受け付けます。また、
validate
にバリデーション用の関数を渡すことで、ユーザ入力を検証して、不適合な入力を弾き、入力画面をキープすることができます。エラーメッセージを独自のものにしたい場合は、true/false
ではなく文字列を返すようにすることで、判定をNGとしたうえで、関数が返した文字列をエラーメッセージとして表示してくれます(errorMessageってオプションあったほうがわかりやすいきがしますが)。タイマーで入力をキャンセルする
ここまでは公式Readmeにもしっかり書いてあるので難なく対応できました。が、要件を一つ忘れていたのでここで追加します。
ポケモンの対戦では遅延行為を防ぐため、あらゆる行動に制限時間が設けられています。もちろん選出も例外ではなく、すべてのプレイヤーは1分30秒(記事執筆時点)以内で3匹のポケモンを選び出す必要があります。これに間に合わない場合は、強制的に上から順に3匹のポケモンが選出されます。
この要件をCLIに反映させてみます。
const Enquirer = require('enquirer'); // (信じられないことに)配列 myPokemonNamesをchoicesオプションとして渡すと破壊的に配列が変更されるので、 // 違うArrayインスタンスを返すようにFunctionに包んでいる const myPokemonNames = () =>{ return [ 'フシギバナ', 'リザードン', 'カメックス', 'ゴリランダー', 'エースバーン', 'インテレオン', ]; }; (async ()=> { const prompt = new Enquirer.MultiSelect({ name: 'selections', message: '誰を出す?', choices: myPokemonNames(), validate: (selectedItems) => { if(selectedItems.length === 3){ return true; } return '3匹選んでください'; }, }) let timer; prompt.once('run', ()=>{ timer = setTimeout(()=>{ prompt.cancel() }, 5000) }) prompt.once('close', ()=>{ clearTimeout(timer); }); const answer = await prompt.run().catch(() => { // 時間切れです return myPokemonNames().slice(0,3); }); console.log(`${answer.join(',')} を選出しました`); })();これまでとは少し実装方法を変えています。これまではInquirer.js風の実装をしていましたが、ここではコチラのように、ライブラリにビルトインで入っている
Prompt
の子クラスを用いた実装にしています。コンストラクタの形式はこれまで
prompt
メソッドに渡していたものに似ていますが、各クラスごとに自明なものMultipleSelect
クラスにおけるtype
やmultiple:true
など)が不要になっています(TypeScriptなら型チェックも効いて快適)。この方式では
Prompt
クラスのrun()
メソッドを実行したときにユーザの入力を受け付けるようになり、同cansel()
メソッドを呼んでやることで、強制的に入力を終了させることが可能です。
Prompt
クラスはEventEmitter
を継承しており、上記コードでは入力受付開始時のrun
イベントに反応してタイマーを作動させ、一定時間経過後にキャンセルするようにしています。入力終了時(キャンセル/タイムアウト含む)に発生するclose
イベントが発生したタイミングでは、タイマーを止める処理を実行するようにしています。最後に、プロンプトをキャンセルしたときには
Promise
がreject
されるので、catch節でエラーを拾って、「時間切れの場合は先頭の3匹強制選出」を示す結果を返すようにしています(ちなみにcatch節のコールバックには何も引数が入って来ません)カウントダウンを表示する
制限時間を超過すると時間切れになるようにはできましたが、選出中に「後何秒?」が分からないのは辛いものがあります。これも対応しましょう。
(async ()=> { let timeRemaining = 10; const prompt = new Enquirer.MultiSelect({ name: 'selections', message: () => { return `誰を出す? 残り ${timeRemaining} 秒` }, choices: myPokemonNames(), validate: (selectedItems) => { if(selectedItems.length === 3){ return true; } return '3匹選んでください'; }, }) let interval; prompt.once('run', ()=>{ interval = setInterval(()=>{ timeRemaining -= 1; if(timeRemaining <= 0){ prompt.cancel() } else { prompt.render() } }, 1000) }) prompt.once('close', ()=>{ clearInterval(interval); }); const answer = await prompt.run().catch(()=>{ console.log('時間切れです'); return myPokemonNames().slice(0,3); }); console.log(`${answer.join(',')} を選出しました`); })();タイマー処理を
setTimeout
をsetInterval
に変えて、1秒毎にカウントダウンするように変更しつつ、メッセージに「残り n 秒」を表示させるために、以下の修正を施しています。
message
オプションに残り秒数を表示する文字列を返す関数を渡す- 1秒毎に
prompt.render()
メソッドを実行するこれにより、プロンプトの内容が毎秒再描画され、残り時間がカウントダウンされていく様子を表示することができました。カウントダウン部分のみを他のライブラリや独自実装などで代替しようとすると、カーソルの状態が衝突して表示がおかしくなったりするので、このあたりをサポートしてくれているのは有り難い限りです。
複数の質問をする & 前の回答を考慮して選択肢を変更する
これで完成かと思いましたが、そうはいきません。確かに選出は6匹から3匹を選び出す作業ですが、同時に「誰を先発させるか」を決める作業でもあることを忘れていました。
最初に選択する要素には特別な意味をもたせる必要がありそうなので、最初に先発を聞いて選んでもらった後に、控えの二匹を選出してもらうようにしてみます。
(async ()=> { let timeRemaining = 10; let currentPrompt; let interval; // Enquirerインスタンスの参照が欲しいのでstaticメソッドのpromptではなく // new Enquirer()して、そいつのpromptメソッドを呼ぶようにする const enquirer = new Enquirer(); enquirer.on('prompt', (prompt) => { currentPrompt = prompt; prompt.once('run', ()=>{ interval = setInterval(()=>{ timeRemaining -= 1; if(timeRemaining <= 0){ currentPrompt.cancel() } else { currentPrompt.render() } }, 1000) }); prompt.once('close', ()=>{ clearInterval(interval); }); }) const answer = await enquirer.prompt([ { type: 'select', name: 'starter', message: () => { return `先発は誰にする? 残り ${timeRemaining} 秒` }, choices: myPokemonNames(), }, { type: 'select', multiple: true, name: 'reserves', message: () => { return `控えは誰にする? 残り ${timeRemaining} 秒` }, choices() { return myPokemonNames().filter((name) => { return this.state.answers.starter !== name; }) }, validate: (selectedItems) => { if(selectedItems.length === 2){ return true; } return '2匹選んでください'; }, } ]).catch(console.error); if (answer) { const selected = [answer.starter].concat(answer.reserves); console.log(`${selected.join(',')} を選出しました`); } else { console.log('時間切れです') console.log(`${myPokemonNames().slice(0,3).join(',')} を選出しました`); } })();公式ドキュメント によると、Enquirerは複数の質問を連続して表示することに対応しているようですが、
Enquirer.prompt
メソッドにオプションの配列を渡してあげる形式にする必要があります。このままだとタイマー処理によってキャンセルすることができないので、どうにかしてPrompt
クラスのインスタンスを参照する必要があります。ということをやろうとしているのが上の方にあるこの処理です。
// Enquirerインスタンスの参照が欲しいのでstaticメソッドのpromptではなく // new Enquirer()して、そいつのpromptメソッドを呼ぶようにする const enquirer = new Enquirer(); enquirer.on('prompt', (prompt) => {Enquirerクラスは内部的に保持している
Prompt
インスタンスを処理するタイミングでprompt
イベントを発火しつつPrompt
インスタンスを渡してくれるので、そこでこれまでのケースと同じようにイベントハンドルを仕掛けています。先発で選んだポケモンを控えの選択肢に出さない
message
オプションなどと同様にchoices
オプションにも関数を指定することが可能です。そして、この関数内部でthis.state.answers
を参照することで前の質問に対する入力の値を得ることができます。コレを利用して、控えポケモンの選択肢から、先発に選んだポケモンを除外しています。choices() { return myPokemonNames().filter((name) => { return this.state.answers.starter !== name; }) },その他
今回の例では
choices
にはString配列を渡していましたが、{ name: '興梠', value: 'rocky' }
のようなオブジェクトの配列を渡すことで、見た目上はname
に指定した値を表示しつつ、実際にanswer
で得られるのはvalue
に指定した値にする、ということも可能です(多分大体そうする)。加えて、このようにオブジェクトを渡す方式にしている場合は、最後の例を実現するにあたって
choices
をフィルタする代わりに、各choice
のオブジェクトにdisabled: true
などを渡すことで選択不能な状態にすることができるみたいです。(というか複数聞きたいなら複数回
prompt
呼んでしまえばいいじゃないのって思ったけど違うのかな)まとめ
無事、ポケモン選出画面の要件を満たすことができました。
技術的には以下のあたりがリポジトリ検索したり調べたりコード見てみたりしないと見えてこなかった印象があるので、実現したい方は参考にしてみると良いでしょう。(PullRequestチャンスでもある)
- タイマーでプロンプトを終了させるためには
Prompt#cancel
- Promptクラスの参照は、最初から
Prompt
クラスを直接使った方法で実装するか、Enquirer
クラスのprompt
イベントをリスニングして降ってくるのを拾う最後に
この謎チュートリアルはノンフィクションです(実際に勢いでポケモン対戦できるCLIを書いているときの展開をほぼそのまま再現しました)
追記 : CLIじゃなくてCUIって指摘をどこかで頂きましたが、定義の違いについて明確に書かれているものは見つけられなかったので、教えて頂けるとありがたいです