## 手把手教你開發第一個命令行程序
[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`安裝依賴庫