一起來學習如何用 Node 來製作 CLI

時間小魚發表於2019-03-01

CLI 是什麼

提起 CLI,不由得會想起 vue-cliangular-cli,它們都是基於 Node 的命令列工具。

為什麼要開發一個 CLI

假設你現在要建立一個新專案 ,這個專案配置和之前的專案配置是一樣的。在你沒有 CLI 的時候,你只能通過複製、貼上來進行。然而,當你有了 CLI,你就可以通過命令來完成這些步驟。當然,你可以說就新建一個專案,完全沒必要再開發一個 CLI 工具。那如果你要新建 n 個專案呢?這個時候,有 CLI 和沒有 CLI 的區別就體現出來了。

怎麼開發一個 CLI

準備

開發一個 CLI,需要用到以下工具:

開始

新建一個資料夾,名稱起做 demo-cli,並在資料夾內 npm init。在 demo-cli 資料夾內,新建 bin 資料夾,並在該資料夾內新建 index.js 檔案。緊接著,開啟 demo-cli 資料夾內的 package.json 檔案,在裡面新增如下命令。

{
    "bin": {
        "demo": "./bin/index.js"
    }
}
複製程式碼

這句程式碼的意思是指,在你使用 demo 命令的時候,會去執行 bin 資料夾下的 index.js 檔案。

這時候,我們在 index.js 檔案,寫入以下程式碼。


#!/usr/bin/env node

console.log(`hello CLI`);

複製程式碼

demo-cli 目錄下依次執行 npm linkdemo,這個時候,你會發現控制檯輸出了 hello CLI

一起來學習如何用 Node 來製作 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 的選項,當然,這些定義的選項也會被作為該命令的幫助文件。

  • version:用來定義版本號。commander 預設幫我們新增 -V, --version 選項。當然,我們也可以重設它。
  • command<> 代表必填,[] 代表選填。當 .command() 帶有描述引數時,不能採用 .action(callback) 來處理子命令,否則會出錯。這告訴 commander,你將採用單獨的可執行檔案作為子命令。
  • parse:解析 process.argv,解析完成後的資料會存放到 new Command().args 陣列中。process.argv 裡面儲存內容如下:
一起來學習如何用 Node 來製作 CLI

所以,我們可以通過 program.args[0] 來取出 dir 的值。

問題:為什麼當 command 沒有描述引數,且 parse 方法使用鏈式呼叫會報錯?(猜想:commanddesc 引數時,返回的是 this,當沒有 desc 引數時,返回的是新物件,根據 API Document 得出)

```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}
> ${description}`);
        spinner.succeed(`建立完畢`);
    });
}

複製程式碼
  • inquirer 主要提供互動式命令的功能。validate 返回 true 代表輸入值驗證合法,如果返回任意字串,則會替代預設的錯誤訊息返回。
  • 通過 Nodefs 模組來判斷資料夾是否已存在。

    path.resolve 方法用於將相對路徑轉為絕對路徑。它可以接受多個引數,依次表示所要進入的路徑,直到將最後一個引數轉為絕對路徑。如果根據引數無法得到絕對路徑,就以當前所在路徑作為基準。除了根目錄,該方法的返回值都不帶尾部的斜槓。

參考

相關文章