Node 系列 - 004 - Inquirer.js

——————————☆☆☆——————————html

Node 系列相應地址:前端

——————————☆☆☆——————————node

一 前言

通過前面 TypeScript 環境的搭建和 commander.js 的配合,咱們如今能夠在 .ts 文件中編寫對應指令,而後經過 npm run xxx 來運行項目了,可是這種方式有個 Bug:git

  • 當指令過多的時候,咱們壓根記不住那麼多的指令!

因此,就須要一個智能提示,將指令簡化並可視化。github

二 集成 Inquirer.js

這邊 jsliang 想的一個法子就是經過終端那種問答形式的來解決這個問題(後續可能安排頁面或者 Chrome 插件等)typescript

那麼,廢話少說,Here we go~npm

首先,安裝必須的包:json

  • 安裝 Inquirer.jsnpm i inquirer
  • 安裝 @types/inquirer(可選,TS 必裝):npm i @types/inquirer -D

而後。咱們就能夠開始耍起來了,接入前面的 TypeScript 和 commander.js,拿起 index.tspackage.json 就是一頓修改:數組

src/index.ts
import program from 'commander';
import inquirer from 'inquirer';
import { sortCatalog } from './sortCatalog';

program
  .version('0.0.1')
  .description('工具庫')

program
  .command('jsliang')
  .description('jsliang 幫助指令')
  .action(() => {
    inquirer
    .prompt([
      { 
        type: 'rawlist',
        name: 'question1',
        message: '請問須要什麼服務?',
        choices: ['公共服務', '其餘']
      },
    ])
    .then((answers) => {
      if (answers.question1 === '公共服務') {
        inquirer.prompt([
          {
            type: 'rawlist',
            name: 'question',
            message: '當前公共服務有:',
            choices: ['文件排序']
          }
        ]).then((answers) => {
          if (answers.question === '文件排序') {
            inquirer.prompt([
              {
                type: 'input',
                name: 'question',
                message: '須要排序的文件夾爲?(絕對路徑)',
                default: 'D:/xx',
              }
            ]).then(async (answers) => {
              const result = await sortCatalog(answers.question);
              if (result) {
                console.log('排序成功!');
              }
            }).catch((error) => {
              console.error('出錯啦!', error);
            });
          }
        }).catch((error) => {
          console.error('出錯啦!', error);
        });
      } else if (answers === '其餘') {
        // 作其餘事情
      }
    }).catch((error) => {
      console.error('出錯啦!', error);
    });
  });

program.parse(process.argv);

注意這裏 sort 改爲 jsliang 了(人不要臉天下無敵)。async

package.json
{
  "name": "jsliang",
  "version": "1.0.0",
  "description": "Fe-util, Node 工具庫",
  "main": "index.js",
  "scripts": {
    "jsliang": "ts-node ./src/index.ts jsliang"
  },
  "keywords": [
    "jsliang",
    "Node 工具庫",
    "Node"
  ],
  "author": "jsliang",
  "license": "ISC",
  "devDependencies": {
    "@types/inquirer": "^7.3.1",
    "@types/node": "^15.12.2",
    "@typescript-eslint/eslint-plugin": "^4.26.1",
    "@typescript-eslint/parser": "^4.26.1",
    "eslint": "^7.28.0",
    "ts-node": "^10.0.0",
    "typescript": "^4.3.2"
  },
  "dependencies": {
    "commander": "^7.2.0",
    "inquirer": "^8.1.0"
  }
}

因而就有了效果:

Inquirer-01.png

同樣的絲滑好用,還能夠控制文件夾路徑了~

可是!小夥伴們看到上面代碼,是否是有種想吐的感受。

  • 問題 1:呀,這是啥,這些代碼你寫了什麼功能?
  • 問題 2:太噁心了吧,竟然不支持 async/await

OK,一一解決問題,我們先講解下 Inquirer.js 裏面的一些操做。

三 Inquirer.js 使用技巧

在上面的代碼中,經過 .prompt(Array<Object>) 能夠傳遞多個問題信息,而後經過回調獲取答案,舉例一個輸入框:

inquirer.prompt([
  { 
    type: 'input',
    name: 'question',
    message: '請問須要什麼服務?',
  }
]).then((res) => {
  console.log('成功!', res);
}).catch((err) => {
  console.error('報錯!', err);
});

