## 手把手教你开发第一个命令行程序
[TOC]
### 需求:
仿照`npm`命令,实现我们自己的命令行程序**`mynpm`**,包括`mynpm -V ,mynpm init ,mynpm install`这几个命令
### 简单需求分析:
* `mynpm -V `
打印版本
* `mynpm init `
1.仿照npm init提示用户输入各种信息
2.所有用户输入信息保存到mypackage.json的文件中
* `mynpm install `
调用`npm install`命令,根据`-save` ,`--save-dev `, `-g`这几个参数的不同使用不同的处理方法
* `--salve`:模块安装成功之后,把模块名称写入`mypackage.json`文件的`dependencies`字段中
* `--save-dev`:模块安装成功之后,把模块名称写入`mypackage.json`文件的`devDependencies`字段中
* `-g`:全局安装模块
### 需求实现
#### 创建工程
1. `mkdir mynpm`
1. `cd mynpm`
1. `npm init`
1. `创建入口文件index.js`
#### 第三方模块安装
实现上面的需求,需要用到的模块:
* 路径处理
* 文件系统处理
* 进程
* shelljs
* 命令行参数处理
* 命令行交互模块
使用`npm init`命令时,需要用户确认输入信息,这时,需要格式化输出`JSON`到命令行,可以使用第三方模块`jsonFormat`
除了路径处理(`path`)模块,其他模块均为第三方模块,需要先安装才能引用
`npm install --save json-format co co-prompt commander fs-extra shelljs `
#### 代码实现
* 模块引用
~~~
#!/usr/bin/env node
//模块引用
var path = require('path');
var co = require('co');
var prompt = require('co-prompt');
var program = require('commander');
var fs = require('fs-extra');
var shell = require('shelljs');
//格式化输出JSON对象
var jsonFormat = require('json-format');
~~~
* 子命令和参数定义
~~~
//定义版本号,由本模块package.json的version决定,使用-V或者--version打印
program
.version(require('./package.json').version)
.usage('[command] [options]')
.option('-d, --description', 'Description of the module.'); //参数定义
program
.command('init')
.description('general package.json')
.option('-y,--yes', 'general package.json without prompt')
.action(init);//init命令触发时,调用init()方法
program
.command('install [optional...]')
.description('install modules')
.option('-S,--save', 'save modules as dependencies')
.option('-D,--save-dev', 'save modules as dependencies')
.option('-g,--global', 'save modules as global')
.action(install);//install命令触发时,调用install()方法
program.parse(process.argv);
~~~
* 实现 init()方法
~~~
/**
* 处理init命令
* @param {Object} pOptions - init命令的参数
*/
function init(pOptions) {
co(function*() {
var mypackage = {};
var isPromptMode = pOptions.yes ? false : true;
//默认名称为当前文件夹名称的小写
var _defaultName = path.basename(process.cwd()).toLowerCase();
//默认入口文件为index.js
var _defaultEntryPoint = 'index.js'
// var jsArr = shell.ls('*.js');没有js文件时,有额外输出
var jsArr = shell.ls();
//当前工作目录下,如果存在js文件,将找到的第一个js文件作为默认值
for (var i = 0; i < jsArr.length; i++) {
if (path.extname(jsArr[i]) === '.js') {
_defaultEntryPoint = jsArr[i];
break;
}
};
var _testCommand = 'echo \"Error: no test specified\" && exit 1';
//拼接将生成的package.json文件的绝对路径
var mypackagePath = path.resolve(process.cwd(), 'package.json');
var _inputName = '',
_inputVersion = '',
_inputDesc = '',
_inputEntryPoint = '',
_inputTestCommand = '',
_gitRepository = '',
_keywords = '',
_author = '',
_license = '',
_confirm = '';
if (isPromptMode) {
console.log('This utility will walk you through creating a package.json file.It only covers the most common items, and tries to guess sensible defaults.\n\n' + 'Press ^C at any time to quit.');
_inputName = yield prompt('name: (' + _defaultName + ') ');
_inputVersion = yield prompt('version: (1.0.0) ');
_inputDesc = yield prompt.multiline('description: ');
_inputEntryPoint = yield prompt('entry point: (' + _defaultEntryPoint + ') ');
_inputTestCommand = yield prompt('test command: ');
_gitRepository = yield prompt('git repository: ');
_keywords = yield prompt('keywords: ');
_author = yield prompt('author: ');
_license = yield prompt('license: (ISC) ');
}
//处理将生成的package.json信息
mypackage.name = _inputName || _defaultName;
mypackage.version = _inputVersion || '1.0.0';
mypackage.description = _inputDesc || '';
mypackage.main = _inputEntryPoint || _defaultEntryPoint;
mypackage.scripts = {};
mypackage.scripts.test = _inputTestCommand || _testCommand;
if (_gitRepository) {
mypackage.repository = {};
mypackage.repository.type = 'git';
mypackage.repository.url = _gitRepository;
}
mypackage.keywords = _keywords.split(' ') || [];
mypackage.author = _author || '';
mypackage.license = _license || 'ISC';
if (isPromptMode) {
console.log('About to write to ' + mypackagePath + '\n');
console.log(jsonFormat(mypackage) + '\n');
_confirm = yield prompt.confirm('Is this ok?(y|n)');
if (!_confirm) {
console.log('Aborted');
process.exit();
}
} else {
console.log('Wrote to ' + mypackagePath + '\n');
console.log(jsonFormat(mypackage) + '\n');
}
//将信息写入package.json文件
fs.outputJsonSync(mypackagePath, mypackage);
/*确保入口文件存在——优化npm init方法,懒得每次都创建入口文件(比如index.js)*/
fs.ensureFileSync(mypackage.main);
process.exit();
});
}
~~~
* 实现install()方法
~~~
/**
* 处理install命令
* @param {Array} pOptional - 安装的模块名称
* @param {Object} pOptions - install命令后面带的参数
*/
function install(pOptional, pOptions) {
var _command = ['npm', 'install'];
_command = _command.concat(pOptional);
if (pOptions.global)
_command.push('-g')
else if (pOptions.save) {
ensurePackageJsonExist();
_command.push('--save')
} else if (pOptions.saveDev) {
ensurePackageJsonExist();
_command.push('--save-dev')
}
// console.log(_command.join(' '))
shell.exec(_command.join(' '), { async: true });
}
/**
* package.json不存在,显示帮助信息
*/
function ensurePackageJsonExist() {
if (!fs.existsSync(path.join(process.cwd(), 'package.json'))) {
console.log('在当前文件夹下找不到\"package.json\“文件,建议先init')
program.help();
}
}
~~~
### 使用命令行程序
#### 使用前的配置
为了能全局使用命令行程序,必须先在package.json文件中加一个字段
~~~
"bin": {
"mynpm": "./index.js"
},
~~~
这里,`mynpm`是我定义的名称(读者可自行定义),后面将使用`mynpm`来讲解命令的调用
>Tips:
要保证入口文件(`index.js`)第一行已经加上`#!/usr/bin/env node`,配置才算成功
#### 发布后再安装使用
* 发布模块
1. npm login
1. npm publish
* 使用已发布的模块**(假设发布模块的名称为`mynpm-cli`)**
1. 安装:
`npm install -g mynpm-cli`
1. 现在你可以使用mynpm命令代替npm命令来执行init和install了
#### 本地使用不发布
1. cd到模块工作目录下
1. 执行`npm link`或者`npm install -g`
1. 现在你可以使用mynpm命令代替npm命令来执行init和install了
### 完整demo源码下载地址
**mynpm-cli demo 源码下载地址:https://github.com/outsiderLazy/mynpm-cli <br/>**
**mynpm-cli模块npm地址:https://www.npmjs.com/package/mynpm-cli <br/>**
>Tips:
1.demo模块原本的名称是`mynpm`,但该名称已经被占用,故模块改名为`mynpm-cli`。
为了命令的简洁,`bin`的字段没改,模块安装成功之后,你可以直接使用`mynpm init`和`mynpm install`命令
2.如果你下载了源码,想要本地使用,需要先在工作目录下运行`npm install`安装依赖库