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\'
]
其中,-f
被bin/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
可用
- 创建
node_modules/.hooks/
目录 创建
pred5
文件console.log(\'---------pre--------\')
- 修改文件权限为可执行
chmod 777 node_modules/.hooks/pred5
- 执行命令
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.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.stdout
和process.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
实例,并放弃对input
和output
流的控制
事件
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
监听命令行的keypress
、line
事件。
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源码解析的主要内容,如果未能解决你的问题,请参考以下文章