其中 Object 裏面能夠塞:

  • type:【String】提示的類型,默認 input,包含 inputnumberconfirmlistrawlistexpandcheckboxpasswordeditor
  • name:【String】存儲當前問題回答的變量
  • message:【String|Function】提問的問題內容
  • default:【String|Number|Boolean|Array|Function】默認值
  • choices:【Array|Function】列表選項
  • validate:【Function】驗證方法,校驗輸入值是否可行,有效返回 true,不然返回字符串表示錯誤信息(返回 false 則爲默認的錯誤信息)
  • filter:【Function】對答案進行過濾處理,返回處理後的值
  • transformer:【Function】操做答案的顯示效果
  • when:【Function|Boolean】接受答案,根據前面的內容判斷是否須要展現該問題
  • pageSize:【Number】在 listrawlistexpandcheckbox 這種多選項中,進行分頁拆分
  • prefix:【String】修改默認前綴
  • suffix:【String】修改默認後綴
  • askAnswered:【Boolean】已有答案是否強制提問
  • loop:【Boolean】list 是否能循環滾動選擇,默認 true

相信你也看不懂,我們將一些可能用到的寫一寫用例吧。

後續代碼爲簡寫,全寫大概爲下面代碼所示,後面就不哆嗦了
import program from 'commander';
import inquirer from 'inquirer';

program
  .version('0.0.1')
  .description('工具庫')

program
  .command('jsliang')
  .description('jsliang 幫助指令')
  .action(() => {
    inquirer
    .prompt([
      { 
        type: 'rawlist',
        name: 'question',
        message: '請問須要什麼服務?',
        choices: ['公共服務', '其餘']
      },
    ])
    .then((answers) => {
      console.log('答案:', answers);
    }).catch((error) => {
      console.error('出錯啦!', error);
    });
  });

program.parse(process.argv);
注意:
① 下面這些舉例,你也能夠在 Inquires.js 中找到,可是 jsliang 但願搬運到本身這篇文章中方便後續檢索。
② 若是有評論沒看到這個註釋就吐槽 jsliang 抄寫人家 README,那 jsliang 也無話可說,只是被吐槽了幾回,稍微寫點註釋

3.1 輸入框

輸入文本

Inquirer-02.png

可配合參數:type, name, message[, default, filter, validate, transformer]

inquirer.prompt([
  { 
    type: 'input',
    name: 'question',
    message: '問題?',
    default: 'liangjunrong',
  }
]);

輸入數字

Inquirer-03.png

可配合參數:type, name, message[, default, filter, validate, transformer]

inquirer.prompt([
  { 
    type: 'number',
    name: 'question',
    message: '問題?',
    default: '1',
  }
]);

輸入密碼

Inquirer-04.png

可配合參數:type, name, message, mask,[, default, filter, validate]

inquirer.prompt([
  { 
    type: 'password',
    name: 'question',
    message: '問題?',
  }
]);

3.2 單選項

沒下標的單選項

Inquirer-05.png

可配合參數:type, name, message, choices[, default, filter, loop]

inquirer.prompt([
  { 
    type: 'list',
    name: 'question',
    message: '問題?',
    default: 'jsliang',
    choices: ['liangjunrong', 'jsliang']
  }
]);

添加分隔符

Inquirer-06.png

inquirer.prompt([
  { 
    type: 'list',
    name: 'question',
    message: '問題?',
    default: 'jsliang',
    choices: [
      'liangjunrong',
      new inquirer.Separator(), // 添加分隔符
      'jsliang',
    ]
  }
]);

有下標的單選項

Inquirer-07.png

可配合參數:type, name, message, choices[, default, filter, loop]

inquirer.prompt([
  { 
    type: 'rawlist',
    name: 'question',
    message: '問題?',
    default: 'jsliang',
    choices: ['liangjunrong', 'jsliang']
  }
]);

3.3 多選項

Inquirer-08.png

可配合參數:type, name, message, choices[, filter, validate, default, loop]

inquirer.prompt([
  { 
    type: 'checkbox',
    name: 'question',
    message: '問題?',
    choices: ['liangjunrong', 'jsliang']
  }
]);

3.4 確認框

Inquirer-09.png

可配合參數:type, name, message, [default]

inquirer.prompt([
  { 
    type: 'confirm',
    name: 'question',
    message: '問題?',
  }
]);

3.5 校驗輸入

Inquirer-10.png

inquirer.prompt([
  { 
    type: 'input',
    name: 'phone',
    message: '請輸入手機號',
    validate: (val) => {
      if (val.match(/\d{11}/g)) {
        return true;
      }
      return '請輸入 11 位數字';
    },
  }
]);

四 動態提問

上面咱們說了 2 個問題:

  • 問題 1:呀,這是啥,這些代碼你寫了什麼功能?
  • 問題 2:太噁心了吧,竟然不支持 async/await

