開發(fā)前端 CLI 腳手架思路解析
點(diǎn)擊上方?前端Q,關(guān)注公眾號(hào)
回復(fù)加群,加入前端Q技術(shù)交流群
為什么要自己搞腳手架
在實(shí)際的開發(fā)過程中,我們經(jīng)常用別人開發(fā)的腳手架,以節(jié)約搭建項(xiàng)目的時(shí)間。但是,當(dāng) npm 沒有自己中意的腳手架時(shí),我們不得不自己動(dòng)手,此時(shí)學(xué)會(huì)開發(fā)前端 CLI 腳手架的技能就顯得非常重要。搭建一個(gè)符合大眾化的腳手架能使自己在項(xiàng)目經(jīng)驗(yàn)上加個(gè)分哦!
什么時(shí)候需要腳手架
其實(shí)很多時(shí)候從 0 開始搭建的項(xiàng)目都可以做成模板,而腳手架的主要核心功能就是利用模板來快速搭建一個(gè)完整的項(xiàng)目結(jié)構(gòu),后續(xù)我們只需在這上面進(jìn)行開發(fā)就可以了。
入門需知
下面我們以創(chuàng)建 js 插件項(xiàng)目的腳手架來加深我們對(duì)前端腳手架的認(rèn)知。
在此之前,我們先把需要用到的依賴庫熟悉一下(點(diǎn)擊對(duì)應(yīng)庫名跳轉(zhuǎn)到對(duì)應(yīng)文檔):
chalk[1]?(控制臺(tái)字符樣式) commander[2]?(實(shí)現(xiàn) NodeJS 命令行) download[3]?(實(shí)現(xiàn)文件遠(yuǎn)程下載) fs-extra[4]?(增強(qiáng)的基礎(chǔ)文件操作庫) handlebars[5]?(實(shí)現(xiàn)模板字符替換) inquirer[6]?(實(shí)現(xiàn)命令行之間的交互) log-symbols[7]?(為各種日志級(jí)別提供著色符號(hào)) ora[8]?(優(yōu)雅終端 Spinner 等待動(dòng)畫) update-notifier[9]?(npm 在線檢查更新)
功能策劃
我們先用思維導(dǎo)圖來策劃一下我們的腳手架需要有哪些主要命令:init(初始化模板)、template(下載模板)、mirror(切換鏡像)、upgrade(檢查更新),相關(guān)導(dǎo)圖如下:
開始動(dòng)手
新建一個(gè)名為 js-plugin-cli 的文件夾后打開,執(zhí)行?npm init -y?快速初始化一個(gè)?package.json,然后根據(jù)下面創(chuàng)建對(duì)應(yīng)的文件結(jié)構(gòu):
js-plugin-cli
├─?.gitignore
├─?.npmignore
├─?.prettierrc
├─?LICENSE
├─?README.md
├─?bin
│??└─?index.js
├─?lib
│??├─?init.js
│??├─?config.js
│??├─?download.js
│??├─?mirror.js
│??└─?update.js
└─?package.json
復(fù)制代碼
其中 .gitignore、.npmignore、.prettierrc、LICENSE、README.md 是額外附屬文件(非必須),但這里推薦創(chuàng)建好它們,相關(guān)內(nèi)容根據(jù)自己習(xí)慣設(shè)定就行。在項(xiàng)目里打開終端,先把需要的依賴裝上,后續(xù)可以直接調(diào)用。
yarn?add?-D?chalk?commander?download?fs-extra?handlebars?inquirer?log-symbols?ora?update-notifier
復(fù)制代碼
注冊(cè)指令
當(dāng)我們要運(yùn)行調(diào)試腳手架時(shí),通常執(zhí)行?node ./bin/index.js?命令,但我還是習(xí)慣使用注冊(cè)對(duì)應(yīng)的指令,像?vue init webpack demo?的?vue?就是腳手架指令,其他命令行也要由它開頭。打開?package.json?文件,先注冊(cè)下指令:
"main":?"./bin/index.js",
"bin":?{
??"js-plugin-cli":?"./bin/index.js"
}
main?中指向入口文件?bin/index.js,而?bin?下的?js-plugin-cli?就是我們注冊(cè)的指令,你可以設(shè)置你自己想要的名稱(盡量簡(jiǎn)潔)。
萬物皆-v
我們先編寫基礎(chǔ)代碼,讓?js-plugin-cli -v?這個(gè)命令能夠在終端打印出來。
打開?bin/index.js?文件,編寫以下代碼 :
#!/usr/bin/env?node
//?請(qǐng)求?commander?庫
const?program?=?require('commander')
//?從?package.json?文件中請(qǐng)求?version?字段的值,-v和--version是參數(shù)
program.version(require('../package.json').version,?'-v,?--version')
//?解析命令行參數(shù)
program.parse(process.argv)
其中?#!/usr/bin/env node?(固定第一行)必加,主要是讓系統(tǒng)看到這一行的時(shí)候,會(huì)沿著對(duì)應(yīng)路徑查找 node 并執(zhí)行。調(diào)試階段時(shí),為了保證?js-plugin-cli?指令可用,我們需要在項(xiàng)目下執(zhí)行?npm link(不需要指令時(shí)用?npm unlink?斷開),然后打開終端,輸入以下命令并回車:
js-plugin-cli?-v
此時(shí),應(yīng)該返回版本號(hào)?1.0.0,如圖:
接下來我們將開始寫邏輯代碼,為了維護(hù)方便,我們將在?lib?文件夾下分模塊編寫,然后在?bin/index.js?引用。
upgrade 檢查更新
打開?lib/update.js?文件,編寫以下代碼 :
//?引用?update-notifier?庫,用于檢查更新
const?updateNotifier?=?require('update-notifier')
//?引用?chalk?庫,用于控制臺(tái)字符樣式
const?chalk?=?require('chalk')
//?引入?package.json?文件,用于?update-notifier?庫讀取相關(guān)信息
const?pkg?=?require('../package.json')
//?updateNotifier?是?update-notifier?的方法,其他方法可到?npmjs?查看
const?notifier?=?updateNotifier({
??//?從?package.json?獲取?name?和?version?進(jìn)行查詢
??pkg,
??//?設(shè)定檢查更新周期,默認(rèn)為?1000?*?60?*?60?*?24(1?天)
??//?這里設(shè)定為?1000?毫秒(1秒)
??updateCheckInterval:?1000,
})
function?updateChk()?{
??//?當(dāng)檢測(cè)到版本時(shí),notifier.update?會(huì)返回?Object
??//?此時(shí)可以用?notifier.update.latest?獲取最新版本號(hào)
??if?(notifier.update)?{
????console.log(`New?version?available:?${chalk.cyan(notifier.update.latest)},?it's?recommended?that?you?update?before?using.`)
????notifier.notify()
??}?else?{
????console.log('No?new?version?is?available.')
??}
}
//?將上面的?updateChk()?方法導(dǎo)出
module.exports?=?updateChk
這里需要說明兩點(diǎn):updateCheckInterval?默認(rèn)是?1?天,也就意味著今天檢測(cè)更新了一次,下一次能進(jìn)行檢測(cè)更新的時(shí)間點(diǎn)應(yīng)該為明天同這個(gè)時(shí)間點(diǎn)之后,否則周期內(nèi)檢測(cè)更新都會(huì)轉(zhuǎn)到?No new version is available.。
舉個(gè)栗子:我今天 10 點(diǎn)的時(shí)候檢查更新了一次,提示有新版本可用,然后我下午 4 點(diǎn)再檢查一次,此時(shí)將不會(huì)再提示有新版本可用,只能等到明天 10 點(diǎn)過后再檢測(cè)更新才會(huì)重新提示新版本可用。因此,將?updateCheckInterval?設(shè)置為?1000?毫秒,就能使每次檢測(cè)更新保持最新狀態(tài)。
另外,update-notifier?檢測(cè)更新機(jī)制是通過?package.json?文件的?name?字段值和?version?字段值來進(jìn)行校驗(yàn):它通過?name?字段值從 npmjs 獲取庫的最新版本號(hào),然后再跟本地庫的?version?字段值進(jìn)行比對(duì),如果本地庫的版本號(hào)低于 npmjs 上最新版本號(hào),則會(huì)有相關(guān)的更新提示。
當(dāng)然,此時(shí)我們還需要把?upgrade?命令聲明一下,打開?bin/index.js?文件,在合適的位置添加以下代碼:
//?請(qǐng)求?lib/update.js
const?updateChk?=?require('../lib/update')
//?upgrade?檢測(cè)更新
program
??//?聲明的命令
??.command('upgrade')
??//?描述信息,在幫助信息時(shí)顯示
??.description("Check?the?js-plugin-cli?version.")
??.action(()?=>?{
????//?執(zhí)行?lib/update.js?里面的操作
????updateChk()
??})
添加后的代碼應(yīng)該如圖所示:
記得把?program.parse(process.argv)?放到最后就行。添加好代碼后,打開控制臺(tái),輸入命令?js-plugin-cli upgrade?查看效果:
為了測(cè)試效果,我將本地庫?js-plugin-cli?下?package.json?的?name?改為?vuepress-creator,version?默認(rèn)為?1.0.0,而 npmjs 上?vuepress-creator?腳手架最新版本為 2.x,因此會(huì)有更新的提示。
mirror 切換鏡像鏈接
我們通常會(huì)把模板放 Github 上,但是在國內(nèi)從 Github 下載模板不是一般的慢,所以我考慮將模板放 Vercel 上,但是為了避免一些地區(qū)的用戶因網(wǎng)絡(luò)問題不能正常下載模板的問題,我們需要將模板鏈接變成可定義的,然后用戶就可以自定義模板鏈接,更改為他們自己覺得穩(wěn)定的鏡像托管平臺(tái)上,甚至還可以把模板下載下來,放到他們自己服務(wù)器上維護(hù)。
為了能夠記錄切換后的鏡像鏈接,我們需要在本地創(chuàng)建 config.json 文件來保存相關(guān)信息,當(dāng)然不是由我們手動(dòng)創(chuàng)建,而是讓腳手架來創(chuàng)建,整個(gè)邏輯過程如下:
所以我們還需要在?lib?文件夾下創(chuàng)建?config.js?文件,用于生成默認(rèn)配置文件。
打開?lib/config.js?文件,添加以下代碼:
//?請(qǐng)求?fs-extra?庫
const?fse?=?require('fs-extra')
const?path?=?require('path')
//?聲明配置文件內(nèi)容
const?jsonConfig?=?{
??"name":?"js-plugin-cli",
??"mirror":?"https://zpfz.vercel.app/download/files/frontend/tpl/js-plugin-cli/"
}
//?拼接?config.json?完整路徑
const?configPath?=?path.resolve(__dirname,?'../config.json')
async?function?defConfig()?{
??try?{
????//?利用?fs-extra?封裝的方法,將?jsonConfig?內(nèi)容保存成?json?文件
????await?fse.outputJson(configPath,?jsonConfig)
??}?catch?(err)?{
????console.error(err)
????process.exit()
??}
}
//?將上面的?defConfig()?方法導(dǎo)出
module.exports?=?defConfig
這里需要注意的是,我們不要再直接去用內(nèi)置的?fs?庫,推薦使用增強(qiáng)庫?fs-extra,fs-extra?除了封裝原有基礎(chǔ)文件操作方法外,還有方便的 json 文件讀寫方法。
打開?lib/mirror.js?文件,添加以下代碼:
//?請(qǐng)求?log-symbols?庫
const?symbols?=?require('log-symbols')
//?請(qǐng)求?fs-extra?庫
const?fse?=?require('fs-extra')
const?path?=?require('path')
//?請(qǐng)求?config.js?文件
const?defConfig?=?require('./config')
//?拼接?config.json?完整路徑
const?cfgPath?=?path.resolve(__dirname,?'../config.json')
async?function?setMirror(link)?{
??//?判斷?config.json?文件是否存在
??const?exists?=?await?fse.pathExists(cfgPath)
??if?(exists)?{
????//?存在時(shí)直接寫入配置
????mirrorAction(link)
??}?else?{
????//?不存在時(shí)先初始化配置,然后再寫入配置
????await?defConfig()
????mirrorAction(link)
??}
}
async?function?mirrorAction(link)?{
??try?{
????//?讀取?config.json?文件
????const?jsonConfig?=?await?fse.readJson(cfgPath)
????//?將傳進(jìn)來的參數(shù)?link?寫入?config.json?文件
????jsonConfig.mirror?=?link
????//?再寫入?config.json?文件
????await?fse.writeJson(cfgPath,?jsonConfig)
????//?等待寫入后再提示配置成功
????console.log(symbols.success,?'Set?the?mirror?successful.')
??}?catch?(err)?{
????//?如果出錯(cuò),提示報(bào)錯(cuò)信息
????console.log(symbols.error,?chalk.red(`Set?the?mirror?failed.?${err}`))
????process.exit()
??}
}
//?將上面的?setMirror(link)?方法導(dǎo)出
module.exports?=?setMirror
需要注意的是?async?和?await,這里用的是 Async/Await 的寫法,其他相關(guān)寫法可參照?fs-extra[10]?。async?一般默認(rèn)放函數(shù)前面,而?await?看情況添加,舉個(gè)例子:
...
const?jsonConfig?=?await?fse.readJson(cfgPath)
jsonConfig.mirror?=?link
await?fse.writeJson(cfgPath,?jsonConfig)
console.log(symbols.success,?'Set?the?mirror?successful.')
...
我們需要等待 fs-extra 讀取完,才可以進(jìn)行下一步,如果不等待,就會(huì)繼續(xù)執(zhí)行?jsonConfig.mirror = link?語句,就會(huì)導(dǎo)致傳入的 json 結(jié)構(gòu)發(fā)生變化。再比如?await fse.writeJson(cfgPath, jsonConfig)?這句,如果去掉?await,將意味著還在寫入 json 數(shù)據(jù)(假設(shè)寫入數(shù)據(jù)需要花 1 分鐘)時(shí),就已經(jīng)繼續(xù)執(zhí)行下一個(gè)語句,也就是提示?Set the mirror successful.,但實(shí)際上寫入文件不會(huì)那么久,就算去掉?await,也不能明顯看出先后執(zhí)行關(guān)系。老規(guī)矩,我們還需要把?mirror?命令聲明一下,打開?bin/index.js?文件,在合適的位置添加以下代碼:
//?請(qǐng)求?lib/mirror.js
const?setMirror?=?require('../lib/mirror')
//?mirror?切換鏡像鏈接
program
??.command('mirror?' )
??.description("Set?the?template?mirror.")
??.action((tplMirror)?=>?{
????setMirror(tplMirror)
??})
打開控制臺(tái),輸入命令?js-plugin-cli mirror 你的鏡像鏈接?查看效果:
此時(shí),在項(xiàng)目下應(yīng)該已經(jīng)生成 config.json 文件,里面相關(guān)內(nèi)容應(yīng)該為:
{
??"name":?"js-plugin-cli",
??"mirror":?"https://zpfz.vercel.app/download/files/frontend/tpl/js-plugin-cli/"
}
download 下載/更新模板
網(wǎng)絡(luò)上很多教程在談及腳手架下載模板時(shí)都會(huì)選擇?download-git-repo?庫,但是這里我選擇?download?庫,因?yàn)槔盟梢詫?shí)現(xiàn)更自由的下載方式,畢竟?download-git-repo?庫主要還是針對(duì) Github 等平臺(tái)的下載,而?download?庫可以下載任何鏈接的資源,甚至還有強(qiáng)大的解壓功能(無需再安裝其他解壓庫)。
在此之前,我們得先明白?lib/download.js?需要執(zhí)行哪些邏輯:下載/更新模板應(yīng)屬于強(qiáng)制機(jī)制,也就是說,不管用戶本地是否有模板存在,lib/download.js?都會(huì)下載并覆蓋原有文件,以保持模板的最新狀態(tài),相關(guān)邏輯圖示如下:
打開?lib/download.js?文件,添加以下代碼:
//?請(qǐng)求?download?庫,用于下載模板
const?download?=?require('download')
//?請(qǐng)求?ora?庫,用于實(shí)現(xiàn)等待動(dòng)畫
const?ora?=?require('ora')
//?請(qǐng)求?chalk?庫,用于實(shí)現(xiàn)控制臺(tái)字符樣式
const?chalk?=?require('chalk')
//?請(qǐng)求?fs-extra?庫,用于文件操作
const?fse?=?require('fs-extra')
const?path?=?require('path')
//?請(qǐng)求?config.js?文件
const?defConfig?=?require('./config')
//?拼接?config.json?完整路徑
const?cfgPath?=?path.resolve(__dirname,?'../config.json')
//?拼接?template?模板文件夾完整路徑
const?tplPath?=?path.resolve(__dirname,?'../template')
async?function?dlTemplate()?{
??//?參考上方?mirror.js?主代碼注釋
??const?exists?=?await?fse.pathExists(cfgPath)
??if?(exists)?{
????//?這里記得加?await,在?init.js?調(diào)用時(shí)使用?async/await?生效
????await?dlAction()
??}?else?{
????await?defConfig()
????//?同上
????await?dlAction()
??}
}
async?function?dlAction()?{
??//?清空模板文件夾的相關(guān)內(nèi)容,用法見?fs-extra?的?README.md
??try?{
????await?fse.remove(tplPath)
??}?catch?(err)?{
????console.error(err)
????process.exit()
??}
??//?讀取配置,用于獲取鏡像鏈接
??const?jsonConfig?=?await?fse.readJson(cfgPath)
??//?Spinner?初始設(shè)置
??const?dlSpinner?=?ora(chalk.cyan('Downloading?template...'))
??//?開始執(zhí)行等待動(dòng)畫
??dlSpinner.start()
??try?{
????//?下載模板后解壓
????await?download(jsonConfig.mirror?+?'template.zip',?path.resolve(__dirname,?'../template/'),?{
??????extract:?true
????});
??}?catch?(err)?{
????//?下載失敗時(shí)提示
????dlSpinner.text?=?chalk.red(`Download?template?failed.?${err}`)
????//?終止等待動(dòng)畫并顯示?X?標(biāo)志
????dlSpinner.fail()
????process.exit()
??}
??//?下載成功時(shí)提示
??dlSpinner.text?=?'Download?template?successful.'
??//?終止等待動(dòng)畫并顯示???標(biāo)志
??dlSpinner.succeed()
}
//?將上面的?dlTemplate()?方法導(dǎo)出
module.exports?=?dlTemplate
我們先用?fse.remove()?清空模板文件夾的內(nèi)容(不考慮模板文件夾存在與否,因?yàn)槲募A不存在不會(huì)報(bào)錯(cuò)),然后執(zhí)行等待動(dòng)畫并請(qǐng)求下載,模板文件名固定為?template.zip,download?語句里的?extract:true?表示開啟解壓。
上述代碼有兩處加了?process.exit(),意味著將強(qiáng)制進(jìn)程盡快退出(有點(diǎn)類似 return 的作用,只不過?process.exit()?結(jié)束的是整個(gè)進(jìn)程),哪怕還有未完全完成的異步操作。
就比如說第二個(gè)?process.exit()?吧,當(dāng)你鏡像鏈接處于 404 或者其他狀態(tài),它會(huì)返回你相應(yīng)的報(bào)錯(cuò)信息并退出進(jìn)程,就不會(huì)繼續(xù)執(zhí)行下面?dlSpinner.text?語句了。
我們還需要把?template?命令聲明一下,打開?bin/index.js?文件,在合適的位置添加以下代碼:
//?請(qǐng)求?lib/download.js
const?dlTemplate?=?require('../lib/download')
//?template?下載/更新模板
program
??.command('template')
??.description("Download?template?from?mirror.")
??.action(()?=>?{
????dlTemplate()
??})
打開控制臺(tái),輸入命令?js-plugin-cli template?查看效果:
上圖直接報(bào)錯(cuò)返回,提示 404 Not Found,那是因?yàn)槲疫€沒把模板文件上傳到服務(wù)器上。等把模板上傳后就能正確顯示了。
init 初始化項(xiàng)目
接下來是咱們最主要的 init 命令,init 初始化項(xiàng)目涉及的邏輯比其他模板相對(duì)較多,所以放在最后解析。
初始化項(xiàng)目的命令是?js-plugin-cli init 項(xiàng)目名,所以我們需要把?項(xiàng)目名?作為文件夾的名稱,也是項(xiàng)目內(nèi)?package.json?的?name?名稱(只能小寫,所以需要轉(zhuǎn)換)。由于模板是用于開發(fā) js 插件,也就需要拋出全局函數(shù)名稱(比如?import Antd from 'ant-design-vue'?的?Antd),所以我們還需要把模板的全局函數(shù)名稱拋給用戶來定義,通過控制臺(tái)之間的交互來實(shí)現(xiàn)。完成交互后,腳手架會(huì)把用戶輸入的內(nèi)容替換到模板內(nèi)容內(nèi),整個(gè)完整的邏輯導(dǎo)圖如下:
打開?lib/init.js?文件,添加以下代碼:
//?請(qǐng)求?fs-extra?庫,用于文件操作
const?fse?=?require('fs-extra')
//?請(qǐng)求?ora?庫,用于初始化項(xiàng)目時(shí)等待動(dòng)畫
const?ora?=?require('ora')
//?請(qǐng)求?chalk?庫
const?chalk?=?require('chalk')
//?請(qǐng)求?log-symbols?庫
const?symbols?=?require('log-symbols')
//?請(qǐng)求?inquirer?庫,用于控制臺(tái)交互
const?inquirer?=?require('inquirer')
//?請(qǐng)求?handlebars?庫,用于替換模板字符
const?handlebars?=?require('handlebars')
const?path?=?require('path')
//?請(qǐng)求?download.js?文件,模板不在本地時(shí)執(zhí)行該操作
const?dlTemplate?=?require('./download')
async?function?initProject(projectName)?{
??try?{
????const?exists?=?await?fse.pathExists(projectName)
????if?(exists)?{
??????//?項(xiàng)目重名時(shí)提醒用戶
??????console.log(symbols.error,?chalk.red('The?project?already?exists.'))
????}?else?{
??????//?執(zhí)行控制臺(tái)交互
??????inquirer
????????.prompt([{
??????????type:?'input',?//?類型,其他類型看官方文檔
??????????name:?'name',?//?名稱,用來索引當(dāng)前?name?的值
??????????message:?'Set?a?global?name?for?javascript?plugin?',
??????????default:?'Default',?//?默認(rèn)值,用戶不輸入時(shí)用此值
????????},?])
????????.then(async?(answers)?=>?{
??????????//?Spinner?初始設(shè)置
??????????const?initSpinner?=?ora(chalk.cyan('Initializing?project...'))
??????????//?開始執(zhí)行等待動(dòng)畫
??????????initSpinner.start()
??????????//?拼接?template?文件夾路徑
??????????const?templatePath?=?path.resolve(__dirname,?'../template/')
??????????//?返回?Node.js?進(jìn)程的當(dāng)前工作目錄
??????????const?processPath?=?process.cwd()
??????????//?把項(xiàng)目名轉(zhuǎn)小寫
??????????const?LCProjectName?=?projectName.toLowerCase()
??????????//?拼接項(xiàng)目完整路徑
??????????const?targetPath?=?`${processPath}/${LCProjectName}`
??????????//?先判斷模板路徑是否存在
??????????const?exists?=?await?fse.pathExists(templatePath)
??????????if?(!exists)?{
????????????//?不存在時(shí),就先等待下載模板,下載完再執(zhí)行下面的語句
????????????await?dlTemplate()
??????????}
??????????//?等待復(fù)制好模板文件到對(duì)應(yīng)路徑去
??????????try?{
????????????await?fse.copy(templatePath,?targetPath)
??????????}?catch?(err)?{
????????????console.log(symbols.error,?chalk.red(`Copy?template?failed.?${err}`))
????????????process.exit()
??????????}
??????????//?把要替換的模板字符準(zhǔn)備好
??????????const?multiMeta?=?{
????????????project_name:?LCProjectName,
????????????global_name:?answers.name
??????????}
??????????//?把要替換的文件準(zhǔn)備好
??????????const?multiFiles?=?[
????????????`${targetPath}/package.json`,
????????????`${targetPath}/gulpfile.js`,
????????????`${targetPath}/test/index.html`,
????????????`${targetPath}/src/index.js`
??????????]
??????????//?用條件循環(huán)把模板字符替換到文件去
??????????for?(var?i?=?0;?i?????????????//?這里記得?try?{}?catch?{}?哦,以便出錯(cuò)時(shí)可以終止掉?Spinner
????????????try?{
??????????????//?等待讀取文件
??????????????const?multiFilesContent?=?await?fse.readFile(multiFiles[i],?'utf8')
??????????????//?等待替換文件,handlebars.compile(原文件內(nèi)容)(模板字符)
??????????????const?multiFilesResult?=?await?handlebars.compile(multiFilesContent)(multiMeta)
??????????????//?等待輸出文件
??????????????await?fse.outputFile(multiFiles[i],?multiFilesResult)
????????????}?catch?(err)?{
??????????????//?如果出錯(cuò),Spinner?就改變文字信息
??????????????initSpinner.text?=?chalk.red(`Initialize?project?failed.?${err}`)
??????????????//?終止等待動(dòng)畫并顯示?X?標(biāo)志
??????????????initSpinner.fail()
??????????????//?退出進(jìn)程
??????????????process.exit()
????????????}
??????????}
??????????//?如果成功,Spinner?就改變文字信息
??????????initSpinner.text?=?'Initialize?project?successful.'
??????????//?終止等待動(dòng)畫并顯示???標(biāo)志
??????????initSpinner.succeed()
??????????console.log(`
????????????To?get?started:
??????????????cd?${chalk.yellow(LCProjectName)}
??????????????${chalk.yellow('npm?install')}?or?${chalk.yellow('yarn?install')}
??????????????${chalk.yellow('npm?run?dev')}?or?${chalk.yellow('yarn?run?dev')}
??????????`)
????????})
????????.catch((error)?=>?{
??????????if?(error.isTtyError)?{
????????????console.log(symbols.error,?chalk.red("Prompt?couldn't?be?rendered?in?the?current?environment."))
??????????}?else?{
????????????console.log(symbols.error,?chalk.red(error))
??????????}
????????})
????}
??}?catch?(err)?{
????console.error(err)
????process.exit()
??}
}
//?將上面的?initProject(projectName)?方法導(dǎo)出
module.exports?=?initProject
lib/init.js?的代碼相對(duì)較長,建議先熟悉上述的邏輯示意圖,了解這么寫的意圖后就能明白上述的代碼啦!抽主要的片段解析:
inquirer 取值說明inquirer.prompt?中的字段?name?類似 key,當(dāng)你需要獲取該值時(shí),應(yīng)以?answers.key對(duì)應(yīng)值?形式獲取(answers?命名取決于?.then(answers => {})),例:
inquirer.prompt([{
??type:?'input',?//?類型,其他類型看官方文檔
??name:?'theme',?//?名稱,用來索引當(dāng)前?name?的值
??message:?'Pick?a?theme?',
??default:?'Default',?//?默認(rèn)值,用戶不輸入時(shí)用此值
},?]).then(answers?=>?{})
上述要獲取對(duì)應(yīng)值應(yīng)該為?answers.theme。handlebars 模板字符設(shè)置說明
我們事先需要把模板文件內(nèi)要修改的字符串改成?{{ 定義名稱 }}?形式,然后才能用?handlebars.compile?進(jìn)行替換,為了保證代碼可讀性,我們把模板字符整成?{ key:value }?形式,然后?key?對(duì)應(yīng)定義名稱,value?對(duì)應(yīng)要替換的模板字符,例:
const?multiMeta?=?{
??project_name:?LCProjectName,
??global_name:?answers.name
}
上述代碼意味著模板文件內(nèi)要修改的字符串改成?{{ project_name }}?或者?{{ global_name }}?形式,當(dāng)被替換時(shí),將改成后面對(duì)應(yīng)的模板字符。下圖是模板文件:
接下來我們把?init?命令聲明一下,打開?bin/index.js?文件,在合適的位置添加以下代碼:
//?請(qǐng)求?lib/init.js
const?initProject?=?require('../lib/init')
//?init?初始化項(xiàng)目
program
??.name('js-plugin-cli')
??.usage('?[options]' )
??.command('init?' )
??.description('Create?a?javascript?plugin?project.')
??.action(project?=>?{
????initProject(project)
??})
打開控制臺(tái),輸入命令?js-plugin-cli init 你的項(xiàng)目名稱?查看效果:
這樣就完成整個(gè)腳手架的搭建了~然后可以發(fā)布到 npm,以全局安裝方式進(jìn)行安裝(記得?npm unlink?解除連接哦)。
寫在最最最后
這篇文章花了幾天時(shí)間(含寫腳手架 demo 的時(shí)間)編輯的,時(shí)間比較匆趕,若在語句上表達(dá)不夠明白或者錯(cuò)誤,歡迎掘友指出哦~
最后附上項(xiàng)目源碼:js-plugin-cli[11]?,腳手架已經(jīng)發(fā)布到 npm,歡迎小伙伴試用哦!

往期推薦



最后
歡迎加我微信,拉你進(jìn)技術(shù)群,長期交流學(xué)習(xí)...
歡迎關(guān)注「前端Q」,認(rèn)真學(xué)前端,做個(gè)專業(yè)的技術(shù)人...


參考資料
chalk:?https://www.npmjs.com/package/chalk
[2]commander:?https://www.npmjs.com/package/commander
[3]download:?https://www.npmjs.com/package/download
[4]fs-extra:?https://www.npmjs.com/package/fs-extra
[5]handlebars:?https://www.npmjs.com/package/handlebars
[6]inquirer:?https://www.npmjs.com/package/inquirer
[7]log-symbols:?https://www.npmjs.com/package/log-symbols
[8]ora:?https://www.npmjs.com/package/ora
[9]update-notifier:?https://www.npmjs.com/package/update-notifier
[10]fs-extra:?https://www.npmjs.com/package/fs-extra
[11]js-plugin-cli:?https://github.com/zpfz/js-plugin-cli/
