背景介紹
每次開發(fā)新的項(xiàng)目,類型多樣,有vue的、也有小程序的,雖然已經(jīng)有各種類型的項(xiàng)目模板,但每次都需要去下載模板然后修改配置,就顯得特別麻煩,于是開發(fā)一個(gè)腳手架去管理這些項(xiàng)目。
解決思路
研究過vue-cli的一些代碼,它將項(xiàng)目模板和腳手架分離,模板存放在git上,然后在與用戶交互的時(shí)候下載不同的模板,這樣模板可以單獨(dú)維護(hù),即使有更新,也不用更新腳手架。
借鑒vue-cli就可以實(shí)現(xiàn)簡(jiǎn)單的項(xiàng)目管理了。
依賴庫
- commander:解析命令行的命令和參數(shù),用于處理用戶輸入的指令;
- Inquirer:通用的命令行用戶界面集合,用于和用戶進(jìn)行交互;
-
download-git-repo:下載
git倉庫; -
shell:
shell命令執(zhí)行; - ora:終端loading動(dòng)畫;
- chalk:終端樣式;
- log-symbols:給終端加上彩色符號(hào);
- fs-extra:文件操作;
初始化
yarn init
yarn add commander inquirer download-git-repo handlebars shelljs ora chalk
此時(shí)目錄下就會(huì)生成好package.json文件,并且安裝好相關(guān)依賴。