剛纔已經將問題 1 解決了(就是這個 Inquires.js 功能支持),下面咱們看看問題 2 怎麼操做。

其實爲了解決這個問題,咱們須要按照 Inquires.js 中的推薦安裝 Rx.jsRx.js 參考文獻:

開始安裝:

  • 安裝 rxjsnpm i rxjs@5

當前版本爲 v7.1.0,可是看了下 Inquirer.js 舉例的是 v5.x 版本,找了一會找不到新版本的用法,只能出此下舉

其次 jsliang 是真的懶,不想了解 Rx.js 作啥子的,我只但願項目能按照 async/await 方式跑起來

import program from 'commander';
import Rx from 'rxjs/Rx';
import inquirer from 'inquirer';

const prompts = new Rx.Subject();

// 無情的信息處理器
inquirer.prompt(prompts).ui.process.subscribe((result) => {
  console.log('成功:', result);
}, (error: unknown) => {
  console.error('失敗', error);
}, () => {
  console.log('完成');
});

program
  .version('0.0.1')
  .description('工具庫')

program
  .command('jsliang')
  .description('jsliang 幫助指令')
  .action(() => {
    prompts.next({
      type: 'confirm',
      name: 'question',
      message: '問題?',
    });
    prompts.complete();
  });

program.parse(process.argv);

這樣就完成了封裝,更方便處理信息了。(能夠想象後面會有一堆 switch...case... 判斷)

可是,預想不到的是,在多個模塊接入 Inquire.js 後,出問題了。

多個模塊示例
+ src
  - index.ts
  + base
    - config.ts
  + common
    - inquirer.ts
  + jsliang
    - inquirer.ts

暫不須要按照這個目錄更改接口,如下一個目錄爲準

我的懷疑 Rx.js 是單實例緣故

運行時報錯提示:

Inquirer-11.png

npm ERR! code ELIFECYCLE
npm ERR! errno 1
npm ERR! jsliang@1.0.0 test: `ts-node ./src/index.ts test`
npm ERR! Exit status 1
npm ERR!
npm ERR! Failed at the jsliang@1.0.0 test script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.

npm ERR! A complete log of this run can be found in:
npm ERR!     C:\Users\wps\AppData\Roaming\npm-cache\_logs\2021-06-08T11_46_58_005Z-debug.log

排查了老久,應該跟我不熟悉 RX.js 有關,因此就想着能不能更新一波:

【準】按照這個目錄更改文件夾/文件
+ src —————————————————————— src 文件夾
  - index.ts ——————————————— 主入口
  + base ——————————————————— 基礎文件夾,例如 config/math 等
    - config.ts ———————————— 經常使用配置項
    - inquirer.ts —————————— inquirer 總處理口,統一封裝 async/await
    - interface.ts ————————— 暫時將全部通用的 interface.ts 放到這裏
  + common ————————————————— 通用功能
    - index.ts ————————————— common 處理問題的入口
    - sortCatalog.ts —————— inquirer 調用具體的功能文件
  + jsliang ———————————————— 業務功能
    - xx.ts ———————————————— 業務功能文件

順帶給個目錄圖吧:

Inquirer-12.png

src/base/inquirer.ts
import * as myInquirer from 'inquirer';
import Rx from 'rxjs/Rx';
import { Question } from './interface';

export const inquirer = (questions: Question[], answers: any): void => {
  const prompts = new Rx.Subject();

  // 長度判斷
  if (questions.length !== answers.length) {
    console.error('問題和答案長度不一致!');
  }

  // 問題列表
  const questionList = questions.map((item, index) => {
    return () => {
      prompts.next(Object.assign({}, item, {
        name: String(index),
      }));
    };
  });

  // 問題處理器
  myInquirer.prompt(prompts).ui.process.subscribe(async (res) => {
    console.log('執行成功,輸入信息爲:', res);
    const index = Number(res.name);
    
    // 回調函數:結果、問題列表、prompts(控制是否須要中止)
    answers[index](res, questionList, prompts);

    // 默認最後一個問題就自動終止
    if (index === answers.length - 1) {
      prompts.complete(); // 回調函數能夠手動控制終止詢問時機
    }
  }, (error: unknown) => {
    console.error('執行失敗,報錯信息爲:', error);
  }, () => {
    // console.log('完成'); // 一定會執行的代碼
  });

  // 執行第一個問題
  questionList[0]();
};
src/base/interface.ts
export interface Question {
  type: string,
  name?: string,
  message: string,
  default?: string,
  choices?: string[],
  validate?(): boolean,
}

