<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          開發(fā)前端 CLI 腳手架思路解析

          共 14498字,需瀏覽 29分鐘

           ·

          2021-12-20 11:54

          點(diǎn)擊上方?前端Q,關(guān)注公眾號(hào)

          回復(fù)加群,加入前端Q技術(shù)交流群

          作者:左撇峰子,原文鏈接:https://juejin.im/post/6879265583205089287

          為什么要自己搞腳手架

          在實(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-creatorversion?默認(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-extrafs-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.zipdownload?語句里的?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.themehandlebars 模板字符設(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,歡迎小伙伴試用哦!

          聲明:文章著作權(quán)歸作者所有,如有侵權(quán),請(qǐng)聯(lián)系小編刪除。



          往期推薦


          尤雨溪回應(yīng):為什么 Vue 在國際上越來越?jīng)]影響力?
          前端服務(wù)框架調(diào)研:Next.js、Nuxt.js、Nest.js、Fastify
          基于WebAssembly的圖片渲染/視頻處理/云原生的場(chǎng)景應(yīng)用有哪些?

          最后


          • 歡迎加我微信,拉你進(jìn)技術(shù)群,長期交流學(xué)習(xí)...

          • 歡迎關(guān)注「前端Q」,認(rèn)真學(xué)前端,做個(gè)專業(yè)的技術(shù)人...

          點(diǎn)個(gè)在看支持我吧


          參考資料

          [1]

          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/



          瀏覽 62
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  av中文字 | 激情五月天成人电影 | 热热热色色| 国产插逼区| 狠狠狠狠狠狠干 |