在實(shí)際開發(fā)過(guò)程中徘跪,我們經(jīng)常都會(huì)用到腳手架來(lái)構(gòu)建前端工程項(xiàng)目讳推,常見(jiàn)的主流框架都有自己的腳手架,vue-cli葵腹、create-react-app高每、angular-cli。在大型公司都會(huì)有內(nèi)部定制化的腳手架開發(fā)工具践宴,使用腳手架可以大幅提升項(xiàng)目的構(gòu)建速度鲸匿,通過(guò)命令行的交互,選擇你所需要的配置與集成阻肩,可快速完成初始項(xiàng)目的創(chuàng)建带欢。
既然使用了這么多腳手架創(chuàng)建項(xiàng)目,為何不自己實(shí)現(xiàn)一套屬于自己開發(fā)習(xí)慣的腳手架呢烤惊,本文將從0開始搭建一套腳手架開發(fā)工具乔煞。
什么是腳手架
腳手架是一類快速形成工程化目錄的工具(command-line-interface, 縮寫:CLI),簡(jiǎn)單來(lái)說(shuō)柒室,腳手架就是幫你減少「為重復(fù)性工作而做的重復(fù)性工作」的工具渡贾。
基于我們?nèi)粘J褂眠^(guò)的腳手架得知一個(gè)腳手架至少需要實(shí)現(xiàn)以下的功能點(diǎn):
- 有不同的命令執(zhí)行操作,比如:
Commands:
create [options] <app-name>
add [options] <plugin> [pluginOptions]
invoke [options] <plugin> [pluginOptions]
inspect [options] [paths...]
serve
build
ui [options]
init [options] <template> <app-name>
config [options] [value]
outdated [options]
upgrade [options] [plugin-name]
migrate [options] [plugin-name]
info
help [command]
- 可以通過(guò)命令行交互執(zhí)行問(wèn)答列表
? Please pick a preset:
? Default ([Vue 3] babel, eslint)
Default ([Vue 2] babel, eslint)
Manually select features
- 最終根據(jù)用戶的問(wèn)答結(jié)果生成對(duì)應(yīng)的文件
必備的知識(shí)庫(kù)
在完善構(gòu)建腳手架前雄右,需要引入一些腳手架構(gòu)建中必須用到的工具庫(kù)空骚。
commander
可以自定義一些命令行指令纺讲,在輸入自定義的命令行的時(shí)候,會(huì)去執(zhí)行相應(yīng)的操作inquirer
可以在命令行詢問(wèn)用戶問(wèn)題府怯,并且可以記錄用戶回答選擇的結(jié)果fs-extra
是fs的一個(gè)擴(kuò)展刻诊,提供了非常多的便利API,并且繼承了fs所有方法和為fs方法添加了promise的支持牺丙。chalk
可以美化終端的輸出figlet
可以在終端輸出logoora
控制臺(tái)的loading動(dòng)畫download-git-repo
下載遠(yuǎn)程模板
實(shí)現(xiàn)過(guò)程
創(chuàng)建腳手架
新建一個(gè)文件夾则涯,執(zhí)行 npm init
生成package.json文件。
npm init -y
創(chuàng)建bin文件夾冲簿,bin文件夾存放主要的命令程序入口文件粟判,并在文件夾下面新建index.js文件,目錄結(jié)構(gòu)如下:
fe
├─ bin
│ ├─ index.js
└─ package.json
在package.json指定命令的入口文件為剛剛新建的index.js峦剔,bin下面的fe
為程序的快速啟動(dòng)命令档礁。
{
"name": "fe",
"version": "1.0.0",
"main": "index.js",
"bin": {
"fe": "./bin/index.js"
},
...
}
修改入口文件index.js為如下內(nèi)容,文件以#!
開頭代表這個(gè)文件被當(dāng)做一個(gè)執(zhí)行文件來(lái)執(zhí)行吝沫,可以當(dāng)做腳本運(yùn)行呻澜。后面的/usr/bin/env node
代表這個(gè)文件用node執(zhí)行,node基于用戶安裝根目錄下的環(huán)境變量中查找:
#! /usr/bin/env node
console.log('hello fe cli')
最后將當(dāng)前命令鏈接到全局惨险,即可測(cè)試是否正常
npm link
此時(shí)在命令行中輸入剛剛的快速啟動(dòng)命令fe
羹幸,輸出了打印的日志。說(shuō)明執(zhí)行了index.js中的代碼辫愉,接下來(lái)就開始正式搭建腳手架的功能栅受。
[圖片上傳失敗...(image-d9919-1678670772961)]
這里我們可以額外擴(kuò)展一下輸出的樣式,當(dāng)然這個(gè)不是必要的恭朗,平時(shí)安裝依賴包的時(shí)候經(jīng)常會(huì)看到不同顏色的輸出和LOGO屏镊,主要是用了chalk
和figlet
,示例代碼如下:
program
.on('--help', () => {
console.log('\r\n' + chalk.white.bgBlueBright.bold(figlet.textSync('nanChengFE', {
font: 'Standard',
horizontalLayout: 'default',
verticalLayout: 'default',
width: 80,
whitespaceBreak: true
})));
console.log(`\r\nRun ${chalk.cyan(`fe <command> --help`)} for detailed usage of given command\r\n`)
})
最終輸出的樣式如下痰腮,具體的其他LOGO樣式和字體顏色可以看官方文檔而芥。
[圖片上傳失敗...(image-a79c7c-1678670772961)]
腳手架功能完善
基于commander
執(zhí)行自定義命令指令,具體的使用可以看相關(guān)文檔膀值,以下實(shí)現(xiàn)一個(gè)簡(jiǎn)單的create
命令蔚出,傳入?yún)?shù)并打印輸出日志。
#! /usr/bin/env node
const { Command } = require('commander');
const program = new Command();
program
.name('fe cli')
.description('這里是描述文案')
.version('1.0.0');
program.command('create <name>')
.description('創(chuàng)建一個(gè)新工程')
.action((name) => {
console.log('[ 工程名稱 ] >', name)
});
program.parse(process.argv);
通過(guò)command定義create命令虫腋,并要求必傳參數(shù)name骄酗,再通過(guò)action回調(diào)獲取到傳入的參數(shù),后續(xù)即可通過(guò)用戶輸入的名稱創(chuàng)建項(xiàng)目悦冀。
[圖片上傳失敗...(image-e7f45-1678670772961)]
接下來(lái)進(jìn)入創(chuàng)建文件的過(guò)程趋翻,在創(chuàng)建文件的時(shí)候需要校驗(yàn)當(dāng)前目錄下是否已經(jīng)存在,如果存在是否需要執(zhí)行覆蓋的動(dòng)作盒蟆。
const path = require('path')
const fs = require('fs-extra')
// 當(dāng)前命令行執(zhí)行的目錄
const cwd = process.cwd();
// 需要?jiǎng)?chuàng)建的目錄
const targetPath = path.join(cwd, name)
// 目錄是否存在
if (fs.existsSync(targetPath)) {
// 強(qiáng)制創(chuàng)建
if (options.force) {
} else {
// 詢問(wèn)用戶是否需要強(qiáng)制創(chuàng)建
}
} else {
// 目錄不存在正常創(chuàng)建
}
以上代碼可以看出options.force
命令參數(shù)可以直接進(jìn)入到強(qiáng)制創(chuàng)建的過(guò)程踏烙。如果還有其他的想法想通過(guò)不同的指令做不同的處理师骗,可以參考強(qiáng)制創(chuàng)建的邏輯進(jìn)行其他的自定義。
上面提到了詢問(wèn)用戶是否需要強(qiáng)制創(chuàng)建讨惩,這需要通過(guò)命令行與用戶進(jìn)行交互辟癌,獲取到用戶選擇的信息,inquirer
這個(gè)包可以在命令行詢問(wèn)用戶問(wèn)題并拿到結(jié)果進(jìn)行后續(xù)的邏輯交互荐捻。
let { action } = await inquirer.prompt([{
name: 'action',
type: 'list',
message: '目錄已存在黍少,請(qǐng)選擇:',
choices: [{
name: '覆蓋',
value: 'overwrite'
}, {
name: '取消',
value: false
}]
}])
if (!action) {
return;
} else if (action === 'overwrite') {
// 移除已存在的目錄
await fs.remove(targetPath)
}
[圖片上傳失敗...(image-9e376c-1678670772961)]
接下來(lái)繼續(xù)詢問(wèn)要選擇哪個(gè)模版,這個(gè)github有提供相關(guān)的接口处面,也可以自己維護(hù)模版配置的相關(guān)接口厂置,核心就是讓用戶選擇需要的模版,比如模版里有vue魂角,react昵济,小程序等不同場(chǎng)景或框架模版,用戶選擇模版后獲取到對(duì)應(yīng)的遠(yuǎn)端模版地址進(jìn)行下載野揪,這里會(huì)用到兩個(gè)新的npm包访忿,ora
可以在控制臺(tái)輸出loading動(dòng)畫,download-git-repo
執(zhí)行下載遠(yuǎn)程模板斯稳。示例代碼如下:
const spinner = ora('遠(yuǎn)端倉(cāng)庫(kù)開始下載...');
spinner.start();
// 本地存放地址
const tmp = path.join(
process.cwd(),
'templates',
'工程目錄名稱',
);
// 已存在路徑執(zhí)行刪除
if (fs.existsSync(tmp)) {
await fs.remove(tmp)
}
// 下載遠(yuǎn)端模版
download(
'遠(yuǎn)端倉(cāng)庫(kù)地址',
tmp,
{
clone: true,
},
(err) => {
if (err) {
spinner.fail('[ 遠(yuǎn)端倉(cāng)庫(kù)下載失敗 ]')
logger.console.error();
}
spinner.succeed('[ 遠(yuǎn)端倉(cāng)庫(kù)下載成功 ]')
}
)
操作過(guò)程如下:
[圖片上傳失敗...(image-1cbf1-1678670772961)]
如果只是做一個(gè)簡(jiǎn)單版本的腳手架下載模版到這里基本就差不多了醉顽,選擇對(duì)應(yīng)的框架就下載對(duì)應(yīng)的模版。但是實(shí)際開發(fā)過(guò)程中往往還不夠平挑,不同的框架中還會(huì)不同的配置,比如使用vue框架時(shí)系草,是否需要自動(dòng)加入vuex通熄,vue-router等等。所以在不同的模版中我們還要進(jìn)行下一步的用戶問(wèn)詢配置找都,基于用戶回答的結(jié)果創(chuàng)建更符合的工程文件唇辨。
這一段可以參考Vue-cli的源碼,在不同的模板項(xiàng)目中增加相關(guān)的問(wèn)詢配置能耻,比如在Vue模版中新增了以下prompts配置:
"name": {
"type": "string",
"required": true,
"message": "填寫項(xiàng)目名稱"
},
"description": {
"type": "string",
"required": false,
"message": "填寫項(xiàng)目描述",
"default": "ZZZ前端vue項(xiàng)目"
},
...
在執(zhí)行結(jié)束下載文件后繼續(xù)執(zhí)行項(xiàng)目問(wèn)詢配置列表赏枚,效果如下:
[圖片上傳失敗...(image-28c51-1678670772961)]
最后根據(jù)獲取到的用戶回答結(jié)果進(jìn)行工程文件的生成,比如將獲取到的工程名稱直接填充到模版內(nèi)容中晓猛,根據(jù)用戶的選擇是否需要增加vuex的使用等饿幅。這塊主要會(huì)用到以下幾個(gè)依賴。
- Metalsmith 靜態(tài)網(wǎng)站(博客戒职,項(xiàng)目)的生成器
- handlerbars 模板編譯器栗恩,通過(guò)template和json,輸出一個(gè)html
- consolidate 模板引擎整合庫(kù)
在vue_cli源碼中注冊(cè)了2個(gè)渲染器洪燥,類似于vue中的 v-if v-else的條件渲染磕秤,這個(gè)我們可以根據(jù)實(shí)際需要擴(kuò)展其他的條件渲染乳乌。
// register handlebars helper
Handlebars.registerHelper('if_eq', function (a, b, opts) {
return a === b
? opts.fn(this)
: opts.inverse(this)
})
Handlebars.registerHelper('unless_eq', function (a, b, opts) {
return a === b
? opts.inverse(this)
: opts.fn(this)
})
在模版文件中使用對(duì)應(yīng)的標(biāo)識(shí),結(jié)合模板編譯器即可動(dòng)態(tài)生成內(nèi)容市咆,具體的構(gòu)建細(xì)節(jié)有興趣的大家可以看看源碼或者其他文章詳細(xì)的解析汉操。
{{#if_eq userSelectVuex 'vuex'}}
import Vuex from 'vuex'
Vue.use(Vuex)
{{/if_eq}}
列舉這些是想說(shuō)明如果要改造一個(gè)腳手架生成的模版內(nèi)容我們直接可以找到對(duì)應(yīng)的配置進(jìn)行增加修改,在用戶問(wèn)詢處增加需要的場(chǎng)景字段蒙兰,獲取到對(duì)應(yīng)的值進(jìn)行模版內(nèi)容的動(dòng)態(tài)處理磷瘤。
總結(jié)
到這里我們實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的開發(fā)腳手架,有自定義的快捷命令癞己,可以進(jìn)行模板選擇膀斋,可以根據(jù)不同的模板進(jìn)行下一步的配置選擇,最終生成工程文件痹雅。這只是一個(gè)簡(jiǎn)單的實(shí)現(xiàn)仰担,看vue腳手架的命令就可以看出還有很多功能點(diǎn)可以進(jìn)一步完善,有興趣的同學(xué)可以深入研究绩社。
到此本文就結(jié)束了摔蓝,看完本文如果覺(jué)得有用,記得點(diǎn)個(gè)贊支持愉耙,收藏起來(lái)說(shuō)不定哪天就用上啦~
專注前端開發(fā)贮尉,分享前端相關(guān)技術(shù)干貨,公眾號(hào):南城大前端(ID: nanchengfe)