手把手教你從0搭建前端腳手架詳解
本篇文章用來(lái)為大家提供一個(gè)搭建簡(jiǎn)易前端腳手架的思路。
先來(lái)看一眼實(shí)現(xiàn)的效果。

從圖上來(lái)看這個(gè)腳手架的功能非常的簡(jiǎn)單只有一個(gè)創(chuàng)建的命令,其他都是幫助和顯示版本號(hào)的。

也就是上圖這句,創(chuàng)建一個(gè)新項(xiàng)目,只需要輸入create 項(xiàng)目名便可使用,在創(chuàng)建時(shí)執(zhí)行了一系列的操作,這一塊的思路很簡(jiǎn)單,就是將git倉(cāng)庫(kù)中的項(xiàng)目模板拷貝下來(lái)再依據(jù)使用者的不同操作對(duì)復(fù)制下來(lái)的模板的部分文件進(jìn)行修改就可以了,大致思路便介紹到這里,接下來(lái)我們便來(lái)詳細(xì)的講講如何實(shí)現(xiàn),以及會(huì)用到的依賴。
腳手架目錄結(jié)構(gòu)

了解搭建的腳手架
腳手架就是在啟動(dòng)的時(shí)候詢問(wèn)一些簡(jiǎn)單的問(wèn)題,并且通過(guò)用戶回答的結(jié)果去渲染對(duì)應(yīng)的模板文件,我們接下來(lái)的流程亦是如此
腳手架的初始化
由于它是一個(gè)npm的包,因此我們需要使用npm的初始化命令,隨意新建一個(gè)文件夾打開命令行,輸入npm init,會(huì)出現(xiàn)以下情況。

| 名稱 | 意思 | 默認(rèn)值 |
| package name | 包的名稱 | 創(chuàng)建文件夾時(shí)的名稱 |
| version | 版本號(hào) | 1.0.0 |
| description | 包的描述 | 創(chuàng)建文件時(shí)的名稱 |
| entry point | 入口文件 | index.js |
| test command | 測(cè)試命令 | — |
| git repository | git倉(cāng)庫(kù)地址 | — |
| git倉(cāng)庫(kù)地址 | 關(guān)鍵詞,上傳到npm官網(wǎng)時(shí)在頁(yè)面中展示的關(guān)鍵詞 | — |
| author | 作者信息,對(duì)象的形式,里面存儲(chǔ)一些郵箱、作者名、url | — |
| license | 執(zhí)照 | MIT |
這就是輸入初始化命令時(shí)會(huì)詢問(wèn)的東西,回答完這些后就會(huì)生成一個(gè) package.json 的文件,這個(gè)文件就是記錄包的信息。
如果想要了解更多,可查看如下地址:
package.json詳解
腳手架依賴安裝
用到如下依賴請(qǐng)安裝。
npm i path npm i chalk@4.1.0 npm i fs-extra npm i inquirer@8.2.4 npm i commander npm i axios npm i download-git-repo
詢問(wèn)用戶問(wèn)題
創(chuàng)建入口文件
在詢問(wèn)問(wèn)題前我們需要先創(chuàng)建一個(gè)入口文件,創(chuàng)建完成后在package.json中添加bin項(xiàng),并且將入口文件路徑寫進(jìn)去

填寫完入口文件路徑后在入口文件內(nèi)隨便輸出一句, 但必須在入口文件頂層聲明文件執(zhí)行方式為node。
聲明代碼:
#! /usr/bin/env node

寫完后我們需要測(cè)試一下我們是否可以正常的訪問(wèn)的我們的腳手架,在本文件夾打開命令行,輸入 npm link ,該命令會(huì)創(chuàng)建一個(gè)全局訪問(wèn)的包的快捷方式,這個(gè)是臨時(shí)的就是本地測(cè)試的時(shí)候用的,這個(gè)在命令行輸入你的腳手架的名稱可以看到入口文件輸出的內(nèi)容。

