一、功能設計
每個前端小組都會有自己的獨特的業務場景,從這些業務場景從提取公共部分,並打造一個前端項目模版,是非常有必要的
爲了能夠基於這個項目模版快速創建一個新項目,就需要腳手架工具登場
所以這裏至少有兩個項目倉庫:前端模版項目、腳手架工具
而對於腳手架工具,它應當具備這樣的功能:輸入一個命令和項目名稱,創建對應的項目目錄,其內容就是模版項目
my-cli create my-project
基於此,腳手架工具的內部邏輯也就很清晰了:
創建一個 create 命令,其行爲是拉取模版項目的代碼(用 git clone 即可實現)
爲了實現這個命令,我們需要藉助一個神奇的工具:commander
二、Commander
Commander.js 是完整的 node.js 命令行解決方案
可以通過它創建一個 program 對象,作爲程序的主體
const { Command } = require('commander');
const program = new Command();
program.version('0.0.1');
commander 常用的功能有創建選項和命令
選項 option 需要基於短線 " - " 聲明,常見的 -h 、 -V 都是選項
每個選項可以定義一個短選項名稱(" - " 後面接單個字符)和一個長選項名稱(" -- "後面接一個或多個單詞)
解析後的選項可以通過 .opts() 方法獲取,從而執行對應的操作
比如創建一個 -d 選項:
program
.option('-d, --debug', 'output extra debugging')
program.parse(process.argv);
const options = program.opts();
if (options.debug) console.log(options);
還可以對選項定義參數及默認值:
program
.option('-d, --debug <type>', 'output extra debugging', 'hard')
這裏就對 debug 選項定義了 type 參數,並設置其默認值爲 hard
用尖括號 <> 定義的參數爲必填,用中括號 [] 創建的參數爲選填
命令 command 可以通過 .command() 創建,並通過鏈式調用 .description() 和 .action() 定義該命令的描述和具體行爲
program
.command('create <name> [type]')
.description('create a new project')
.action((name, type) => {
console.info('project', name);
});
和選項類似,命令也可以通過 <> 和 [] 定義參數
除了像上面那樣通過鏈式調用創建命令行爲(description、action)之外,還可以用單文件的形式描述命令,不過我沒有跑通
commander 還有更多更強大的功能,這裏就不逐一介紹了,詳情可以參考中文文檔
三、正式開始
學習了 commander 之後,就可以着手腳手架的開發了
首先創建腳手架工具的目錄,如 my-cli,然後在目錄下創建一個 .gitignore 文件
# Dependency directories
node_modules/
接着通過 npm init 創建 package.json
創建之後可以刪除其中的 main 和 scripts,然後安裝 commander
npm install commander --save
然後創建 bin 目錄及 bin/cli.js 文件:
#!/usr/bin/env node
const { Command } = require('commander');
const { name, version } = require('../package.json');
const program = new Command();
program.name(name).version(version);
program
.command("create <project-name>")
.description("create a new project")
.action((name) => {
console.info('project', name);
});
program.parse();
頂部的 #!/usr/bin/env node 是告訴操作系統,用 /usr/bin 下的 node 來執行這個腳本
一個簡單的 cli 命令就創建好了,可以回到根目錄,用 node 執行 cli.js 試試:
node bin/cli.js create my-project
可以看到 create 命令正常執行
但直接通過 node + 路徑 的形式運行代碼還是太死板了,可以通過 npm link 將項目掛到全局,就能像正常的腳手架工具那樣用了
首先需要在 package.json 裏添加 bin 命令:
{
...
"bin": {
"my-cli": "bin/cli.js"
}
}
這裏的 my-cli 是包的名稱,後面的路徑需要寫全後綴
然後在根目錄執行 npm link 就能將 my-cli 鏈接到全局
npm link 是一個很好的本地測試的手段,調試完成後可以通過 npm unlink 卸載
四、完善 create 命令
據文檔介紹, commander 支持將命令拆成單文件進行維護,但我沒有搞出來,最後只好加了一層 map 來拆文件
在根目錄下新建一個 command 目錄,並創建 command/create.js 和 command/index.js 兩個文件:
然後將 create 命令的相關邏輯移到 command/create.js 中
// create.js
function createAction(name) {
console.log('project', name);
}
const create = {
alias: 'c',
params: '<project-name>',
description: 'create a new project',
action: createAction,
}
module.exports = create;
並在 command/index.js 中導出
// index.js
const create = require('./create.js');
module.exports = {
create,
};
最後來改造 bin/cli.js
#!/usr/bin/env node
const { Command } = require('commander');
const { name, version } = require('../package.json');
const commands = require('../command/index.js');
const program = new Command();
program.name(name).version(version);
// 創建命令
Reflect.ownKeys(commands).map((name) => {
const { params, alias, action, description } = commands[name] || {};
program.command(`${name} ${params || ''}`)
.alias(alias)
.description(description)
.action((...args) => {
typeof action === 'function' && action(...args);
})
});
program.parse(process.argv);
項目的結構基本成型,接下來完善 create 命令
在文章的開頭就已經分析過了,create 命令只需要做一個事情,就是將項目 clone 到當前目錄
const { exec } = require("child_process");
function createAction(name) {
// 這是模板項目的倉庫地址
const url = "http://github.com/xxx/template.git";
// 克隆項目
exec(`git clone ${url} ${name}`, (error, stdout, stderr) => {
if (error) {
console.log(error);
process.exit();
}
console.log("Success");
process.exit();
});
}
如果模板項目是 github 上的項目,應該沒什麼大問題
而如果模板項目是小組內部的 gitlab 項目,就需要放開模板項目的訪問權限,至少讓小組人員都能夠訪問
五、優化體驗
在完成了 create 命令之後,我們的腳手架工具就可以算是開發完成
但爲了更好的體驗,可以藉助這些工具加以改造:chalk、inquirer
1. chalk
它可以在命令行打印彩色文字:
import chalk from 'chalk';
const error = chalk.bold.red;
const warning = chalk.hex('#FFA500');
console.log(chalk.blue('Hello world!'));
console.log(error('Error!'));
console.log(warning('Warning!'));
2. inquirer
這是一個讓用戶與命令行交互的工具
它提供了很多 api,讓用戶可以在程序運行的過程中輸入內容,從而影響程序運行的結果
const inquirer = require('inquirer');
inquirer
.prompt([
/* Pass your questions in here */
])
.then((answers) => {
console.log('success', answers);
})
.catch((error) => {
console.log('error');
});
在上面的 prompt 中配置需要用戶輸入/選擇的(表單)內容
比如讓用戶輸入項目名稱:
.prompt([
{
type: "input",
message: "項目名稱:",
name: "projectName",
validate: (val) => {
// 對輸入的值做判斷
if (!val || !val.trim()) {
return chalk.red("項目名不能爲空,請重新輸入");
} else if (val.includes(" ")) {
return chalk.red("項目名不能包含空格,請重新輸入");
}
return true;
},
},
])
除了這裏的 input 類型外,inquirer 還提供了很多交互類型,如單選列表 list、多選 checkbox、確認項 confirm 等
在瞭解了 chalk 和 inquirer 之後,我們就可以進一步改造 create 命令
npm install inquirer chalk --save
// create.js
const inquirer = require("inquirer");
const chalk = require("chalk");
const { exec } = require("child_process");
function createProject(name) {
// 這是模板項目的倉庫地址
const url = "[email protected]:wisewrong/chart-admin.git";
exec(`git clone ${url} ${name}`, (error, stdout, stderr) => {
if (error) {
console.log(chalk.red(error));
process.exit();
}
console.log(chalk.green("Success"));
process.exit();
});
}
const create = {
alias: "c",
params: "[project-name]",
description: "create a new project",
action: (project) => {
project
? createProject(project)
: inquirer
.prompt([
{
type: "input",
message: "項目名稱:",
name: "projectName",
validate: (val) => {
// 對輸入的值做判斷
if (!val || !val.trim()) {
return chalk.red("項目名不能爲空,請重新輸入");
} else if (val.includes(" ")) {
return chalk.red("項目名不能包含空格,請重新輸入");
}
return true;
},
},
])
.then((answer) => {
createProject(answer.projectName);
});
},
};
module.exports = create;
麻雀雖小五臟俱全,這樣一個簡單的腳手架就開發完了
如果需要發佈到 npm,可以參考我之前的文章《vue-cli 3.x 開發插件併發布到 npm》
後面我也會繼續完善這個腳手架,添加更多的交互,以及展示進度條(專業挖坑,從來不填...)