NPM工程化 & inquirer源码解析

Posted 米花儿团儿

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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

// 切换到NPM包目录下
npm link

npm link可以将本地包以软链的形式注册到全局node_modules/bin下,以npm_package_name为包名。

// 切换到项目目录下
npm link package_name

在项目目录下,通过npm link package_name可以将本地NPM包链接到项目中,进行本地调试开发。

// 在项目目录下
npm unlink package_name

unlink取消项目与本地NPM包的绑定

// 在NPM包目录下
npm unlink

取消本地NPM包的全局注册

基于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.stdoutconsole.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对应的结果进行输出。

以上是关于NPM工程化 & inquirer源码解析的主要内容,如果未能解决你的问题,请参考以下文章

Inquirer.js

MyBatis源码- SqlSession门面模式 & selectList 源码解析

一文弄懂 npm & yarn 包管理机制(深度解析!)

inquire和enquire的的 区别

inquire 和enquire 的区别

python“inquirer”模块是不是不适用于 Jupyter Notebook?