乐趣区

关于node.js:NPM工程化-inquirer源码解析

npm run-script 利用开始

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

...
  "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"
  },
...

对其中一一解说:

自定义npm run-script

NPM 敌对型环境 (npm init -y) 下,能够将 node index.js 定义在 npm_package_scripts_* 中作为别名间接执行。

{
  "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.js

npm_package变量

npm run-script自定义的命令,能够将 package.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 字段定义常量:

{
  "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 确认环境变量门路。

#!/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 对应的后果进行输入。

退出移动版