一塊兒來學習如何用 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 方法用於將相對路徑轉爲絕對路徑。它能夠接受多個參數,依次表示所要進入的路徑,直到將最後一個參數轉爲絕對路徑。若是根據參數沒法獲得絕對路徑,就以當前所在路徑做爲基準。除了根目錄,該方法的返回值都不帶尾部的斜槓。

參考

相關文章
相關標籤/搜索