## 手把手教你开发第一个命令行程序 [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`安装依赖库