export interface Result {
  name: string,
  answer: string,
}

按照這樣子設置後,就能夠在其餘地方愉快玩耍了:

src/common/index.ts
import { inquirer } from '../base/inquirer';
import { Result } from '../base/interface';
import { sortCatalog } from './sortCatalog';

const common = (): void => {
  // 測試新特性
  const questionList = [
    {
      type: 'list',
      message: '請問須要什麼服務?',
      choices: ['公共服務', '其餘']
    },
    {
      type: 'list',
      message: '當前公共服務有:',
      choices: ['文件排序']
    },
    {
      type: 'input',
      message: '須要排序的文件夾爲?(絕對路徑)',
      default: 'D:/xx',
    },
  ];

  const answerList = [
    async (result: Result, questions: any) => {
      if (result.answer === '公共服務') {
        questions[1]();
      } else if (result.answer === '其餘') {
        // 作其餘事情
        console.log('暫未開通該服務');
      }
    },
    async (result: Result, questions: any) => {
      console.log(result);
      if (result.answer === '文件排序') {
        questions[2]();
      }
    },
    async (result: Result) => {
      const sortResult = await sortCatalog(result.answer);
      if (sortResult) {
        console.log('排序成功!');
      }
    },
  ];

  inquirer(questionList, answerList);
};

export default common;

傳遞問題數組,而後回調函數處理內容,知足我當前的需求,咱就再也不改造了。

其餘詳細文件內容以下:

src/index.ts
import program from 'commander';
import common from './common';

program
  .version('0.0.1')
  .description('工具庫')

program
  .command('jsliang')
  .description('jsliang 幫助指令')
  .action(() => {
    common();
  });

program.parse(process.argv);
src/base/config.ts
/**
 * @name 默認的全局配置
 * @time 2021-05-22 16:12:21
 */
import path from 'path';

// 基礎目錄
export const BASE_PATH = path.join(__dirname, './docs');

// 忽略目錄
export const IGNORE_PATH = [
  '.vscode',
  'node_modules',
];
src/common/sortCatalog.ts
/**
 * @name 文件排序功能
 * @time 2021-05-22 16:08:06
 * @description 規則
   1. 系統順序 1/10/2/21/3,但願排序 1/2/3/10/21
   2. 插入文件 1/2/1-1,但願排序 1/2/3(將 1-1 變成 2,2 變成 3)
*/
import fs from 'fs';
import path from 'path';
import { IGNORE_PATH } from '../base/config';

const recursion = (filePath: string, level = 0) => {
  const files = fs.readdirSync(filePath);

  files
    .filter((item => !IGNORE_PATH.includes(item))) // 過濾忽略文件/文件夾
    .sort((a, b) =>
      Number((a.split('.')[0]).replace('-', '.'))
      - Number((b.split('.')[0]).replace('-', '.'))
    ) // 排序文件夾
    .forEach((item, index) => { // 遍歷文件夾
      // 設置舊文件名稱和新文件名稱
      const oldFileName = item;
      const newFileName = `${index + 1}.${oldFileName.slice(oldFileName.indexOf('.') + 1)}`;

      // 設置舊文件路徑和新文件路徑
      const oldPath = `${filePath}/${oldFileName}`;
      const newPath = `${filePath}/${newFileName}`;

      // 判斷文件格式
      const stat = fs.statSync(oldPath);

      // 判斷是文件夾仍是文件
      if (stat.isFile()) {
        fs.renameSync(oldPath, newPath); // 重命名文件
      } else if (stat.isDirectory()) {
        fs.renameSync(oldPath, newPath); // 重命名文件夾
        recursion(newPath, level + 1); // 遞歸文件夾
      }
    });
};

export const sortCatalog = (filePath: string): boolean => {
  // 絕對路徑
  if (path.isAbsolute(filePath)) {
    recursion(filePath);
  } else { // 相對路徑
    recursion(path.join(__dirname, filePath));
  }

  return true;
};

那麼,Inquirer.js 接入就搞定了,試試咱們的 npm run jsliang,能夠正常使用!

後面能夠愉快寫功能啦~

五 參考文獻


jsliang 的文檔庫由 梁峻榮 採用 知識共享 署名-非商業性使用-相同方式共享 4.0 國際 許可協議 進行許可。<br/>基於 https://github.com/LiangJunrong/document-library 上的做品創做。<br/>本許可協議受權以外的使用權限能夠從 https://creativecommons.org/licenses/by-nc-sa/2.5/cn/ 處得到。
相關文章
相關標籤/搜索