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


米花儿团儿
1.3k 声望75 粉丝