npm cli

  工作中我们经常使用别人开发好的工具,来提高工作效率。但有时候,工作中某些重复的工作,并没有现成的工具可以提供给我们使用,这个时候就会幻想要是有一键生成的工具那该多好呀。

  但是作为一个开发者,问题不应该总是停留在幻想阶段,而是应该分析需求,解决问题,这才符合极客精神,才是开发装x的利器。既然没有,那就我来创作。

  废话不多说,下面开搞(感觉在开车)。

灵感,起源

  1.   在今年4-5月份,写了个npm库,为了将常用到的方法,汇总一下,并且拿来即用(地址)(目前又攒了一些具有通用性的方法了,等下一次更新一波)。当时就有个考虑,写第二个包,关于脚手架,命令的使用。

  2. 除了开始的预期想法,还有就是在7月份开发时,写了不少组件,为了团队其他人使用,需要对组件写readme,使用方式,参数配置等等。写markdown时发现组件较多,markdown格式需要一直复制粘贴,感觉也挺烦。

需求分析

  1. 分析components 文件夹下的组件,按顺序生成统一格式的markdown格式。
  2. 提供文件忽略功能。
  3. 提供指定文件夹分析功能。
  4. 提供生成文件的可命名配置。
  5. 提供生成文件的指定目录配置。
  6. 提供是否深度遍历。(可能组件都写在components下,不需要深度遍历)。
  7. 提供根据文件内容首行标识相同的文件生成markdown,(通过首行备注组件开发人,将同一人开发的组件快速生成在一个md文件内)。

另外:

  1. 因为之前对目录树有些想法,所以加了目录树生成。
  2. 开发中使用yargs,对子命令和命令模块开发有了想法,又增添了子命令和模块化开发(比较简单)。

开发

1. 了解package.json的bin字段。

  bin项用来指定各个内部命令对应的可执行文件的位置,下面代码指定,tool命令对应的可执行文件为 bin 子目录下的 index.js。Npm会寻找这个文件,在 node_modules/.bin/目录下建立符号链接

  而tool将是我们要使用的命令,在terminal输入对应的命令tool。则会执行index.js文件。其实类似node ./index.js,本质是执行js文件代码。

  但是通过bin的方式来执行文件,需要一些特殊处理。

1
2
3
4
5
// package.json

"bin": {
"tool": "./index.js"
}
1
2
3
4
// index.js

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

如上,即可实现最简单命令cli了,只需在terminal上执行命令tool则可以打印3333。

2. 理解命令参数

使用cli工具时,经常看见命令后面带有参数,例如eslint --fix src

cli的js文件执行,都是在node环境下运行的。而命令的参数,可以用node的全局对象process下的,process.argv

2.1 process.argv

  • 属性返回一个数组,包含当启动 Node.js 进程时传入的命令行参数。

  • 第一个元素是 process.execPath。

  • 第二个元素将是正在执行的 JavaScript 文件的路径。

  • 其余元素将是任何其他命令行参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// index.js
process.argv.forEach((val, index) => {
console.log(`${index}: ${val}`);
});

// 运行
$ node index.js one two=three four

输出如下:
0: /usr/local/bin/node
1: /Users/mjr/work/node/index.js
2: one
3: two=three
4: four

当我们能拿到参数时,其实就已经可以开发命令了。只需要根据参数的不同进行判断,取值和逻辑处理,基本上能实现多数的命令开发。但是这个不美观,和我们用的参数方式也不一样。

2.2 yargs 模块能够解决如何处理命令行参数

yargs能帮助我们在被执行的文件里所接受的参数进行包装处理。yargs.argv就是包装后的process.argv。

注意:

和process.argv很大的不同是它返回的是一个对象而不是数组,以连词线 -或–开头的参数会作为对象中的一个key值,而它后面的非连词线开头的参数会作为key的值。

基本使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

var argv = require('yargs').argv;

if (argv.a > 3 && argv.b < 53) {
console.log(1);
} else {
console.log(2);
}

terminal执行:

tool --a=4 --b=22

输出:
1

上面这种,在处理单命令,多参数比较简单的脚手架开发是足够用的。

使用help提示

1
2
3
4
5
6
7
// terminal 
tool --h 可以提示命令和参数说明

// index.js
const argv = require('yargs')
.help('h')
.argv

配置命令参数和撰写描述

通过option字段可以指定命令后面都可以使用哪些参数,参数可以使用的别名,以及默认值等等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const yargs = require('yargs');

let argv = yargs
.config(config.global) // 可配置项目
.option('ignore', {
alias: 'i', // 指定别名 --ignore 和 --i 其实是一样的。
describe: '要忽略的文件或目录名', // 参数的解释。
default: 'node_modules,.git' // 默认值。
})
.option('dir', {
alias: 'd',
describe: '需要处理的目标目录',
default: './'
})
.option('file', {
alias: 'f',
describe: '忽略的是文件还是目录,与i参数配合使用',
default: false
})
.help('h')
.argv

if (argv.ignore) { // do someting }
if (argv.dir) { // do someting }

关于yargs上的config

  • 上面的config是针对于命令的参数配置,可以理解为配置的默认值。
  • option 的默认值会被config的同名值覆盖。 但是在terminal 使用命令时,对参数赋值又可以覆盖config。tool --ignore=3 > config({ignore: 2}) > option(default: 1)

使用config处理复杂配置

使用eslint时我们经常会有个.eslintrc.js文件作为配置文件。同样,我们也可以写类似的配置文件,只需增加一个插件即可快速实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const findUp = require('find-up');
const yargs = require('yargs');

const configPath = findUp.sync(['.tree', '.tree.json']);
const config = configPath ? JSON.parse(fs.readFileSync(configPath)) : {};

let argv = yargs
.config(config)
.option('file', {
alias: 'f',
describe: '忽略的是文件还是目录,与i参数配合使用',
default: false
})
.help('h')
.argv

// 我们只需要新建个.tree

的隐藏文件,将配置写入即可。find-up会读取.tree的内容(该写法必须是标准的json)。然后根据上面提到的参数优先级,(tool --ignore=3 > config > option),则可以清楚的知道什么时候,哪个位置的参数时生效的。

3. 子命令和模块开发

yargs 不仅仅提供了参数的封装,还可以进行复杂的命令开发和模块化。

index.js

1
2
3
4
5
6
7
8
const command = require('./command/commandtree.js');
const analyze = require('./command/commandanalyze.js');
const yargs = require('yargs');
let argv = yargs
.command(command)
.command(analyze)
.help('h')
.argv

moduleA: commandtree.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 子命令A: tool analyze

module.exports = {
command: 'analyze',
aliases: ['analyze', 'a'],
desc: 'analyze file',
builder: (yargs) => {
return yargs
.option('dest', {
describe: '指定生成的目录',
default: './_doc'
})
.help('h')
},
handler: (argv) => {
console.log(argv, 'handler');
}
}

moduleB: commandanalyze.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 子命令B: tool gen

module.exports = {
command: 'gen', // 配置子命令的名称
aliases: ['gen', 'g'], // 指定子命令的别名
desc: 'generate file',
builder: (yargs) => { // 参数是yargs对象,可以继续使用yargs的相关api开发。
return yargs
.option('name', {
alias: 'n',
describe: '生成文件的名称',
default: 'tree'
})
.help('h')
},
handler: (argv) => { // 是传过来的参数,可以根据参数处理逻辑。
console.log(argv)
}
}
返回
顶部