最基本的交互命令
在完成上一步后我們就要開始與用戶進(jìn)行交互了,這個(gè)時(shí)候我們就需要用到一個(gè)用于自定義命令行指令的依賴 commander。
引入依賴:
const program = require('commander')簡(jiǎn)單介紹一下commander依賴常用的方法
command
命令。.command()的第一個(gè)參數(shù)為命令名稱。命令參數(shù)可以跟在名稱后面,也可以用.argument()單獨(dú)指定。
參數(shù)可為必選的(尖括號(hào)表示)、可選的(方括號(hào)表示)或變長(zhǎng)參數(shù)(點(diǎn)號(hào)表示,如果使用,只能是最后一個(gè)參數(shù))。
例如:
// 創(chuàng)建一個(gè)create命令
.command('create <app-name>')
parse
解析。.parse()的第一個(gè)參數(shù)是要解析的字符串?dāng)?shù)組,也可以省略參數(shù)而使用process.argv,這里我們也是用process.argv用來(lái)解析node的參數(shù)。
例如:
// 解析用戶執(zhí)行命令傳入?yún)?shù) program.parse(process.argv);
option
選項(xiàng)。option()可以附加選項(xiàng)的簡(jiǎn)介。第一個(gè)參數(shù)可以定義一個(gè)短選項(xiàng)名稱(-后面接單個(gè)字符)和一個(gè)長(zhǎng)選項(xiàng)名稱(–后面接一個(gè)或多個(gè)單詞),使用逗號(hào)、空格或|分隔。第二個(gè)參數(shù)為該選項(xiàng)的簡(jiǎn)介。
例如:
.option('-f, --force', '如果存在的話強(qiáng)行覆蓋')
action
處理函數(shù)。用command創(chuàng)建的自定義命令的處理函數(shù),action攜帶的實(shí)參順序就是命令上的參數(shù)的順序。
例如:
program.command('create <app-name>')
// 這個(gè)name 就代表第一個(gè)必填參數(shù) options就代表其余, 如果有第二個(gè)就在寫一個(gè),最后一個(gè)永遠(yuǎn)是剩余參數(shù)
.action((name, options) => {
console.log(name)
// 打印執(zhí)行結(jié)果
// require("../lib/create")(name, options)
})
編寫交互命令 create
入口文件
#! /usr/bin/env node
const program = require('commander');
const chalk = require('chalk');
// 定義命令和參數(shù)
// create命令
program
.command('create <app-name>')
.description('create a new project')
// -f or --force 為強(qiáng)制創(chuàng)建,如果創(chuàng)建的目錄存在則直接覆蓋
.option('-f, --force', 'overwrite target directory if it exist')
.action((name, options) => {
// 打印執(zhí)行結(jié)果
console.log('項(xiàng)目名稱', name)
})
// 解析用戶執(zhí)行命令傳入?yún)?shù)
program.parse(process.argv);
這里我們創(chuàng)建了一個(gè)叫 create 的自定義指令,這個(gè)命令有著必填的項(xiàng)目名、可以選擇的強(qiáng)制覆蓋的選項(xiàng) -f,有著處理函數(shù)action。
我們?cè)赼ction中接收并打印了用戶輸入的項(xiàng)目名稱。
接下來(lái)我們?cè)俅芜\(yùn)行一下自己的腳手架并帶上create命令,我的叫test
test-cli create app
出現(xiàn)如下就說(shuō)明第一個(gè)命令創(chuàng)建成功了

這里請(qǐng)注意 解析用戶命令參數(shù)的操作一定要在最后一行否則什么都不會(huì)出現(xiàn)。
program.parse(process.argv)
到這里為止我們成功為我們腳手架創(chuàng)建了第一個(gè)交互命令,想查看更多關(guān)于 commander 的請(qǐng)點(diǎn)擊這里commander。
創(chuàng)建第一個(gè)模板項(xiàng)目
在創(chuàng)建了一個(gè)基本命令 create 后我們就要開始創(chuàng)建一個(gè)模板并在用戶使用該命令時(shí)復(fù)制并修改我們所創(chuàng)建的模板。
創(chuàng)建一個(gè)模板
我們?cè)趶?fù)制模板前需要一個(gè)模板,現(xiàn)在的我們隨便創(chuàng)建一個(gè)文件夾并取名為template里面創(chuàng)建一個(gè)html。

