以inquirer講解原生Node命令行

npm run-script應用開始

查看某些NPM包的npm_package_scripts,常常能夠看到一下run-script示例:html

...
  "scripts": {
    "prerelease": "npm test && npm run integration",
    "release": "env-cmd lerna version",
    "postversion": "lerna publish from-git",
    "fix": "npm run lint -- --fix",
    "lint": "eslint . -c .eslintrc.yaml --no-eslintrc --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint",
    "integration": "jest --config jest.integration.js --maxWorkers=2",
    "pretest": "npm run lint",
    "test": "jest"
  },
...

對其中一一講解:node

自定義npm run-script

NPM友好型環境(npm init -y)下,能夠將node index.js定義在npm_package_scripts_*中做爲別名直接執行。git

{
  "name": "cli",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "d1": "node ./demo1/bin/operation.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

在命令行中輸入npm run d1就是執行node ./demo1/bin/operation.jsnpm

npm_package變量

npm run-script自定義的命令,能夠將package.json其它配置項當變量使用json

{
  "name": "cli",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "d1": "node ./demo1/bin/operation.js",
    "d1:var": "%npm_package_scripts_d1%",
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

在平常應用中,能夠用config字段定義常量:api

{
  "name": "cli",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "config": {
    "port": 8081
  },
  "scripts": {
    "d1": "node ./demo1/bin/operation.js",
    "d1:var": "%npm_package_scripts_d1%",
    "test": "echo %npm_package_config_port%"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

平臺差別數組

  • Linux/Mac:$npm_package_*
  • Windows:$npm_package_*
  • 跨平臺:cross_var第三方NPM包

shebang

僅在Unix系統中可用,在首行指定#!usr/bin/env node,執行文件時,會在該用戶的執行路徑下運行指定的執行環境異步

能夠經過type env確認環境變量路徑。async

#!/usr/bin/env node
console.log('-------------')

能夠直接以文件名執行上述文件,而不須要node index.js去執行函數

E:\demos\node\cli> ./index.js
--------

process.env環境變量

具備平臺差別

  • Unix: run-cli
mode=development npm run build

便可在邏輯代碼中可得到process.env.mode === "develop"

  • Windows: run-cli

不容許該方式定義環境變量

  • 跨平臺

藉助cross-env定義環境變量

多命令串行

示例以下:

{
  "name": "cli",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "d2:o1": "node ./demo2/bin/ope1.js",
    "d2:o2": "node ./demo2/bin/ope2.js",
    "d2:err": "node ./demo2/bin/op_error.js",
    "d2": "npm run d2:o1 && npm run d2:o2"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

&&能夠鏈接多個命令,使之串行執行。

若前一命令中有異步方法,會等異步執行結束,進程徹底結束後,纔會執行後繼命令。

// ./demo2/bin/ope1.js
console.log(1)
setTimeout(() => {
  console.log(2)
}, 4000)
console.log(3)
// ./demo2/bin/ope2.js
console.log(4)

執行結果:

1
3
2
4

多命令並行

具備平臺差別

  • Unix: &能夠鏈接多個命令,使之並行執行。
  • Windows&多命令依舊串行。
  • 跨平臺:藉助npm-run-all第三方NPM包

串行示例在Mac輸出結果:

1
3
4
2

條件執行

在多命令編排的流程中,可能在某些條件下須要結束流程。

當即結束process.exit(1)

// demo2/bin/op_error.js
console.log(1)
process.exit(1)
setTimeout(() => {
  console.log(2)
}, 4000)
console.log(3)
// demo2/bin/ope2.js
console.log(4)

執行命令"d2:error": "npm run d2:err && npm run d2:o2",輸出結果:

1
Error

其中process.exit(1)後續的代碼及任務都再也不執行。

當前進程執行完結束process.exitCode = 1

// demo2/bin/op_error.js
console.log(1)
process.exitCode = 1
setTimeout(() => {
  console.log(2)
}, 4000)
console.log(3)

改造op_error.js,執行npm run d2:error,輸出結果:

1
3
2
Error

其中process.exitCode = 1後續的代碼仍繼續執行,然後繼任務再也不執行。

npm run-script傳參

npm run-script參數

自定義命令"d4": "node ./demo4/bin/operation.js"

console.log(process.argv)

執行npm run d4 -f,輸出結果:

E:\demos\node\cli>npm run d4 -f
npm WARN using --force I sure hope you know what you are doing.

> cli@1.0.0 d4 E:\demos\node\cli
> node ./demo4/bin/operation.js

[
  'D:\\nodejs\\node.exe',
  'E:\\demos\\node\\cli\\demo4\\bin\\operation.js'
]

其中,-f不被bin/operation.js承接,而是做爲npm run-script的參數消化掉(即便npm run-script不識別該參數)。

  • -s

    • 靜默執行npm run-script:忽略日誌輸出
  • -d

    • 調試模式執行npm run-script:日誌全Level輸出

界定npm run-script結束

執行npm run d4 -- -f,輸出結果:

E:\demos\node\cli>npm run d4 -- -f

> cli@1.0.0 d4 E:\demos\node\cli
> node ./demo4/bin/operation.js "-f"

[
  'D:\\nodejs\\node.exe',
  'E:\\demos\\node\\cli\\demo4\\bin\\operation.js',
  '-f'
]

其中,-fbin/operation.js承接。

可見,在npm run-script <command>後使用--界定npm參數的結束,npm會將--以後的全部參數直接傳遞給自定義的腳本。

NPM鉤子

npm_package_scripts_*定義

{
  "name": "cli",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "pred5": "node ./demo5/bin/pre.js",
    "d5": "node ./demo5/bin/operation.js",
    "postd5": "node ./demo5/bin/post.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

執行npm run d5,在執行node ./demo5/bin/operation.js以前會自動執行"pred5": "node ./demo5/bin/pre.js",在執行node ./demo5/bin/operation.js以後會自動執行"postd5": "node ./demo5/bin/post.js"

node_modules/.hooks/定義

Unix可用

  1. 建立node_modules/.hooks/目錄
  2. 建立pred5文件

    console.log('---------pre--------')
  3. 修改文件權限爲可執行chmod 777 node_modules/.hooks/pred5
  4. 執行命令npm run d5便可

場景:

"postinstall": "husky install"

NPM全局命令行調試

npm link能夠將本地包以軟鏈的形式註冊到全局node_modules/bin下,作僞全局調試。

基於Node模塊的命令行

示例1:process.stdin & process.stdout交互命令行

function cli () {
  process.stdout.write("Hello");
  process.stdout.write("World");
  process.stdout.write("!!!");
  process.stdout.write('\n')
  console.log("Hello");
  console.log("World");
  console.log("!!!");
  process.on('exit', function () {
    console.log('----exit')
  })
  process.stdin.setEncoding('utf8')

  process.stdin.on('data', (input) => {
    console.dir(input)
    input = input.toString().trim()
    if (['Y', 'y', 'YES', 'yes'].indexOf(input) > -1) {
      console.log('success')
    }
    if (['N', 'n', 'No', 'no'].indexOf(input) > -1) {
      console.log('reject')
    }
  })
  process.stdout.write('......\n')
  console.log('----------------00000000000------------')
  process.stdout.write('確認執行嗎(y/n)?')
  process.stdout.write('......\n')
}
cli()

stdin

  • 標準輸入監聽控制檯的輸入
  • 以回車標識結束
  • 獲取的輸入包含回車字符

stdout

process.stdout vs. console.log

其中console.log輸出底層調用的是process.stdout,在輸出以前進行了處理,好比調用util.format方法

區別 process.stdout console.log
參數 只能接收字符串作參數 支持ECMA的全部數據類型
參數個數 僅一個字符串 能夠接收多個
換行 行內連續輸出 自動追加換行
格式化 不支持 支持'%s'、'%c'格式化
輸出自身 WriteStream對象 字符串

示例2:process.stdin工做模式

process.stdin.setEncoding('utf8');

function readlineSync() {
  return new Promise((resolve, reject) => {
    console.log(`--status----${process.stdin.readableFlowing}`);
    process.stdin.resume();
    process.stdin.on('data', function (data) {
      console.log(`--status----${process.stdin.readableFlowing}`);
      process.stdin.pause(); // stops after one line reads  // 暫停 input 流,容許稍後在必要時恢復它。
      console.log(`--status----${process.stdin.readableFlowing}`);
      resolve(data);
    });
  });
}

async function main() {
  let input = await readlineSync();
  console.log('inputLine1 = ', input);
  console.log('bye');
}

main();

若n次調用readlineSync(),會爲data事件監聽屢次綁上處理函數,回調函數會執行n次。

stdin

標準輸入是可讀流的實例

工做模式

符合可讀流的工做模式:

  • 流動模式(flowing)

    在流動模式中,數據自動從底層系統讀取,並經過 EventEmitte接口的事件儘量快地被提供給應用程序
  • 暫停模式(paused)

    在暫停模式中,必須顯式調用 stream.read()讀取數據塊

工做狀態

  • null
  • false
  • true
    可經過readable.readableFlowing查看相應的工做模式

狀態切換

  • 添加 'data' 事件句柄。
  • 調用 stream.resume() 方法。
  • 調用 stream.pipe() 方法將數據發送到可寫流。

進程結束

  • 若是事件循環中沒有待處理的額外工做,則 Node.js 進程會自行退出。
  • 調用process.exit()會強制進程儘快退出,即便還有還沒有徹底完成的異步操做在等待,包括對 process.stdoutprocess.stderr 的 I/O 操做。

示例3:readline模塊

const readline = require('readline');
const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
  prompt: '請輸入> '
});

rl.prompt();

rl.on('line', (line) => {
  console.dir(line)
  switch (line.trim()) {
    case 'hello':
      console.log('world!');
      break;
    default:
      console.log(`你輸入的是:'${line.trim()}'`);
      break;
  }
  rl.prompt();
}).on('close', () => {
  console.log('再見!');
  process.exit(0);
});

readline模塊

建立UI界面

const rl = readline.createInterface({
  input: process.stdin,  // 定義輸入UI
  output: process.stdout,  // 定義輸出UI
  historySize: 0,    // 禁止歷史滾動 —— 默認:30
  removeHistoryDuplicates: true,  // 輸入歷史去重 —— 默認:false
  completer: function (line) { // 製表符自動填充匹配文本
    const completions = '.help .error .exit .quit .q'.split(' ');
    const hits = completions.filter((c) => c.startsWith(line));
    return [hits.length ? hits : completions, line];  // 輸出數組:0 —— 匹配結果;1 —— 輸入
  },
  prompt: '請輸入> '  // 命令行前綴
});

方法

rl.prompt()

之前綴開啓新的輸入行

rl.close()

關閉readline.Interface實例,並放棄對inputoutput流的控制

事件

line事件
rl.on('line', (line) => {
  // 相對比process.stdin.on('data', function (chunk) {}),輸入line不包含換行符
  switch (line.trim()) {
    case 'hello':
      console.log('world!');
      break;
    default:
      console.log(`你輸入的是:'${line.trim()}'`);
      break;
  }
  rl.prompt();
});

inquirer源碼解析

核心:
  • 命令行UI

    • readline.createInterface
  • 渲染輸出

    • rl.output.write
  • 事件監聽

    • rxjs
加強交互體驗:
  • mute-stream:控制輸出流輸出
  • chalk:多彩日誌打印
  • figures:命令行小圖標
  • cli-cursor:光標的隱藏、顯示控制
下面以type="list"爲例進行說明

建立命令行

this.rl = readline.createInterface({
  terminal: true,
  input: process.stdin,
  output: process.stdout
})

渲染輸出

var obs = from(questions)
this.process = obs.pipe(
  concatMap(this.processQuestion.bind(this)),
  publish()
)

將傳入的參數轉換爲數據流形式,對其中的每一項數據進行渲染processQuestion

render(error) {
    var message = this.getQuestion();
    if (this.firstRender) {
      message += chalk.dim('(Use arrow keys)');
    }
    if (this.status === 'answered') {
      message += chalk.cyan(this.opt.choices.getChoice(this.selected).short);
    } else {
      var choicesStr = listRender(this.opt.choices, this.selected);
      var indexPosition = this.opt.choices.indexOf(
        this.opt.choices.getChoice(this.selected)
      );
      message +=
        '\n' + choicesStr;
    }
    this.firstRender = false;
    this.rl.output.unmute();
    this.rl.output.write(message);
    this.rl.output.mute();
  }

其中:

  • 藉助chalk進行輸出的色彩多樣化;
  • listRender將每個choice拼接爲字符串;
  • 使用this.selected標識當前選中項,默認爲0;
  • 使用this.rl.output.write將字符串輸出;
  • 藉助mute-stream控制命令行無效輸出;

事件監聽

function observe(rl) {
  var keypress = fromEvent(rl.input, 'keypress', normalizeKeypressEvents)
    .pipe(takeUntil(fromEvent(rl, 'close')))
    // Ignore `enter` key. On the readline, we only care about the `line` event.
    .pipe(filter(({ key }) => key !== 'enter' && key.name !== 'return'));
  return {
    line: fromEvent(rl, 'line'),
    keypress: keypress,
    normalizedUpKey: keypress.pipe(
      filter(
        ({ key }) =>
          key.name === 'up' || key.name === 'k' || (key.name === 'p' && key.ctrl)
      ),
      share()
    ),
    normalizedDownKey: keypress.pipe(
      filter(
        ({ key }) =>
          key.name === 'down' || key.name === 'j' || (key.name === 'n' && key.ctrl)
      ),
      share()
    ),
    numberKey: keypress.pipe(
      filter((e) => e.value && '123456789'.indexOf(e.value) >= 0),
      map((e) => Number(e.value)),
      share()
    ),
  };
};

藉助Rx.fromEvent監聽命令行的keypressline事件。

var events = observe(this.rl);
events.normalizedUpKey
  .pipe(takeUntil(events.line))
  .forEach(this.onUpKey.bind(this));
events.normalizedDownKey
  .pipe(takeUntil(events.line))
  .forEach(this.onDownKey.bind(this));
events.line
  .pipe(
    take(1)
  )
  .forEach(this.onSubmit.bind(this));

訂閱事件,對相應的事件進行處理

onUpKey () {
    console.log('--------up')
    this.selected = incrementListIndex(this.selected, 'up', this.opt);
    this.render();
  }
  onDownKey () {
    console.log('--------down')
    this.selected = incrementListIndex(this.selected, 'down', this.opt);
    this.render();
  }
  onSubmit () {
    console.log('------------submit')
  }

修改this.selected值,經過this.render進行命令行的界面更新。
監聽line事件,將this.selected對應的結果進行輸出。

相關文章
相關標籤/搜索