打開package.json增加bin字段,定義命令名和執(zhí)行文件
{
"name": "lp-cli",
...
"bin": {
"lp-create-project": "bin/index.js"
},
"scripts": {
"test": "node bin/index.js create abc"
},
...
}
創(chuàng)建bin/index.js文件,并在文件開頭寫上#!/usr/bin/env node,這個(gè)代表這個(gè)文件是node環(huán)境下的腳本文件。
處理命令行
創(chuàng)建一個(gè)命令,輸入create即可,使用方法lp-create-project create [項(xiàng)目名]
const program = require('commander');
program.version(`lp-cli ${require('../package').version}`).usage('<command> [options]');
program
.command('create [name]')
.description('創(chuàng)建一個(gè)由lp-cli支持的初始化項(xiàng)目')
.option('create <項(xiàng)目名>', '創(chuàng)建時(shí)輸入項(xiàng)目名')
.action((name) => {
if(!name){
console.log(
`\n Usage: create [projectName]\n`
);
return
}
require('../lib/create')(name);
});
program.parse(process.argv);
入口文件只做命令行處理,具體其它步驟抽離出來。
命令行交互
這部分就靠inquirer實(shí)現(xiàn),lib/question.js構(gòu)建問答系統(tǒng),收集交互過程中用戶提交選擇的信息。
const inquirer = require('inquirer');
module.exports = () => {
return inquirer.prompt([
{
type: 'list',
message: '請(qǐng)選擇項(xiàng)目類型',
name: 'type',
choices: ['vue', 'wxapp'],
},
{
type: 'confirm',
message: '是否初始化 git 倉庫',
name: 'ifGitInit',
default: true,
},
{
type: 'confirm',
message: '下載完成是否自動(dòng)安裝依賴包',
name: 'ifInstall',
default: true,
},
{
type: 'list',
message: '請(qǐng)選擇安裝方式',
name: 'installWay',
choices: ['yarn', 'npm'],
when: (answers) => {
return answers.ifInstall;
},
},
]);
};
功能實(shí)現(xiàn)
在lib/generator.js生成器中根據(jù)用戶的輸入實(shí)現(xiàn)具體的功能。
- 下載項(xiàng)目
const download = require('download-git-repo');
download(gitRepo[this.answers.type], this.answers.name, (err) => {
if (err) {
console.log(symbols.error, chalk.red('項(xiàng)目創(chuàng)建失敗'));
} else {
console.log(symbols.success, chalk.green('項(xiàng)目創(chuàng)建成功'));
}
});
-
git初始化
const shell = require('shelljs');
shell.cd(this.answers.name).exec('git init', (err) => {
if (err) {
console.log('git倉庫創(chuàng)建失敗');
} else {
console.log(symbols.success, chalk.green('git 倉庫初始化成功'));
}
});
- 是否安裝依賴包
const shell = require('shelljs');
shell.cd(this.answers.name).exec(`${this.answers.installWay == 'yarn' ? 'yarn' : 'npm i'}`, (err) => {
if (err) {
console.log('依賴包安裝失敗');
} else {
console.log(symbols.success, chalk.green('依賴包安裝成功'));
}
});
}
視覺美化后的完整功能
// 下載模板
const download = require('download-git-repo');
// 命令行操作
const shell = require('shelljs');
// 顯示提示圖標(biāo)
const symbols = require('log-symbols');
// 字體加顏色
const chalk = require('chalk');
// 動(dòng)畫效果
const ora = require('ora');
const gitRepo = {
vue: 'MonsterNO/vue-demp',
wxapp: 'MonsterNO/pro_template',
};
module.exports = class Generator {
constructor(answers) {
this.answers = answers;
}
//下載
downloadGit() {
let spinner = ora('下載中...');
return new Promise((resolve, reject) => {
download(gitRepo[this.answers.type], this.answers.name, (err) => {
if (err) {
spinner.fail();
console.log(symbols.error, chalk.red('項(xiàng)目創(chuàng)建失敗'));
reject(err);
} else {
spinner.succeed();
console.log(symbols.success, chalk.green('項(xiàng)目創(chuàng)建成功'));
resolve();
}
});
});
}
//git初始化
gitInit() {
if (this.answers.ifGitInit) {
shell.cd(this.answers.name).exec('git init', (err) => {
if (err) {
console.log(symbols.error, chalk.red(err));
} else {
console.log(symbols.success, chalk.green('git 倉庫初始化成功'));
}
});
}
}
// 安裝依賴包
addPackage() {
if (this.answers.ifInstall) {
let spinner = ora('安裝中...');
spinner.start();
shell.cd(this.answers.name).exec(`${this.answers.installWay == 'yarn' ? 'yarn' : 'npm i'}`, (err) => {
if (err) {
spinner.fail();
console.log(symbols.error, chalk.red(err));
} else {
spinner.succeed();
console.log(symbols.success, chalk.green('依賴包安裝成功'));
}
});
}
}
};
調(diào)用生成器
在lib/create.js中調(diào)用執(zhí)行
const Generator = require('./Generator');
module.exports = async (name) => {
let answers = await question();
answers.name = name
let generator = new Generator(answers);
await generator.downloadGit();
generator.gitInit();
generator.addPackage();
};

優(yōu)化
- 項(xiàng)目文件夾重復(fù)處理
在lib/create.js函數(shù)調(diào)用功能函數(shù)前校驗(yàn)項(xiàng)目文件夾是否存在可選擇清理文件夾或者不處理。
const fs = require('fs-extra')
const chalk = require('chalk');
const inquirer = require('inquirer');
module.exports = async (name) => {
if(fs.existsSync(name)){
let {action} = await inquirer.prompt([
{
name:'action',
type:'list',
message:`文件夾${name}已存在,請(qǐng)選擇下一步操作:`,
choices:[
{name:'clear',value:'clear'},
{name:'cancel',value:false}
]
}
])
if(!action){
return
}else{
console.log(`\nRemoving ${chalk.cyan(name)}...`)
await fs.remove(name)
}
}
};

- 考慮下載項(xiàng)目模板可能會(huì)出錯(cuò),加上異常捕獲
//異常退出程序
try {
await generator.downloadGit();
} catch (err) {
process.exit(1)
}
到此這個(gè)腳手架的基本功能已經(jīng)可以正常使用了,同時(shí)可以繼續(xù)研究加入更多的功能,去優(yōu)化整體的一個(gè)功能。
發(fā)布
最后可以通過yarn publish 把腳手架發(fā)布到NPM上就可以使用了。