CLI 是什麼
提起 CLI
,不由得會想起 vue-cli
和 angular-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 link
、demo
,這個時候,你會發現控制檯輸出了 hello CLI
。
備註:
#!/usr/bin/env node
告訴作業系統用Node
來執行此檔案npm link
作用主要是,在開發npm
模組的時候,我們會希望邊開發邊除錯。這個時候,npm link
就派上用場了。
逐步深入
- 在
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
來開發命令列的可能性。我們可以通過 commander
的 option
方法,來定義 commander
的選項,當然,這些定義的選項也會被作為該命令的幫助文件。
version
:用來定義版本號。commander
預設幫我們新增-V, --version
選項。當然,我們也可以重設它。command
:<>
代表必填,[]
代表選填。當.command()
帶有描述引數時,不能採用.action(callback)
來處理子命令,否則會出錯。這告訴commander
,你將採用單獨的可執行檔案作為子命令。parse
:解析process.argv
,解析完成後的資料會存放到new Command().args
陣列中。process.argv
裡面儲存內容如下:
所以,我們可以通過 program.args[0]
來取出 dir
的值。
問題:為什麼當
command
沒有描述引數,且parse
方法使用鏈式呼叫會報錯?(猜想:command
有desc
引數時,返回的是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);
```
複製程式碼
- 在
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
代表輸入值驗證合法,如果返回任意字串,則會替代預設的錯誤訊息返回。- 通過
Node
中fs
模組來判斷資料夾是否已存在。
path.resolve
方法用於將相對路徑轉為絕對路徑。它可以接受多個引數,依次表示所要進入的路徑,直到將最後一個引數轉為絕對路徑。如果根據引數無法得到絕對路徑,就以當前所在路徑作為基準。除了根目錄,該方法的返回值都不帶尾部的斜槓。