像這樣創(chuàng)建好后,我們就有了一個(gè)模板,但我們依然需要讓模板有一個(gè)可被下載、查詢的地方,這里我選擇的是使用 git 組織倉(cāng)庫(kù),因?yàn)檫@樣可以直接通過(guò)git提供的接口進(jìn)行文件下載,包括選擇不同的模板等。
上傳模板
我們先去 git 的官網(wǎng)中新建一個(gè)存放模板的組織倉(cāng)庫(kù)。

點(diǎn)擊圖中的位置進(jìn)入組織,并點(diǎn)擊下圖的創(chuàng)建

會(huì)進(jìn)入到付費(fèi)的位置,沒有大需求就選免費(fèi)

填寫信息完基本就算創(chuàng)建成功了

接下來(lái)在組織中創(chuàng)建一個(gè)儲(chǔ)存庫(kù)

這里我們暫且選擇可見的倉(cāng)庫(kù),千萬(wàn)不要選擇私人倉(cāng)庫(kù),否則git接口會(huì)找不該倉(cāng)庫(kù)

創(chuàng)建好后的倉(cāng)庫(kù),就直接將模板代碼提交至也本次創(chuàng)建的倉(cāng)庫(kù)中就可以了,我們?cè)?code>vscode中進(jìn)行演示。
先點(diǎn)擊推送

如果沒有推送的倉(cāng)庫(kù)則會(huì)提示是否添加推送倉(cāng)庫(kù),我們點(diǎn)擊推送遠(yuǎn)程倉(cāng)庫(kù),并從中找到自己的倉(cāng)庫(kù)


擇完成后輸入倉(cāng)庫(kù)名稱,然后會(huì)報(bào)錯(cuò),報(bào)錯(cuò)原因就是因?yàn)闀簾o(wú)推送的內(nèi)容,這個(gè)使用,正常的在 vscode 中提交代碼就行了,然后查看自己的倉(cāng)庫(kù),會(huì)出現(xiàn)上傳的內(nèi)容

增加一個(gè)新的版本標(biāo)簽
跟著下列圖操作




