前言
目前前端發(fā)展蒸蒸日上珍德,工程化也越來越成熟。在這期間出現(xiàn)了很多優(yōu)秀的框架和工具矗漾。與此同時(shí)伴隨著與框架搭配使用的腳手架也呼之欲出锈候。前端腳手架工具發(fā)展的日益強(qiáng)大,比如vue-cli
敞贡,create-react-app
等等是在vue
泵琳,react
開發(fā)搭建項(xiàng)目常用的腳手架。小編在看了vue-cli3
誊役,vue-cli2
的腳手架實(shí)現(xiàn)之后获列,心血來潮自己實(shí)現(xiàn)一個(gè)簡(jiǎn)易版的腳手架,下邊我們一起來學(xué)習(xí)一下腳手架的實(shí)現(xiàn)流程蛔垢。
小編福利推薦击孩,更多精彩內(nèi)容請(qǐng)點(diǎn)擊鏈接,點(diǎn)擊這里
實(shí)現(xiàn)思路
我認(rèn)為vue-cli3
和vue-cli2
的實(shí)現(xiàn)區(qū)別有如下幾點(diǎn)
- 就是
vue-cli3
不在從git
倉庫下載模板啦桌,而是自己生成代碼和創(chuàng)建文件和文件夾溯壶。 -
vue-cli3
把webpack
的配置內(nèi)置了,不在暴露出來甫男,提供用戶自定義的配置文件來自定義自己的配置且改;而vue-cli2
則是把配置完全暴露出來,可以任意修改板驳。
本文我們這里是基于vue-cli2
的實(shí)現(xiàn)思路來一步步實(shí)現(xiàn)一個(gè)簡(jiǎn)單的react
版本腳手架又跛。下邊是小編整體的實(shí)現(xiàn)過程
1、添加自己腳手架的命令(lbs)
2若治、使用commander
工具為自己的lbs
命令添加解析參數(shù)慨蓝,解析參數(shù),添加自定義命令端幼;附上官方文檔 commander文檔
3礼烈、使用inquirer
實(shí)現(xiàn)命令行和用戶的交互(用戶輸入,選擇)婆跑;附上官方文檔 inquirer文檔
4此熬、根據(jù)用戶輸入的項(xiàng)目名稱,模板來下載滑进,解壓模板
5犀忱、修改模板里邊的文件(package.json,index.html等)
6扶关、為項(xiàng)目安裝依賴阴汇,結(jié)束
開始擼代碼
本文實(shí)現(xiàn)一個(gè) lbs init [projectName] --force
命令
projectName:
輸入的項(xiàng)目名稱
--force:
定義的選項(xiàng)(當(dāng)前目錄存在輸入的[projectName]
文件夾時(shí)候,是否強(qiáng)制覆蓋)
添加腳手架命令(lbs)
創(chuàng)建項(xiàng)目這一步省略
利用package.json
的bin
項(xiàng)來指定自己定義的命令對(duì)應(yīng)的可執(zhí)行文件的位置节槐,我們?cè)?code>package.json搀庶,添加如下代碼
"bin":{
"lbs": "./bin/lbs.js"
},
然后創(chuàng)建bin/lbs.js
文件,添加測(cè)試代碼:
#!/usr/bin/env node
console.log("hello lbs-cli")
第一行是必須添加的疯淫,是指定這里用node解析這個(gè)腳本地来。默認(rèn)找/usr/bin
目錄下,如果找不到去系統(tǒng)環(huán)境變量查找熙掺。
然后我們?cè)谌我饽夸浵麓蜷_cmd
窗口未斑,輸入lbs
命令,你會(huì)發(fā)現(xiàn)找不到命令币绩。其實(shí)我們還需要做一步操作蜡秽,就是把本地項(xiàng)目全局安裝一下,在當(dāng)前項(xiàng)目下執(zhí)行npm install . -g
缆镣,然后在cmd
下執(zhí)行lbs
命令芽突,你會(huì)發(fā)現(xiàn)會(huì)輸出我們打印的字符串。
到這里我們已經(jīng)成功在系統(tǒng)里添加了自己定義的lbs
命令董瞻,那么我們?cè)趺礊閘bs添加init寞蚌,create田巴,--version等等參數(shù)呢?
使用commander豐富我們的lbs命令
不熟悉commander的使用請(qǐng)看commander文檔
我們首先要安裝一下插件挟秤,然后初步嘗試一下為我們的lbs
命令添加版本查看的選項(xiàng)
const { program } = require("commander")
const pkg = require("./../package.json")
program.version(pkg.version,'-v --version')
program.parse(process.argv)
此時(shí)我們?cè)谌我饷钚袌?zhí)行lbs -v
或者lbs --version
壹哺,可以看到在控制臺(tái)輸出版本信息
接下來為lbs
命令添加一個(gè)命令:
// projectName 是一個(gè)可選參數(shù)
program.command('init [projectName]')
.description("初始化項(xiàng)目")
// 添加一個(gè)選項(xiàng)
.option('-f --force','如果存在輸入的項(xiàng)目目錄,強(qiáng)制刪除項(xiàng)目目錄')
.action((projectName,cmd)=>{
// projectName 是我們輸入的參數(shù)艘刚,
console.log(projectName)
// cmd是Command對(duì)象
console.log(cmd.force)
})
這里我們添加了一個(gè)init命令管宵,支持一個(gè)可選參數(shù)和一個(gè)-f的可選選項(xiàng)
這時(shí)候我們執(zhí)行一下
lbs init test -f
可以在控制臺(tái)查看到我們輸入的test
和cmd
對(duì)象∨噬酰可以在cmd中查找到存在force屬性箩朴。
如果執(zhí)行lbs init
,輸出如下
如果執(zhí)行lbs init test
秋度,輸出如下
這里我們主要是獲取這兩個(gè)數(shù)據(jù)炸庞,如果你的命令還有其它的復(fù)雜功能,還可以擴(kuò)展其它參數(shù)和選項(xiàng)荚斯。
這里只是command
的一種使用方式燕雁,當(dāng)我們?yōu)?code>command添加第二個(gè)描述參數(shù),就意味著使用獨(dú)立的可執(zhí)行文件作為子命令鲸拥,比如你的命令是init
那么你就需要?jiǎng)?chuàng)建一個(gè)lbs-init
腳本文件拐格,這個(gè)文件負(fù)責(zé)執(zhí)行你指定的命令,按照lbs-${command}
的方式創(chuàng)建腳本刑赶,我們創(chuàng)建lbs-init.js
文件
把命令修改如下捏浊,為command
方法添加第二個(gè)參數(shù)
// projectName 是一個(gè)可選參數(shù)
program.command('init [projectName]','init project')
.description("初始化項(xiàng)目")
// 添加一個(gè)選項(xiàng)
.option('-f --force','如果存在輸入的項(xiàng)目目錄,強(qiáng)制刪除項(xiàng)目目錄')
.action((projectName,cmd)=>{
console.log(projectName) // projectName 是我們輸入的參數(shù)撞叨,
console.log(cmd.force) // cmd是Command對(duì)象
})
執(zhí)行lbs init
金踪,你會(huì)發(fā)現(xiàn)什么也沒輸出。因?yàn)檫@里不會(huì)執(zhí)行到action方法牵敷,會(huì)去執(zhí)行我們創(chuàng)建的lbs-init.js
這個(gè)空文件胡岔。所以什么也不會(huì)輸出。這時(shí)候lbs.js
只需要定義init
命令就可以了枷餐。只需要這一行就足夠了program.command('init [projectName]','init project')
然后在lbs-init.js
添加解析代碼
const { program } = require("commander")
let projectName;
let force;
program.arguments('[projectName]') // 指定解析的參數(shù)
.description("初始化項(xiàng)目")
.option('-f --force','如果存在輸入的項(xiàng)目目錄靶瘸,強(qiáng)制刪除項(xiàng)目目錄')
.action((name,cmd)=>{
projectName = name;
force = cmd.force;
});
program.parse(process.argv);
console.log(projectName,force)
重新執(zhí)行lbs init test -f
發(fā)現(xiàn)數(shù)據(jù)都能獲取。到這里我們已經(jīng)可以為我們的lbs init
命令自定義參數(shù)和選項(xiàng)了毛肋,那么當(dāng)用戶只執(zhí)行lbs init
命令怨咪,這時(shí)候我們就獲取不到項(xiàng)目名稱,我們?cè)趺崔k呢润匙?請(qǐng)往下看
使用inquirer
實(shí)現(xiàn)命令行和用戶的交互(用戶輸入诗眨,選擇,問答)
這里我們需要安裝chalk
孕讳,inquirer
插件
chalk:
主要是自定義顏色控制臺(tái)輸出
創(chuàng)建一個(gè)logger.js工具類匠楚,主要是輸出控制臺(tái)信息
const chalk = require('chalk');
exports.warn = function(message){
console.log(chalk.yellow(message));
}
exports.error = function(message){
console.log(chalk.red(message))
}
exports.info = function(message){
console.log(chalk.white(message))
}
exports.infoGreen = function(message){
console.log(chalk.green(message))
}
exports.exit = function(error){
if(error && error instanceof Error){
console.log(chalk.red(error.message))
}
process.exit(-1);
}
這個(gè)庫是我們可以和用戶交互的工具巍膘;第一個(gè)問題是輸入項(xiàng)目名稱,第二個(gè)問題是讓用戶選擇一個(gè)模板芋簿,這里的模板需要在github上準(zhǔn)備好典徘,我這里只準(zhǔn)備了一個(gè)lb-react-apps-template,這個(gè)模板是基于react-apps-template這個(gè)項(xiàng)目重新建了一個(gè)git倉庫益咬。這個(gè)模板的具體實(shí)現(xiàn)可以可以看之前`webpack的系列文章:react+webpack4搭建前端項(xiàng)目,后邊兩個(gè)模板是是不存在的
// 設(shè)置用戶交互的問題
const questions = [
{
type: 'input',
name:'projectName',
message: chalk.yellow("輸入你的項(xiàng)目名字:")
},
{
type:'list',
name:'template',
message: chalk.yellow("請(qǐng)選擇創(chuàng)建項(xiàng)目模板:"),
choices:[
{name:"lb-react-apps-template",value:"lb-react-apps-template"},
{name:"template2",value:"tempalte2"},
{name:"template3",value:"tempalte3"}
]
}
];
// 如果用戶命令參數(shù)帶projectName,只需要詢問用戶選擇模板
if(projectName){
questions.splice(0,1);
}
// 執(zhí)行用戶交互命令
inquirer.prompt(questions).then(result=>{
if(result.projectName) {
projectName = result.projectName;
}
const templateName = result.template;
// 獲取projectName templateName
console.log("項(xiàng)目名稱:" + projectName)
console.log("模板名稱:" + templateName)
if(!templateName || !projectName){
// 退出
logger.exit();
}
// 往下走
checkProjectExits(projectName,templateName); // 檢查目錄是否存在
}).catch(error=>{
logger.exit(error);
})
這里的checkProjectExits
下邊會(huì)實(shí)現(xiàn)帜平,可以先忽略幽告。這時(shí)候我們執(zhí)行lbs init
,可以看到成功獲取到projectName
和templateName
接下來我們還需要判斷用戶輸入的項(xiàng)目名稱在當(dāng)前目錄是不是存在裆甩,在存在的情況下
1冗锁、如果用戶執(zhí)行的命令包含--force
,那么直接把存在的目錄刪除嗤栓,
2冻河、如果命令不包含 --force
,那么需要詢問用戶是否需要覆蓋茉帅。如果用戶需要覆蓋叨叙,那就直接刪除存在的文件夾,不過用戶不允許堪澎,那就直接退出
添加checkProjectExits
檢查目錄存在的方法擂错,代碼如下
function checkProjectExits(projectName,templateName){
const currentPath = process.cwd();
const filePath = path.join(currentPath,`${projectName}`); // 獲取項(xiàng)目的真實(shí)路徑
if(force){ // 強(qiáng)制刪除
if(fs.existsSync(filePath)){
// 刪除文件夾
spinner.logWithSpinner(`刪除${projectName}...`)
deletePath(filePath)
spinner.stopSpinner(false);
}
startDownloadTemplate(projectName, templateName) // 開始下載模板
return;
}
if(fs.existsSync(filePath)){ // 判斷文件是否存在 詢問是否繼續(xù)
inquirer.prompt( {
type: 'confirm',
name: 'out',
message: `${projectName}文件夾已存在,是否覆蓋樱蛤?`
}).then(data=>{
if(!data.out){ // 用戶不同意
exit();
}else{
// 刪除文件夾
spinner.logWithSpinner(`刪除${projectName}...`)
deletePath(filePath)
spinner.stopSpinner(false);
startDownloadTemplate(projectName, templateName) // 開始下載模板
}
}).catch(error=>{
exit(error);
})
}else{
startDownloadTemplate(projectName, templateName) // 開始下載模板
}
}
function startDownloadTemplate(projectName,templateName){
console.log(projectName,templateName)
}
我們這里用到了一個(gè)spinner
的工具類钮呀,新建lib/spinner.js
,主要是一個(gè)轉(zhuǎn)菊花的動(dòng)畫提示昨凡,代碼如下
const ora = require('ora')
const chalk = require('chalk')
const spinner = ora()
let lastMsg = null
exports.logWithSpinner = (symbol, msg) => {
if (!msg) {
msg = symbol
symbol = chalk.green('?')
}
if (lastMsg) {
spinner.stopAndPersist({
symbol: lastMsg.symbol,
text: lastMsg.text
})
}
spinner.text = ' ' + msg
lastMsg = {
symbol: symbol + ' ',
text: msg
}
spinner.start()
}
exports.stopSpinner = (persist) => {
if (!spinner.isSpinning) {
return
}
if (lastMsg && persist !== false) {
spinner.stopAndPersist({
symbol: lastMsg.symbol,
text: lastMsg.text
})
} else {
spinner.stop()
}
lastMsg = null
}
我們新建lib/io.js
爽醋,實(shí)現(xiàn)deletePath
刪除目錄方法,如下
function deletePath (filePath){
if(fs.existsSync(filePath)){
const files = fs.readdirSync(filePath);
for(let index=0; index<files.length; index++){
const fileNmae = files[index];
const currentPath = path.join(filePath,fileNmae);
if(fs.statSync(currentPath).isDirectory()){
deletePath(currentPath)
}else{
fs.unlinkSync(currentPath);
}
}
fs.rmdirSync(filePath);
}
}
可以創(chuàng)建my-app
文件夾便脊,這時(shí)候可以測(cè)試一下lbs init my-app -f
和lbs init -f
命令蚂四,查看my-app是否刪除,
執(zhí)行lbs init
哪痰,根據(jù)一步步提示证杭,輸入已經(jīng)存在的目錄名稱作為項(xiàng)目名稱;選擇模板妒御,檢查是否my-app文件夾被刪除解愤,如下
下載,解壓模板
下載模板乎莉,需要我們根據(jù)選擇的模板名稱拼接github倉庫相對(duì)應(yīng)的zip壓縮包的url送讲,然后執(zhí)行node的下載代碼奸笤,(注意這里是把下載的zip壓縮包下載到系統(tǒng)的臨時(shí)目錄)下載成功后把zip壓縮包解壓到用戶輸入項(xiàng)目名稱的目錄,解壓成功后刪除已下載的壓縮包哼鬓。這一個(gè)流程就結(jié)束了
這其中下載利用request
插件监右,解壓用到了decompress
插件,這兩個(gè)插件需要提前安裝一下异希,這兩個(gè)插件有不熟悉使用的小伙伴可以提前熟悉一下相關(guān)使用
重寫上邊的startDownloadTemplate
方法
function startDownloadTemplate(projectName,templateName){
// 開始下載模板
downloadTemplate(templateName, projectName , (error)=>{
if(error){
logger.exit(error);
return;
}
// 替換解壓后的模板package.json, index.html關(guān)鍵內(nèi)容
replaceFileContent(projectName,templateName)
})
}
function replaceFileContent(projectName,templateName){
console.log(projectName,templateName);
}
新建lib/download.js
健盒,實(shí)現(xiàn)downloadTemplate
下載模板的方法,代碼如下
const request = require("request")
const fs = require("fs")
const path = require("path")
const currentPath = process.cwd();
const spinner = require("./spinner")
const os = require("os")
const { deletePath , unzipFile } = require("./io")
exports.downloadTemplate = function (templateName,projectName,callBack){
// 根據(jù)templateName拼接github對(duì)應(yīng)的壓縮包url
const url = `https://github.com/liuboshuo/${templateName}/archive/master.zip`;
// 壓縮包下載的目錄称簿,這里是在系統(tǒng)臨時(shí)文件目錄創(chuàng)建一個(gè)目錄
const tempProjectPath = fs.mkdtempSync(path.join(os.tmpdir(), `${projectName}-`));
// 壓縮包保存的路徑
const file = path.join(tempProjectPath,`${templateName}.zip`);
// 判斷壓縮包在系統(tǒng)中是否存在
if(fs.existsSync(file)){
fs.unlinkSync(file); // 刪除本地系統(tǒng)已存在的壓縮包
}
spinner.logWithSpinner("下載模板中...")
let stream = fs.createWriteStream(file);
request(url,).pipe(stream).on("close",function(err){
spinner.stopSpinner(false)
if(err){
callBack(err);
return;
}
// 獲取解壓的目錄
const destPath = path.join(currentPath,`${projectName}`);
// 解壓已下載的模板壓縮包
unzipFile(file,destPath,(error)=>{
// 刪除創(chuàng)建的臨時(shí)文件夾
deletePath(tempProjectPath);
callBack(error);
});
})
}
在lib/io.js
添加解壓zip壓縮包的方法扣癣,代碼如下
const decompress = require("decompress");
exports.unzipFile = function(file,destPath,callBack){
decompress(file,destPath,{
map: file => {
// 這里可以修改文件的解壓位置,
// 例如壓縮包中文件的路徑是 ${destPath}/lb-react-apps-template/src/index.js =》 ${destPath}/src/index.js
const outPath = file.path.substr(file.path.indexOf('/') + 1)
file.path = outPath
return file
}}
).then(files => {
callBack()
}).catch(error=>{
callBack(error)
})
}
這里可以執(zhí)行以下lbs init my-app
測(cè)試一下
修改項(xiàng)目中的模板文件(package.json憨降,index.html等)
重寫replaceFileContent
方法父虑,這一步是把模板中的一些文件的內(nèi)容修改以下,比如package.json的name授药,index.html的title值
function replaceFileContent(projectName,templateName){
const currentPath = process.cwd();
try{
// 讀取項(xiàng)目的package.json
const pkgPath = path.join(currentPath,`${projectName}/package.json`);
// 讀取內(nèi)容
const pkg = require(pkgPath);
// 修改package.json的name屬性為項(xiàng)目名稱
pkg.name = projectName;
fs.writeFileSync(pkgPath,JSON.stringify(pkg,null,2));
const indexPath = path.join(currentPath, `${projectName}/index.html`);
let html = fs.readFileSync(indexPath).toString();
// 修改模板title為項(xiàng)目名稱
html = html.replace(/<title>(.*)<\/title>/g,`<title>${projectName}</title>`)
fs.writeFileSync(indexPath,html);
}catch(error){
exit(error)
}
// 安裝依賴
install(projectName)
}
function install(projectName){
console.log(projectName)
}
安裝依賴
重寫install
方法士嚎,這里利用child_process
包創(chuàng)建一個(gè)node
的子進(jìn)程來執(zhí)行npm install
任務(wù)。注意這里要執(zhí)行的命令npm
在不同系統(tǒng)有區(qū)別悔叽,在window
下執(zhí)行的是npm.cmd
命令莱衩,在linux
和mac
執(zhí)行的是npm
命令
有不熟悉child_process
使用的小伙伴可以深入學(xué)習(xí)一下,這是nodejs自帶的一個(gè)包娇澎,非常有用膳殷,這里貼一下文檔地址 child_process官方文檔,這里利用spawn
方法執(zhí)行系統(tǒng)命令九火,還可以使用execFileSync
方法來執(zhí)行文件等等
const currentPath = process.cwd();
const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm'
// 創(chuàng)建一個(gè)子進(jìn)程執(zhí)行npm install 任務(wù)
const nodeJob = child_process.spawn(npm , ['install'], {
stdio: 'inherit', // 指定父子進(jìn)程通信方式
cwd: path.join(currentPath,projectName)
});
// 監(jiān)聽任務(wù)結(jié)束赚窃,提示用戶創(chuàng)建成功,接下來的操作
nodeJob.on("close",()=>{
logger.info(`創(chuàng)建成功! ${projectName} 項(xiàng)目位于 ${path.join(currentPath,projectName)}`)
logger.info('')
logger.info('你可以執(zhí)行以下命令運(yùn)行開發(fā)環(huán)境')
logger.infoGreen(` cd ${projectName} `);
logger.infoGreen(` npm run dev `);
})
執(zhí)行lbs init
測(cè)試一下
那么到這里一個(gè)簡(jiǎn)易版的腳手架已經(jīng)完成岔激!
有什么疑問可以關(guān)注公眾號(hào)私信哦~