node.js 命令行工具(cli)

10,115 阅读4分钟

一. 先了解一下package.json

每个项目的根目录都有一个package.json文件,定义了这个项目所需要的各种模块,以及项目的配置信息,下面是一个比较完整的package.json文件

{
  "name": "vue-cli",
  "version": "2.9.3",
  "description": "A simple CLI for scaffolding Vue.js projects.",
  "preferGlobal": true,
  "bin": {
    "vue": "bin/vue",
    "vue-init": "bin/vue-init",
    "vue-list": "bin/vue-list"
  },
  "repository": {
    "type": "",
    "url": ""
  },
  "keywords": [
  ],
  "author": "litongqian",
  "license": "MIT",
  "bugs": {
    "url": ""
  },
  "homepage": "",
  "scripts": {
    "test": "npm run lint && npm run e2e",
    "start": "node index.js"
  },
  "dependencies": {
    "async": "^2.4.0",
    "chalk": "^2.1.0",
  },
  "devDependencies": {
    "chai": "^4.1.2",
    "eslint": "^3.19.0",
  },
  "engines": {
    "node": ">=6.0.0"
  }
}
1. 其中scripts字段

指定了运行脚本命令的npm命令行缩写,比如start指定了运行npm run start时,所要执行的命令。

2. bin字段

bin项用来指定各个内部命令对应的可执行文件的位置

"bin": {
    "vue": "bin/vue",
    "vue-init": "bin/vue-init",
    "vue-list": "bin/vue-list"
  },

上面代码指定,vue 命令对应的可执行文件为 bin 子目录下的vue。

3. npm link

项目目录:


自己开发模块的时候,比如上面的mymodule模块,如果想在hello中使用,我们可以采用相对路径require('./mymodule')加载模块。

也可以使用模块标识require('mymodule'),Node加载自定义模块需要安装到node_modules对于上述模块,解决方法就是将模块映射到全局的node_modules目录之中,生成一个符号链接,指向模块的本地目录。


npm link就能起到这个作用,会自动建立这个符号链接。

  • 请设想这样一个场景,你开发了一个模块mymodule,目录为hello/mymodule,你自己的项目要用到这个模块,首先,在模块目录(hello/mymodule)下运行npm link命令。
hello/mymodule$ npm link


上面的命令会在NPM的全局模块目录内,生成一个符号链接文件,该文件的名字就是package.json文件中指定的模块名。

/usr/local/lib/node_modules/mymodule -> /Users/tongqianli/Desktop/work/hello/mymodule

这个时候,已经可以使用全局模块目录下/usr/local/lib/node_modules/mymodule就有了mymodule模块了,但是要想在项目里面引用还不行

这时可以通过添加path解决:

操作系统中都会有一个PATH环境变量,系统调用一个命令的时候,就会在PATH变量中注册的路径中寻找,如果注册的路径中有就调用,否则就提示命令没找到。

-> export PATH=$PATH: # 将 /usr/bin 追加到 PATH 变量中
-> export NODE_PATH="/usr/lib/node_modules;/usr/local/lib/node_modules" #指定 NODE_PATH 变量

NODE_PATH 就是NODE中用来寻找模块所提供的路径注册环境变量。我们可以使用上面的方法指定NODE_PATH环境变量。

更好的办法:

切换到项目目录,再次运行npm link命令,并指定模块名。

hello tongqianli$ npm link mymodule

上面命令生成了本地node_modules/mymodule模块映射到全局的node_modules/mymodule ,全局,全局的映射到本地hello/mymodule。

/Users/tongqianli/Desktop/work/hello/node_modules/mymodule 
-> /usr/local/lib/node_modules/mymodule 
-> /Users/tongqianli/Desktop/work/hello/mymodule

然后,就可以在你的项目中,加载该模块了。

var myModule = require('myModule');

这样一来,mymodule的任何变化,都可以直接反映在项目之中。

如果你的项目不再需要该模块,可以在项目目录内使用npm unlink命令,删除符号链接。