點(diǎn)擊發(fā)布發(fā)行版后就可以了。
下載模板
我們上傳模板后可以通過(guò) git 提供的接口來(lái)完成下載模板的功能,首先我們先去詢問(wèn)用戶要下載的模板名稱然后在用依賴包來(lái)進(jìn)行下載:
https://api.github.com/orgs/geeksTest/repos 獲取該組織下的所有模板
create命令后續(xù)操作
上傳模板后,我們就可以繼續(xù)完成create命令的后續(xù)操作了。
create命令下使用創(chuàng)建函數(shù)
program
.command('create <app-name>')
.description(chalk.cyan('create a new project'))
// -f or --force 為強(qiáng)制創(chuàng)建,如果創(chuàng)建的目錄存在則直接覆蓋
.option('-f, --force', 'overwrite target directory if it exist')
.action((name, options) => {
// 打印執(zhí)行結(jié)果
require("../lib/create")(name, options)
})
創(chuàng)建create文件
創(chuàng)建 create 文件用來(lái)回應(yīng)用戶的 create 命令。
這里用到的依賴
// lib/create.js
const path = require('path')
// fs-extra 是對(duì) fs 模塊的擴(kuò)展,支持 promise 語(yǔ)法
const fs = require('fs-extra')
// 用于交互式詢問(wèn)用戶問(wèn)題
const inquirer = require('inquirer')
// 導(dǎo)出Generator類
const Generator = require('./Generator')
//1. 拋出一個(gè)方法用來(lái)接收用戶要?jiǎng)?chuàng)建的文件夾(項(xiàng)目)名 和 其他參數(shù)
module.exports = async function (name, options) {
// 當(dāng)前命令行選擇的目錄
const cwd = process.cwd();
// 需要?jiǎng)?chuàng)建的目錄地址
const targetAir = path.join(cwd, name)
//2 判斷是否存在相同的文件夾(項(xiàng)目)名
// 目錄是否已經(jīng)存在?
if (fs.existsSync(targetAir)) {
// 是否為強(qiáng)制創(chuàng)建?
if (options.force) {
await fs.remove(targetAir)
} else {
// 詢問(wèn)用戶是否確定要覆蓋
let { action } = await inquirer.prompt([
{
name: 'action',
type: 'list',
message: 'Target directory already exists Pick an action:',
choices: [
{
name: 'Overwrite',
value: 'overwrite'
},{
name: 'Cancel',
value: false
}
]
}
])
// 如果用戶拒絕覆蓋則停止剩余操作
if (!action) {
return;
} else if (action === 'overwrite') {
// 移除已存在的目錄
console.log(`\r\nRemoving...`)
await fs.remove(targetAir)
}
}
}
//3 新建generator類
const generator = new Generator(name, targetAir);
generator.create();
}
創(chuàng)建generator類
// lib/Generator.js
const { getRepoList, getTagList } = require('./http')
const ora = require('ora')
const inquirer = require('inquirer')
const util = require('util')
const downloadGitRepo = require('download-git-repo') // 不支持 Promise
const chalk = require('chalk')
const path = require('path');
const fs = require("fs-extra");
// 添加加載動(dòng)畫
async function wrapLoading(fn, message, ...args) {
// 使用 ora 初始化,傳入提示信息 message
const spinner = ora(message);
// 開始加載動(dòng)畫
spinner.start();
try {
// 執(zhí)行傳入方法 fn
const result = await fn(...args);
// 狀態(tài)為修改為成功
spinner.succeed();
return result;
} catch (error) {
// 狀態(tài)為修改為失敗
spinner.fail('Request failed, refetch ...');
}
}
class Generator {
constructor (name, targetDir){
// 目錄名稱
this.name = name;
// 創(chuàng)建位置
this.targetDir = targetDir;
// 對(duì) download-git-repo 進(jìn)行 promise 化改造
this.downloadGitRepo = util.promisify(downloadGitRepo);
}
// 獲取用戶選擇的模板
// 1)從遠(yuǎn)程拉取模板數(shù)據(jù)
// 2)用戶選擇自己新下載的模板名稱
// 3)return 用戶選擇的名稱
async getRepo() {
// 1)從遠(yuǎn)程拉取模板數(shù)據(jù)
const repoList = await wrapLoading(getRepoList, 'waiting fetch template');
if (!repoList) return;
// 過(guò)濾我們需要的模板名稱
const repos = repoList.map(item => item.name);
// 2)用戶選擇自己新下載的模板名稱
const { repo } = await inquirer.prompt({
name: 'repo',
type: 'list',
choices: repos,
message: 'Please choose a template to create project'
})
// 3)return 用戶選擇的名稱
return repo;
}
// 獲取用戶選擇的版本
// 1)基于 repo 結(jié)果,遠(yuǎn)程拉取對(duì)應(yīng)的 tag 列表
// 2)自動(dòng)選擇最新版的 tag
async getTag(repo) {
// 1)基于 repo 結(jié)果,遠(yuǎn)程拉取對(duì)應(yīng)的 tag 列表
const tags = await wrapLoading(getTagList, 'waiting fetch tag', repo);
if (!tags) return;
// 過(guò)濾我們需要的 tag 名稱
const tagsList = tags.map(item => item.name);
// 2)return 用戶選擇的 tag
return tagsList[0]
}
// 下載遠(yuǎn)程模板
// 1)拼接下載地址
// 2)調(diào)用下載方法
async download(repo, tag){
// 1)拼接下載地址
const requestUrl = `geeksTest/${repo}${tag ? '#'+tag : ''}`;
// 2)調(diào)用下載方法
await wrapLoading(
this.downloadGitRepo, // 遠(yuǎn)程下載方法
'waiting download template', // 加載提示信息
requestUrl, // 參數(shù)1: 下載地址
path.resolve(process.cwd(), this.targetDir) // 參數(shù)2: 創(chuàng)建位置
)
}
// 核心創(chuàng)建邏輯
// 1)獲取模板名稱
// 2)獲取 tag 名稱
// 3)下載模板到模板目錄
// 4) 對(duì)uniapp模板中部分文件進(jìn)行讀寫
// 5) 模板使用提示
async create(){
// 1)獲取模板名稱
const repo = await this.getRepo()
// 2) 獲取 tag 名稱
const tag = await this.getTag(repo)
// 3)下載模板到模板目錄
await this.download(repo, tag)
// 5)模板使用提示
console.log(`\r\nSuccessfully created project ${chalk.cyan(this.name)}`)
console.log(`\r\n cd ${chalk.cyan(this.name)}`)
console.log(`\r\n 啟動(dòng)前請(qǐng)務(wù)必閱讀 ${chalk.cyan("README.md")} 文件`)
}
}
module.exports = Generator;
創(chuàng)建http文件
新建一個(gè)http.js的文件用來(lái)存放要請(qǐng)求的接口,我們用axios去請(qǐng)求.
依賴安裝
npm i commander
// lib/http.js
// 通過(guò) axios 處理請(qǐng)求
const axios = require('axios')
axios.interceptors.response.use(res => {
return res.data;
})
/**
* 獲取模板列表
* @returns Promise
*/
async function getRepoList() {
return axios.get('https://api.github.com/orgs/geeksTest/repos')
}
/**
* 獲取版本信息
* @param {string} repo 模板名稱
* @returns Promise
*/
async function getTagList(repo) {
return axios.get(`https://api.github.com/repos/geeksTest/${repo}/tags`)
}
module.exports = {
getRepoList,
getTagList
}
最后導(dǎo)出了兩個(gè)方法, 模板列表、模板tag列表。
這個(gè)時(shí)候的api接口是可以直接在瀏覽器中訪問(wèn)到的,如果不想被人隨意訪問(wèn)讀取數(shù)據(jù)則可以在git中增加雙因素驗(yàn)證,然后每次訪問(wèn)api時(shí)都會(huì)要求帶上git的訪問(wèn)token否則會(huì)訪問(wèn)不到,查看雙因素詳情
搭建完成
完成這一步后我們?cè)偃ミM(jìn)行test-cli create app命令,會(huì)看到下圖。

