制作npm的命令行工具cli
npm:nodejs packect manage,允许用户从NPM服务器下载别人编写的第三方包到本地使用、下载并安装别人编写的命令行程序(CLI)到本地使用、将自己编写的包或命令行程序上传到NPM服务器供别人使用。
cli:command line interface,命令行界面,是指可在用户提示符下键入可执行指令的界面,
一直知道npm install一些工具之后,自己就能在终端使用,但是一直不了解原理,后来才知道这是npm包的发布和cli工具的结合
Node包管理器
node包管理和iOS的pod很像,主要实现的目的就是解决多人协作中的组件库/模块复用的问题,当然node包管理还能和cli结合使用的好处。
一般公司都有自己的NPM管理平台(不做过多介绍),官方的管理平台是npmjs,这里有成千上万的开源的模块,你只需要一行命令,就能使用别人写好的模块。
创建第一个node模块
使用npm init命令即可创建,如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
nodejs|⇒ mkdir npm-module
nodejs|⇒ cd npm-module
npm-module|⇒ npm init
This utility will walk you through creating a package.json file.
It only covers the most common items, and tries to guess sensible defaults.
See `npm help json` for definitive documentation on these fields
and exactly what they do.
Use `npm install <pkg>` afterwards to install a package and
save it as a dependency in the package.json file.
Press ^C at any time to quit.
package name: (npm-module)
version: (1.0.0)
description: 制作npm的命令行工具cli
entry point: (index.js)
test command:
git repository:
keywords: npm cli
author: rambo
license: (ISC)
About to write to /Users/rambo/Documents/MyProject/nodejs/npm-module/package.json:
{
"name": "npm-module",
"version": "1.0.0",
"description": "制作npm的命令行工具cli",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [
"npm",
"cli"
],
"author": "rambo",
"license": "ISC"
}
Is this OK? (yes) yes
npm-module|⇒ ll
total 8
-rw-r--r-- 1 rambo staff 283 4 12 15:59 package.json
npm-module|⇒ cat package.json
{
"name": "npm-module",
"version": "1.0.0",
"description": "制作npm的命令行工具cli",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [
"npm",
"cli"
],
"author": "rambo",
"license": "ISC"
}
npm-module|⇒vim index.js
exports.showMsg = function () {
console.log("这是我的第一个node模块");
};
npm-module|⇒ ll
total 16
-rw-r--r-- 1 rambo staff 84 4 12 16:10 index.js
-rw-r--r-- 1 rambo staff 283 4 12 15:59 package.json
npm-module|⇒
到此,我们就编写完了一个npm包。
发布包到npmjs,并使用
注册npm账号,去官网注册或是使用npm adduser命令行注册
登录,首次需要,npm login会将证书保存到本地,后面就不需要每次都登录了
1 2 3 4 5
npm-module|⇒ npm login Username: ramboq Password: Email: (this IS public) qiujunyun@163.com Logged in as ramboq on http://npm.mail.netease.com/registry/.
开始发布 npm publish
不想发布的内容模块,可以通过.gitignore或是.npmignore文件进行忽略
使用 cnpm 的注意报错: no_perms Private mode enable, only admin can publish this module 设置回原本的就可以了 npm config set registry registry.npmjs.org 发布完成之后,如果还想回到之前的cnpm,使用下面的命令 npm config set registry registry.npm.taobao.org
创建案例,并应用刚上传的包
在自己的项目中npm install npm-module即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
npm-module|⇒ npm publish npm notice npm notice 📦 npm-module@1.0.0 npm notice === Tarball Contents === npm notice 283B package.json npm notice 84B index.js npm notice === Tarball Details === npm notice name: npm-module npm notice version: 1.0.0 npm notice package size: 1.1 kB npm notice unpacked size: 2.8 kB npm notice shasum: 5667da556208b npm notice integrity: UE9w== npm notice total files: 2 npm notice + npm-module@1.0.0
添加CLI能力
回到上面的例子,我们继续编辑package.json
文件,要想实现cli能力,关键的是配置bin
属性,该属性对应的是可执行文件的路径。例如将 bin
对应的可执行文件路径配置为当前项目下的 ./cli.js
。
1
2
3
4
"bin": {
// rambo 是一个可执行的命令,该命令指向了 ./cli.js 脚本
"rambo": "./cli.js"
}
1
2
3
4
5
npm-module|⇒ ll
total 16
-rw-r--r-- 1 rambo staff 0 4 12 16:29 cli.js
-rw-r--r-- 1 rambo staff 84 4 12 16:10 index.js
-rw-r--r-- 1 rambo staff 323 4 12 16:29 package.json
我们简单编辑下cli.js
,让他调用index.js
1
2
3
4
npm-module|⇒ vim cli.js
#!/usr/bin/env node
var test = require('./index');
test.showMsg();
在 env 中包含了一些环境变量,包括我们安装的一些软件执行路径等,因此可以使用 env 来找到不同操作系统上的 Node 执行路径,从而让文件可被正常的解释和执行。
软链接进行测试
使用npm link
命令将他连接到全局执行环境,这样在系统的任意路径下面都能使用该cli工具。
1
2
3
4
5
6
7
8
npm-module|⇒ sudo npm link
Password:
npm notice created a lockfile as package-lock.json. You should commit this file.
npm WARN npm-module@1.0.0 No repository field.
up to date in 0.099s
/usr/local/bin/rambo -> /usr/local/lib/node_modules/npm-module/cli.js
/usr/local/lib/node_modules/npm-module -> /Users/rambo/Documents/MyProject/nodejs/npm-module
当执行 npm link
后,可以看到在 Mac 下该命令主要做了两件事:
- 为可执行文件
./cli.js
创建一个软链接,将其链接到/usr/local/bin/<package>
- 为当前项目创建一个软链接,将其链接到
/usr/local/lib/node_modules/<package>
因此在全局环境执行 bin
配置的命令时,会启用 Node 去执行对应的可执行文件。
温馨提示:如果
bin
不配置执行的命令名称,默认将使用pageage.json
中的<name>
字段作为命令。
此时在任意位置执行 rambo
命令:
1
2
npm-module|⇒ rambo
这是我的第一个node模块
更新npm包
修改版本号之后,重新publish
1
2
3
npm-module|⇒ npm version 1.0.1
v1.0.1
npm-module|⇒ npm publish
安装使用cli
通过 npm install
命令对工具进行全局安装:
1
2
3
4
5
npm-module|⇒ npm install npm-module -g
/usr/local/bin/npm-module -> /usr/local/lib/node_modules/npm-module/cli.js
+ npm-module@1.0.0
updated 1 package in 12.977s
复制代码
由安装打印信息可以发现,最终 CLI 工具脚本链接指向了 /usr/local/bin/npm-module
。安装成功之后,可以在项目中使用 CLI 指定的命令了。
全局的npm包会安装在/usr/local/lib/node_modules/目录下面
高阶使用
上面讲解的只是一个简单的教程示例,下面会讲解一个示例,算是简约版本的ESLint ESLint-github,用于检查commit
提交信息的规范和提交代码的格式化处理,其中区别教程,还需要考虑的功能如下:
- 帮助信息打印,用于打印支持的命令,选项参数,比如:
rambo --help
- 版本信息,告知使用者当前的CLI版本,比如:
rambo --version
- 交互面板,提供使用者的可选项操作
- 信息打印,提供各种语义颜色打印
- 配置文件管理,用于写入本地进行的一个配置项管理
- http请求
- git hooks实现commit的hook
处理以上功能就需要额外的依赖库,如下:
commander:完整的 node.js 命令行解决方案。实现1、2
inquirer:常见的UI交互式命令行集合。实现3
colors、chalk - 给node.js的控制台输出添加颜色和样式。实现4
内置模块fs:文件操作系统,读取文件是否存在,或是新建文件夹
child_process:子线程,调用exec使用子进程执行命令。
nconf:配置管理工具。可以用json的形式将配置信息写在单独的配置文件中进行读取操作。使用介绍。实现5
request:轻量的http请求。实现6。
部分代码演示
cli.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/usr/bin/env node
const program = require('commander');
const TestMain = require('./index');
const testMain = new TestMain();
program
.version('1.0.0', '-v, --version')
.option('-m, --message <msg>', '输入提交信息')
.option('-t, --test', '这是一条测试命令')
.parse(process.argv);
const options = program.opts();
if (options.test) {
testMain.run();
} else {
console.log('test');
}
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
#!/usr/bin/env node
const { exec } = require('child_process');
require('colors');
const fs = require('fs');
const nconf = require('nconf');
const inquirer = require('inquirer');
const request = require('request');
const cwd = process.cwd();
const home = process.env.HOME;
const testHome = `${cwd}/.test`;
const localConfig = `${cwd}/config.json`
class TestMain {
constructor() {
}
createDirIfneed() {
if (fs.existsSync(testHome)) {
return;
}
fs.mkdirSync(testHome, null);
}
createLocalConfigIfneed() {
/// https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Using_promises
return new Promise((resolve) => {
if (!fs.existsSync(testHome)) {
fs.mkdirSync(testHome, null);
}
nconf.argv().env().file({ file: localConfig });
if (fs.existsSync(localConfig)) {
resolve();
return;
}
const questions = [
{
type: 'rawlist',
name: 'where',
message: '请选择你的祖国',
choices: [
'中国',
'美国',
'日本'
]
}
];
inquirer.prompt(questions).then((answers) => {
nconf.set('where', answers.where);
nconf.save();
resolve();
});
});
}
async run() {
this.createLocalConfigIfneed();
await this.createLocalConfigIfneed();
}
}
module.exports = TestMain;
参考文章
git钩子,在.git/hooks文件夹中