hello tongqianli$ npm unlink mymodule

二. 可执行脚本

写一个简单的脚本hello

$ mkdirhello#创建一个文件夹 
$ cd hello && touch hello #创建命令文件

#!/usr/bin/env node
console.log('hello world');

1 .脚本的第一行通常是指定解释器,即这个脚本必须通过什么解释器执行。这一行以#!字符开头,这个字符称为 Shebang,所以这一行就叫做 Shebang 行。

#!后面就是脚本解释器的位置,Bash 脚本的解释器一般是/bin/sh/bin/bash

node脚本的解释器是/usr/local/bin/node

上面命令使用env命令(这个命令总是在/usr/bin目录),返回Node 可执行文件的位置

#!/usr/bin/env NAME这个语法的意思是,让 Shell 查找$PATH环境变量里面第一个匹配的NAME。如果你不知道某个命令的具体路径,或者希望兼容其他用户的机器,这样的写法就很有用。

#!/usr/bin/env node这里表示使用node作为脚本的解释程序,可以省略node ./hello,直接运行./hello, node的路径通过env来查找,可以避免node安装的路径不一样带来找不到的问题。

打开/usr/bin/env,可以查看到PATH,操作系统通过路径找到node



执行权限和路径

前面说过,只要指定了 Shebang 行的脚本,可以直接执行。这有一个前提条件,就是脚本需要有执行权限。可以使用下面的命令,赋予脚本执行权限。

# 给所有用户执行权限
$ chmod +x hello

# 给所有用户读权限和执行权限
$ chmod +rx hello
# 或者
$ chmod 755 hello

# 只给脚本拥有者读权限和执行权限
$ chmod u+rx hello

脚本的权限通常设为755(拥有者有所有权限,其他人有读和执行权限)或者700(只有拥有者可以执行)。


然后,修改 hello 的权限。

$ chmod 755 hello
$./hello


如果想把 hello 前面的路径去除,可以将 hello 的路径加入环境变量 PATH。但是,另一种更好的做法,是在当前目录下新建 package.json ,写入下面的内容。

{
  "name": "hello",
  "bin": {
    "hello": "./hello"
  }
}

然后执行 npm link 命令。不明白的看上面

$ npm link



执行后会产生一个全局的映射关系,可执行脚本bin/hello映射到全局node_modules/hello/hello,全局的node_modules/hello映射到本地hello,这样就可以使用hello了

三.命令行参数

命令行参数可以用系统变量 process.argv 获取。

修改hello脚本

#!/usr/bin/env node
console.log('hello ', process.argv);

其中process为node进程中的全局变量,process.argv为一数组,数组内存储着命令行的各个部分,argv[0]为node的安装路径,argv[1]为主模块文件路劲,剩下为子命令或参数,如下:


$ hello a b c
# process.argv的值为[ '/usr/local/bin/node', '/usr/local/bin/hello', 'a', 'b', 'c' ]


脚本可以通过 child_process 模块新建子进程,从而执行 Unix 系统命令,修改hello

exec方法用于执行bash命令,exec方法最多可以接受两个参数,第一个参数是所要执行的shell命令,第二个参数是回调函数,该函数接受三个参数,分别是发生的错误、标准输出的显示结果、标准错误的显示结果。


#!/usr/bin/env node
var name = process.argv[2];
var exec = require('child_process').exec;

var child = exec('echo hello ' + name, function(err, stdout, stderr) {
  if (err) throw err;
  console.log(stdout);
});

执行$ hello litongqian

如果我们想查看所有文件,修改hello

var name = process.argv[2];
var exec = require('child_process').exec;

var child = exec(name, function(err, stdout, stderr) {
  if (err) throw err;
  console.log(stdout);
});

执行$ hello ls 

hello目录下有三个文件


四、shelljs 模块

shell.js 模块重新包装了 child_process,调用系统命令更加方便。它需要安装后使用。

npm install --save shelljs

然后,改写脚本。

#!/usr/bin/env node
var name = process.argv[2];
var shell = require("shelljs");