會(huì)詢問(wèn)要?jiǎng)?chuàng)建的模板項(xiàng)目,我這里的遠(yuǎn)程組織模板叫做test,大家選擇自己的模板回車,稍等一下就會(huì)創(chuàng)建成功,并看到在你使用命令的路徑上多出一個(gè)項(xiàng)目名的文件夾,就成功了。

如果有對(duì)模板在下載后進(jìn)行操作的需求可以使用fs依賴進(jìn)行操作,到這里為止我們已經(jīng)完成了一個(gè)簡(jiǎn)易的腳手架搭建,感謝大家耐心觀看。
到此這篇關(guān)于手把手教你從0搭建前端腳手架詳解的文章就介紹到這了,更多相關(guān)搭建前端腳手架內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Three.js引用和環(huán)境搭建過(guò)程詳解
這篇文章主要為大家介紹了Three.js引用和環(huán)境搭建過(guò)程詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-05-05
5種方法告訴你如何使JavaScript 代碼庫(kù)更干凈
J avaScript無(wú)處不在,從PC端到移動(dòng)設(shè)備端,甚至是后端,都在使用JavaSc ript。在本文中,將嘗試一些可用來(lái)使代碼看起來(lái)更簡(jiǎn)潔的實(shí)踐方案,希望能幫助到大家2021-09-09

