文章

制作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,并使用

  1. 注册npm账号,去官网注册或是使用npm adduser命令行注册

  2. 登录,首次需要,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/.
    
  3. 开始发布 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

  4. 创建案例,并应用刚上传的包

    在自己的项目中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提交信息的规范和提交代码的格式化处理,其中区别教程,还需要考虑的功能如下:

  1. 帮助信息打印,用于打印支持的命令,选项参数,比如:rambo --help
  2. 版本信息,告知使用者当前的CLI版本,比如:rambo --version
  3. 交互面板,提供使用者的可选项操作
  4. 信息打印,提供各种语义颜色打印
  5. 配置文件管理,用于写入本地进行的一个配置项管理
  6. http请求
  7. git hooks实现commit的hook

处理以上功能就需要额外的依赖库,如下:

commander:完整的 node.js 命令行解决方案。实现1、2

inquirer:常见的UI交互式命令行集合。实现3

colorschalk - 给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;

参考文章

一些常用的npm包 常用的内置模块

svg生成 图片

git钩子,在.git/hooks文件夹中

使用NPM发布和使用CLI工具

本文由作者按照 CC BY 4.0 进行授权