20200326のNode.jsに関する記事は5件です。

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.0

Node.jsのバージョンが表示されたので無事にインストール出来たようです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Node.js] express + nodemon + BrowserSync でゲボ楽コーディング

はじめに

expressを使ってBrowserSyncを使う記事が少なかったので書きます。
nodemon + BrowserSyncでブラウザノータッチの楽々コーディングを目指そう。

方法

  1. expres側ファイルをnodemonで監視(3000portで起動)
  2. 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 or browser-sync どちらかしか動きません。
nodemonbrowser-syncも一度呼び出すと永続するので・・・

"start": "npm-run-all --parallel←コレ start:*",

start:*に関しては後ろ2行の:以下を表しています。

 "start:nodemon←コレ": "nodemon ./bin/www",
 "start:sync←コレモ": "browser-sync start --config bs-config.js"

他にもいろいろ応用できそう。

参考記事

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

話題のanalyzeコマンドを実装してみた

今、イケてるエンジニア界隈で話題沸騰中のanalyzeコマンドをご存知でしょうか。

こういうやつですね。

スクリーンショット 2020-03-26 9.41.49.png

Yet another analyze command

コレCLIっぽい見た目をしていますが、実はWebブラウザ上1でしか動作しません。不便ですね。
Shellでも使いたい!という声にお答えして、実装してみました!!

コチラです。
NPM

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が便利です。(申し訳程度の技術要素)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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メソッドにオプションを渡してやると、それに応じた方式で入力を受け付ける描画をしたのち、ユーザの入力が完了したときにPromiseresolveしてくれます。結果はオプション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クラスにおけるtypemultiple:trueなど)が不要になっています(TypeScriptなら型チェックも効いて快適)。

この方式ではPromptクラスのrun()メソッドを実行したときにユーザの入力を受け付けるようになり、同cansel()メソッドを呼んでやることで、強制的に入力を終了させることが可能です。

PromptクラスはEventEmitterを継承しており、上記コードでは入力受付開始時のrunイベントに反応してタイマーを作動させ、一定時間経過後にキャンセルするようにしています。入力終了時(キャンセル/タイムアウト含む)に発生するcloseイベントが発生したタイミングでは、タイマーを止める処理を実行するようにしています。

最後に、プロンプトをキャンセルしたときにはPromiserejectされるので、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(',')} を選出しました`);
})();

タイマー処理をsetTimeoutsetIntervalに変えて、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を書いているときの展開をほぼそのまま再現しました)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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メソッドにオプションを渡してやると、それに応じた方式で入力を受け付ける描画をしたのち、ユーザの入力が完了したときにPromiseresolveしてくれます。結果はオプション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クラスにおけるtypemultiple:trueなど)が不要になっています(TypeScriptなら型チェックも効いて快適)。

この方式ではPromptクラスのrun()メソッドを実行したときにユーザの入力を受け付けるようになり、同cansel()メソッドを呼んでやることで、強制的に入力を終了させることが可能です。

PromptクラスはEventEmitterを継承しており、上記コードでは入力受付開始時のrunイベントに反応してタイマーを作動させ、一定時間経過後にキャンセルするようにしています。入力終了時(キャンセル/タイムアウト含む)に発生するcloseイベントが発生したタイミングでは、タイマーを止める処理を実行するようにしています。

最後に、プロンプトをキャンセルしたときにはPromiserejectされるので、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(',')} を選出しました`);
})();

タイマー処理をsetTimeoutsetIntervalに変えて、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って指摘をどこかで頂きましたが、定義の違いについて明確に書かれているものは見つけられなかったので、教えて頂けるとありがたいです

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む