一块儿来学习如何用 Node 来制做 CLI

CLI 是什么

提起 CLI,不禁得会想起 vue-cliangular-cli,它们都是基于 Node 的命令行工具。javascript

为何要开发一个 CLI

假设你如今要创建一个新项目 ,这个项目配置和以前的项目配置是同样的。在你没有 CLI 的时候,你只能经过复制、粘贴来进行。然而,当你有了 CLI,你就能够经过命令来完成这些步骤。固然,你能够说就新建一个项目,彻底不必再开发一个 CLI 工具。那若是你要新建 n 个项目呢?这个时候,有 CLI 和没有 CLI 的区别就体现出来了。html

怎么开发一个 CLI

准备

开发一个 CLI,须要用到如下工具:vue

开始

新建一个文件夹,名称起作 demo-cli,并在文件夹内 npm init。在 demo-cli 文件夹内,新建 bin 文件夹,并在该文件夹内新建 index.js 文件。紧接着,打开 demo-cli 文件夹内的 package.json 文件,在里面新增以下命令。java

{
    "bin": {
        "demo": "./bin/index.js"
    }
}
复制代码

这句代码的意思是指,在你使用 demo 命令的时候,会去执行 bin 文件夹下的 index.js 文件。node

这时候,咱们在 index.js 文件,写入如下代码。git

#!/usr/bin/env node

console.log('hello CLI');

复制代码

在 demo-cli 目录下依次运行 npm linkdemo,这个时候,你会发现控制台输出了 hello CLIgithub

备注:vue-cli

  • #!/usr/bin/env node 告诉操做系统用 Node 来运行此文件
  • npm link 做用主要是,在开发 npm 模块的时候,咱们会但愿边开发边调试。这个时候,npm link 就派上用场了。

逐步深刻

  1. index.js 文件内,写入如下代码。
#!/usr/bin/env node

const program = require('commander');

program
    .version('1.0.0', '-v, --version')
    .command('init <dir>', 'generate a new project')
    .parse(process.argv);
复制代码

commander 提供了一种使用 node.js 来开发命令行的可能性。咱们能够经过 commanderoption 方法,来定义 commander 的选项,固然,这些定义的选项也会被做为该命令的帮助文档。shell

  • version:用来定义版本号。commander 默认帮咱们添加 -V, --version 选项。固然,咱们也能够重设它。
  • command<> 表明必填,[] 表明选填。当 .command() 带有描述参数时,不能采用 .action(callback) 来处理子命令,不然会出错。这告诉 commander,你将采用单独的可执行文件做为子命令。
  • parse:解析 process.argv,解析完成后的数据会存放到 new Command().args 数组中。process.argv 里面存储内容以下:

因此,咱们能够经过 program.args[0] 来取出 dir 的值。

问题:为何当 command 没有描述参数,且 parse 方法使用链式调用会报错?(猜测:commanddesc 参数时,返回的是 this,当没有 desc 参数时,返回的是新对象,根据 API Document 得出)npm

```js
// 正确
program
    .version('1.0.0', '-v, --version')
    .command('init <dir>', 'generate a new project')
    .action(function(dir, cmd){
        console.log(dir, cmd)
    })
    .parse(process.argv);

// 正确
program
    .version('1.0.0', '-v, --version')
    .command('init <dir>', 'generate a new project')
    .action(function(dir, cmd){
        console.log(dir, cmd)
    })
program.parse(process.argv);

// 正确
program
    .version('1.0.0', '-v, --version')
    .command('init <dir>')
    .action(function(dir, cmd){
        console.log(dir, cmd)
    })
program.parse(process.argv);

// 错误
program
    .version('1.0.0', '-v, --version')
    .command('init <dir>')
    .action(function(dir, cmd){
        console.log(dir, cmd)
    })
    .parse(process.argv);
```
复制代码
  1. bin 文件下建立 demo-init.js 文件,部分代码以下:
#!/usr/bin/env node

const shell = require('shelljs');
const program = require('commander');
const inquirer = require('inquirer');
const download = require('download-git-repo');
const ora = require('ora');
const fs = require('fs');
const path = require('path');
const spinner = ora();

program.parse(process.argv);

let dir = program.args[0];

const questions = [{
    type: 'input',
    name: 'name',
    message: '请输入项目名称',
    default: 'demo-static',
    validate: (name)=>{
        if(/^[a-z]+/.test(name)){
            return true;
        }else{
            return '项目名称必须以小写字母开头';
        }
    }
}]

inquirer.prompt(questions).then((answers)=>{
    // 初始化模板文件
    downloadTemplate(answers);
})

function downloadTemplate(params){
    spinner.start('loading');
    let isHasDir = fs.existsSync(path.resolve(dir));
    if(isHasDir){
        spinner.fail('当前目录已存在!');
        return false;
    }
    // 开始下载模板文件
    download('gitlab:git.gitlab.com/demo-static', dir, {clone: true}, function(err){
        if(err){
            spinner.fail(err);
        };
        updateTemplateFile(params);
    })
}

function updateTemplateFile(params){
    let { name, description } = params;
    fs.readFile(`${path.resolve(dir)}/public/package.json`, (err, buffer)=>{
        if(err) {
            console.log(chalk.red(err));
            return false;
        }
        shell.rm('-f', `${path.resolve(dir)}/.git`);
        shell.rm('-f', `${path.resolve(dir)}/public/CHANGELOG.md`);
        let packageJson = JSON.parse(buffer);
        Object.assign(packageJson, params);
        fs.writeFileSync(`${path.resolve(dir)}/public/package.json`, JSON.stringify(packageJson, null, 2));
        fs.writeFileSync(`${path.resolve(dir)}/README.md`, `# ${name}\n> ${description}`);
        spinner.succeed('建立完毕');
    });
}

复制代码
  • inquirer 主要提供交互式命令的功能。validate 返回 true 表明输入值验证合法,若是返回任意字符串,则会替代默认的错误消息返回。
  • 经过 Nodefs 模块来判断文件夹是否已存在。

    path.resolve 方法用于将相对路径转为绝对路径。它能够接受多个参数,依次表示所要进入的路径,直到将最后一个参数转为绝对路径。若是根据参数没法获得绝对路径,就以当前所在路径做为基准。除了根目录,该方法的返回值都不带尾部的斜杠。

参考

相关文章
相关标签/搜索