shell.exec("echo hello " + name);

五、yargs 模块

shelljs 只解决了如何调用 shell 命令,而 yargs 模块能够解决如何处理命令行参数。它也需要安装。

$ npm install --save yargs

yargs 模块提供 argv 对象,用来读取命令行参数。请看改写后的 hello 。

#!/usr/bin/env node
var argv = require('yargs').argv;

console.log('hello ', argv.name);

使用时,下面两种用法都可以。

$ hello --name=tom
hello tom

$ hello --name tom
hello tom

也就是说,process.argv 的原始返回值如下。

$ node hello --name=tom
[ 'node',
  '/usr/local/bin/hell',
  '--name=tom' ]

yargs 可以上面的结果改为一个对象,每个参数项就是一个键值对。

六.发布命令包

通过npm publish进行发布,前提是有npm帐号。如何操作可以查看npm 官方文档

本文是通过原生node.js来开发命令工具,而vue-cli是采用commander.js来简化命令工具开发,

了解了执行流程,学习对应的模块,就可以了!,本文抛个砖头


最后:有时我们用到的命令行不是全局安装的,而是本地安装的

1. package.json bin字段

bin项用来指定各个内部命令对应的可执行文件的位置。

"name":"someTool",
"bin": {
  "someTool": "./bin/someTool.js"
}

上面代码指定,someTool 命令对应的可执行文件为 bin 子目录下的 someTool.js。

当一个项目依赖上面的someTool工具时,同时只是本地安装

{
  "name": "myproject",
  "devDependencies": {
    "someTool": "latest"
  },
  "scripts": {
    start: 'someTool build'  //等同于start: './node_modules/someTool/someTool.js build'

  }
}

npm会寻找这个文件,在node_modules/.bin/目录下建立符号链接。在上面的例子中,someTool.js会建立符号链接npm_modules/.bin/someTool。由于node_modules/.bin/目录会在运行时加入系统的PATH变量,因此在运行npm时,就可以不带路径,直接通过命令来调用这些脚本。

因此,像上面这样的写法可以采用简写。

scripts: {  
  start: './node_modules/someTool/someTool.js build'
}

// 简写为

scripts: {  
  start: 'someTool build'
}

所有node_modules/.bin/目录下的命令,都可以用npm run [命令]的格式运行。在命令行下,键入npm run,然后按tab键,就会显示所有可以使用的命令。

1. npm run

上面代码中,scripts字段指定了两项命令

start
,输入npm run-script start或者npm run start,就会执行 someTool buildnpm runnpm run-script的缩写,一般都使用前者,但是后者可以更好地反应这个命令的本质。

npm run命令会自动在环境变量$PATH添加node_modules/.bin目录,所以scripts字段里面调用命令时不用加上路径,这就避免了全局安装NPM模块。

npm run如果不加任何参数,直接运行,会列出package.json里面所有可以执行的脚本命令。

npm内置了两个命令简写,npm test等同于执行npm run testnpm start等同于执行npm run start

npm run会创建一个Shell,执行指定的命令,并临时将node_modules/.bin加入PATH变量,这意味着本地模块可以直接运行。

举例来说,你执行ESLint的安装命令。

$ npm i eslint --save-dev

运行上面的命令以后,会产生两个结果。首先,ESLint被安装到当前目录的node_modules子目录;其次,node_modules/.bin目录会生成一个符号链接node_modules/.bin/eslint,指向ESLint模块的可执行脚本。

然后,你就可以在package.jsonscript属性里面,不带路径的引用eslint这个脚本。

{
  "name": "Test Project",
  "devDependencies": {
    "eslint": "^1.10.3"
  },
  "scripts": {
    "lint": "eslint ."
  }
}

等到运行npm run lint的时候,它会自动执行./node_modules/.bin/eslint .

如果直接运行npm run不给出任何参数,就会列出scripts属性下所有命令。

$ npm run
Available scripts in the user-service package:
  lint
     jshint **.js
  test
    mocha test/

到此,结束!