寫(xiě)給5年前端妹子的三萬(wàn)字腳手架教程
一接通,就傳來(lái)一陣哭泣聲:“師傅,我找不到工作,找了好久!”
我納悶地問(wèn)道:“你不是有5年前端工作經(jīng)驗(yàn),怎么會(huì)找不到工作?”
“師傅,你走了后,我就一直呆在這家公司,做的都是增刪改查業(yè)務(wù),面試時(shí)他們一聽(tīng)這些就不感興趣,我都不知道怎么介紹我的項(xiàng)目經(jīng)歷?!?/p>
到此我也大致明白了是怎么一回事,安慰了一會(huì)兒,告訴她回頭給你想想辦法,便掛斷電話了。
回到床上久久不能入眠,回想當(dāng)初自己第一次換工作時(shí)遇到過(guò)同樣的問(wèn)題。那時(shí)候工作剛滿三年,起初在面試中也是干巴巴地講述一些增刪改查的項(xiàng)目經(jīng)歷。隨著幾次面試的失敗,開(kāi)始講述一些復(fù)雜表單表格的開(kāi)發(fā)經(jīng)驗(yàn),僥幸跳槽加薪成功。
但是5年工作經(jīng)驗(yàn)還講這些就不適合了,這位5年的妹子應(yīng)該在面試中體現(xiàn)出擁有增刪改查業(yè)務(wù)的最佳實(shí)踐,加上5年前端應(yīng)該具備一些前端工程方面的能力。
于是,第二天打電話告訴妹子:“你先把5年增刪改查業(yè)務(wù)的開(kāi)發(fā)經(jīng)驗(yàn)歸納整理成一份最佳實(shí)踐的開(kāi)發(fā)模板,然后我教你寫(xiě)一個(gè)腳手架,把這些開(kāi)發(fā)模板放在腳手架中,其他項(xiàng)目通過(guò)腳手架來(lái)使用這些模板,提高開(kāi)發(fā)效率。”
妹子遲疑地問(wèn)道:“腳手架,一聽(tīng)就很難,好不好學(xué)呢?”
“放心,腳手架很簡(jiǎn)單的,腳手架其實(shí)是一個(gè)使用 js 編寫(xiě)的 Node.js 命令行工具(CLI),里面也都是js代碼,只不過(guò)用 Node.js 來(lái)執(zhí)行的,還用了一些node的API而已,你先去整理最佳實(shí)踐的模板,腳手架我教你寫(xiě),包教包會(huì)。”
你會(huì)學(xué)到什么
其實(shí)腳手架并不實(shí)現(xiàn),難的是最佳實(shí)踐的整理和沉淀。本文不會(huì)涉及到最佳實(shí)踐方面的內(nèi)容,只是教會(huì)你如何實(shí)現(xiàn)一個(gè)最基礎(chǔ)的腳手架,以此作為展示最佳實(shí)踐的載體。
因?yàn)槭鞘职咽纸堂米訉?xiě)腳手架,所以本文很詳細(xì),字?jǐn)?shù)很多,大概有3萬(wàn)字左右,讀完你會(huì)學(xué)到:
-
如何搭建一個(gè)腳手架的工程 -
如何開(kāi)發(fā)和調(diào)試一個(gè)腳手架 -
腳手架中如何接收和處理命令參數(shù) -
腳手架中如何和用戶交互 -
腳手架中如何拷貝一個(gè)文件夾或文件 -
腳手架中如何動(dòng)態(tài)生成一個(gè)文件 -
腳手架中如何處理路徑問(wèn)題 -
腳手架中如何自動(dòng)安裝模板所需依賴
本文中所有代碼已經(jīng)上傳到 GitHub[1]。
1、搭建一個(gè) monorepo 風(fēng)格的腳手架工程
1.1、如何用 Node.js 運(yùn)行 js
為了避免 Node.js 版本差異導(dǎo)致奇怪的問(wèn)題,建議安裝 16 以上版本的 Node.js 。
隨便找個(gè)地方建個(gè)一個(gè) index.js 文件,并在該文件中添加如下代碼:
console.log('Welcome to Mortal World');
在該文件的地址欄上輸出 cmd 打開(kāi)命令行窗口,輸入 node index.js 命令,回車運(yùn)行該命令。
可以在命令行窗口中看到打印了 Welcome to Mortals World。
1.2、聲明自己的命令
如果你熟悉 Vue , 肯定對(duì) vue-cli 這個(gè)腳手架有一定了解,比如運(yùn)行 vue create myapp 命令來(lái)創(chuàng)建一個(gè) Vue 工程。
如果我們沒(méi)有運(yùn)行 npm install \-g vue-cli 安裝 vue-cli 腳手架,在命令行窗口中直接運(yùn)行 vue create myapp,會(huì)報(bào)錯(cuò),報(bào)錯(cuò)如下圖所示:
可見(jiàn) vue 不是系統(tǒng)命令, vue 只是 vue-cli 腳手架聲明的一個(gè)命令。
那怎么給腳手架聲明一個(gè)命令,其實(shí)非常簡(jiǎn)單,跟我來(lái)操作。
隨便找個(gè)地方創(chuàng)建 mortal-cli 的文件夾,進(jìn)入該文件夾后,在其地址欄上輸出 cmd 打開(kāi)命令行窗口,
運(yùn)行 npm init 命令來(lái)初始化一個(gè)腳手架工程,運(yùn)行成功后,會(huì)在該文件夾中生成一個(gè) pakeage.json 文件。
我們?cè)?pakeage.json 中添加 bin 字段,來(lái)聲明一個(gè)命令,添加后的代碼如下所示:
{
"name": "mortal-cli",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"bin":{
"mortal": "./bin/index.js"
}
}
這樣我們就聲明了一個(gè) mortal 的命令,另外 ./bin/index.js 是運(yùn)行 mortal 命令后會(huì)運(yùn)行的 js 文件的相對(duì)路徑。
接著我們?cè)?mortal-cli 的文件夾中創(chuàng)建一個(gè) bin 文件夾,在 bin 文件夾中創(chuàng)建一個(gè) index.js 文件,并在該文件中添加如下代碼:
js
復(fù)制代碼
#!/usr/bin/env node console.log('Welcome to Mortal World');
注意在文件頭部添加
#!/usr/bin/env node,否則運(yùn)行后會(huì)報(bào)錯(cuò)!!!
這樣就完成了一個(gè)最基礎(chǔ)的腳手架工程,接下來(lái)在命令行窗口輸入 mortal 命令,運(yùn)行該命令。
會(huì)發(fā)現(xiàn),還是報(bào)錯(cuò),提示 mortal 命令不是系統(tǒng)命令,當(dāng)然不是聲明命令的方法錯(cuò)誤。
假如你把這個(gè)腳手架發(fā)布到 npm 上后。由于 mortal-cli/pakeage.json 中 name 的值為 mortal-cli,所以我們運(yùn)行 npm install \-g mortal-cli 將腳手架安裝到本地后,再運(yùn)行 mortal 命令,就會(huì)發(fā)現(xiàn)運(yùn)行成功。
在實(shí)際開(kāi)發(fā)腳手架過(guò)程中不可能這么做,所以我們還要實(shí)現(xiàn)本地調(diào)試腳手架的能力,實(shí)現(xiàn)起來(lái)也非常簡(jiǎn)單,一個(gè)命令搞定。
這個(gè)命令就是 npm link,哈哈想不到吧,在這里就不講述其原理,如果你們需要的話,可以留言,我開(kāi)個(gè)單章講述一下。
輸入 npm link 命令運(yùn)行后,再輸入 mortal 命令,回車運(yùn)行,看到的結(jié)果如下圖所示。
到此,我們成功的聲明了一個(gè) mortal 命令。
1.3、npm link 的弊端
使用 npm link 來(lái)實(shí)現(xiàn)本地調(diào)試有一個(gè)弊端。比如在本地有多個(gè)版本的腳手架倉(cāng)庫(kù),在倉(cāng)庫(kù)A中修改代碼后,運(yùn)行 mortal 命令后,發(fā)現(xiàn)更改的代碼不生效。這是因?yàn)橐呀?jīng)在倉(cāng)庫(kù)B的腳手架工程中運(yùn)行 npm link,導(dǎo)致我們?cè)谶\(yùn)行 mortal 命令后是執(zhí)行倉(cāng)庫(kù)B中的代碼,在倉(cāng)庫(kù)A中修改代碼能生效才怪。要先在倉(cāng)庫(kù)B的腳手架工程中運(yùn)行 npm unlink 后,然后在倉(cāng)庫(kù)A中的腳手架工程中運(yùn)行 npm link 后,修改倉(cāng)庫(kù)A中的代碼才能生效。
為了解決這個(gè)弊端,我們使用 pnpm 來(lái)搭建 monorepo 風(fēng)格的腳手架工程。
在 monorepo 風(fēng)格的工程中可以含有多個(gè)子工程,且每個(gè)子工程都可以獨(dú)立編譯打包后將產(chǎn)物發(fā)成 npm 包,故又稱 monorepo 為多包工程。
由于腳手架發(fā)布到 npm 上的包名為 mortal-cli ,修改調(diào)試子工程的 package.json 文件中的代碼,代碼修改部分如下所示:
{
"scripts": {
"mortal": "mortal --help"
},
"dependencies": {
"mortal-cli": "workspace:*"
}
}
注意,在 dependencies 字段中聲明 mortal-cli 依賴包的版本,要用workspace:* 來(lái)定義,而不是具體版本號(hào)來(lái)定義。
在 pnpm 中使用 workspace: 協(xié)議定義某個(gè)依賴包版本號(hào)時(shí),pnpm 將只解析存在工作空間內(nèi)的依賴包,不會(huì)去下載解析 npm 上的依賴包。
把 mortal-cli 依賴包引入,執(zhí)行 pnpm i 安裝依賴,其效果就跟執(zhí)行 npm install \-g mortal-cli一樣,只不過(guò)不是全局安裝而已,只在調(diào)試子工程內(nèi)安裝 mortal-cli 腳手架。然后調(diào)試子工程就直接引用腳手架子工程本地編譯打包后的產(chǎn)物,而不是發(fā)布到 npm 上的產(chǎn)物,徹底做到本地調(diào)試。
另外腳手架子工程和調(diào)試子工程是在同一個(gè)工程中,這樣就做一對(duì)一的調(diào)試,從而解決了使用 npm link 來(lái)實(shí)現(xiàn)本地調(diào)試的弊端。
同時(shí)在 scripts 定義了腳本命令,在調(diào)試工程中執(zhí)行 pnpm mortal 既是執(zhí)行了 mortal 命令,不用腳手架工程中執(zhí)行 npm link 就可以運(yùn)行 mortal 命令。
1.4、monorepo 風(fēng)格的腳手架工程
下面開(kāi)始使用 pnpm 搭建 monorepo 風(fēng)格的腳手架工程,首先在命令行窗口中輸入以下代碼,執(zhí)行安裝 pnpm 。
iwr https://get.pnpm.io/install.ps1 -useb | iex
然后重新找個(gè)地方創(chuàng)建 mortal 文件夾,進(jìn)入該文件夾后,在其地址欄上輸出 cmd 打開(kāi)命令行窗口。
輸入 pnpm init 初始化工程,pnpm 是使用 workspace (工作空間) 來(lái)搭建一個(gè) monorepo 風(fēng)格的工程。
所以我們要 mortal 文件夾中創(chuàng)建 pnpm-workspace.yaml 工作空間配置文件,并在該文件中添加如下配置代碼。
packages:
- 'packages/*'
- 'examples/*'
配置后,聲明了 packages 和 examples 文件夾中子工程是同屬一個(gè)工作空間的,工作空間中的子工程編譯打包的產(chǎn)物都可以被其它子工程引用。
在 packages 文件夾中新建 mortal-cli 文件夾,在其地址欄上輸出 cmd 打開(kāi)命令行窗口。
輸入 pnpm init 命令,執(zhí)行來(lái)初始化一個(gè)工程,執(zhí)行成功后,會(huì)在該文件夾中生成一個(gè) pakeage.json 文件。
我們?cè)?pakeage.json 中添加 bin 字段,來(lái)聲明 mortal 命令,添加后的代碼如下所示:
{
"name": "mortal-cli",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"bin":{
"mortal": "./bin/index.js"
}
}
在 packages/mortal-cli 文件夾中新建 bin 文件夾,在 bin 文件夾中新建 index.js 文件,并在該文件中添加如下代碼:
#!/usr/bin/env node
console.log('Welcome to Mortal World');
在 examples 文件夾中新建 app 文件夾,在其地址欄上輸出 cmd 打開(kāi)命令行窗口, 運(yùn)行 pnpm init 命令來(lái)初始化一個(gè)工程,運(yùn)行成功后,會(huì)在該文件夾中生成一個(gè) pakeage.json 文件。
我們?cè)?pakeage.json 中添加 dependencies 字段,來(lái)添加 mortal-cli 依賴。再給 scripts 增加一條自定義腳本命令。添加后的代碼如下所示:
{
"name": "app",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"mortal": "mortal"
},
"author": "",
"license": "ISC",
"dependencies": {
"mortal-cli": "workspace:*"
}
}
然后在最外層根目錄下運(yùn)行 pnpm i 命令,安裝依賴。安裝成功后,在 app 文件夾目錄下運(yùn)行 pnpm mortal,會(huì)發(fā)現(xiàn)命令行窗口打印出 Welcome to Mortal World,說(shuō)明你的 monorepo 風(fēng)格的腳手架工程的搭建成功了。
此時(shí)整個(gè)工程的目錄結(jié)構(gòu)如下所示
|-- mortal
|-- package.json
|-- pnpm-lock.yaml
|-- pnpm-workspace.yaml
|-- examples
| |-- app
| |-- package.json
|-- packages
|-- mortal-cli
|-- package.json
|-- bin
|-- index.js
1.5、腳手架必備的模塊
一個(gè)最簡(jiǎn)單的腳手架包含以下幾個(gè)模塊。
-
命令參數(shù)模塊 -
用戶交互模塊 -
文件拷貝模塊 -
動(dòng)態(tài)文件生成模塊 -
自動(dòng)安裝依賴模塊
下面我們來(lái)一一將他們實(shí)現(xiàn)。
2、命令參數(shù)模塊
2.1、獲取命令參數(shù)
Node.js 中的 process 模塊提供了當(dāng)前 Node.js 進(jìn)程相關(guān)的全局環(huán)境信息,比如命令參數(shù)、環(huán)境變量、命令運(yùn)行路徑等等。
const process = require('process');
// 獲取命令參數(shù)
console.log(process.argv);
腳手架提供的 mortal 命令后面還可以設(shè)置參數(shù),標(biāo)準(zhǔn)的腳手架命令參數(shù)需要支持兩種格式,比如:
mortal --name=orderPage
mortal --name orderPage
如果通過(guò) process.argv 來(lái)獲取,要額外處理兩種不同的命令參數(shù)格式,不方便。
這里推薦 yargs 開(kāi)源庫(kù)來(lái)解析命令參數(shù)。運(yùn)行以下命令安裝 yargs:
pnpm add yargs --F mortal-cli
pnpm add 是 pnpm 中安裝依賴包的命令, --F mortal-cli,是指定依賴安裝到 mortal-cli 子工程中。
這里要注意,
mortal-cli是取 mortal-cli 子工程中 package.json 中name字段的值,而不是 mortal-cli 子工程文件夾的名稱。
yargs 的使用非常簡(jiǎn)單,其提供的 argv 屬性是對(duì)兩個(gè)格式的命令參數(shù)的處理結(jié)果。
在 bin/index.js 添加如下代碼:
#!/usr/bin/env node
const yargs = require('yargs');
console.log('name', yargs.argv.name);
注意,以上代碼是在 Node.js 環(huán)境中運(yùn)行,Node.js 的模塊是遵循 CommonJS 規(guī)范的,如果要依賴一個(gè)模塊,要使用 Node.js 內(nèi)置
require系統(tǒng)函數(shù)引用模塊使用。
在 app 文件夾目錄下運(yùn)行 pnpm mortal \-- \--name=orderPage ,
注意,在
pnpm mortal后面需要加上兩個(gè)連字符(--),這是為了告訴 pnpm 后面的參數(shù)是傳遞給命令mortal本身的,而不是傳遞給pnpm的。
結(jié)果如下圖所示:
可以通過(guò) yargs.argv.name 獲取命令參數(shù) name 的值。
2.2、設(shè)置子命令
假如腳手架要對(duì)外提供多個(gè)功能,不能將所有的功能都集中在 mortal 命令中實(shí)現(xiàn)。
可以通過(guò) yargs 提供的 command 方法來(lái)設(shè)置一些子命令,讓每個(gè)子命令對(duì)應(yīng)各自功能,各司其職。
yargs.command 的用法是 **yargs.command(cmd, desc, builder, handler)**。
-
cmd:字符串,子命令名稱,也可以傳遞數(shù)組,如['create', 'c'],表示子命令叫create,其別名是c; -
desc:字符串,子命令描述信息; -
builder:一個(gè)返回?cái)?shù)組的函數(shù),子命令參數(shù)信息配置,比如可以設(shè)置參數(shù): -
alias:別名; -
demand:是否必填; -
default:默認(rèn)值; -
describe:描述信息; -
type:參數(shù)類型,string | boolean | number。 -
handler: 函數(shù),可以在這個(gè)函數(shù)中專門(mén)處理該子命令參數(shù)。
下面我們來(lái)設(shè)置一個(gè)用來(lái)生成一個(gè)模板的子命令,把這個(gè)子命令命名為create。
修改在 bin/index.js 文件中的代碼,如下所示:
#!/usr/bin/env node
const yargs = require('yargs');
yargs.command(
['create', 'c'],
'新建一個(gè)模板',
function (yargs) {
return yargs.option('name', {
alias: 'n',
demand: true,
describe: '模板名稱',
type: 'string'
})
},
function (argv) {
console.log('argv', argv);
}
).argv;
在 app 文件夾目錄下分別運(yùn)行 pnpm mortal create \-- \--name=orderPage 和 pnpm mortal c \-- \--name=orderPage 命令,執(zhí)行結(jié)果如下圖所示:
在上面我們配置了子命令 create 的參數(shù) name 的一些參數(shù)信息。那這些要怎么展示給用戶看呢?其實(shí)只要我們輸入子命令的參數(shù)有錯(cuò)誤,就會(huì)在命令行窗口中顯示這些參數(shù)信息。
在 app 文件夾目錄下運(yùn)行 pnpm mortal c \-- \--abc 命令,執(zhí)行結(jié)果如下圖所示:
到此為止,我們最簡(jiǎn)單地實(shí)現(xiàn)了腳手架和用戶之間的交互能力,但是如果自定義參數(shù)過(guò)多,那么命令行參數(shù)的交互方法對(duì)于用戶來(lái)說(shuō)是非常不友好的。所以我們還要實(shí)現(xiàn)一個(gè)用戶交互模塊,如何實(shí)現(xiàn)請(qǐng)看下一小節(jié)。
3、用戶交互模塊
我認(rèn)為比較好的用戶交互方式是訊問(wèn)式的交互,比如我們?cè)谶\(yùn)行 npm init,通過(guò)詢問(wèn)式的交互完成 package.json 文件內(nèi)容的填充。
這里推薦使用 inquirer 開(kāi)源庫(kù)來(lái)實(shí)現(xiàn)詢問(wèn)式的交互,運(yùn)行以下命令安裝 inquirer:
js
復(fù)制代碼
pnpm add [email protected] --F mortal-cli
為了使用 require 引入 inquirer ,要使用 8.2.5 版本的 inquirer。
這里我們主要使用了 inquirer 開(kāi)源庫(kù)的三個(gè)方面的能力:
-
詢問(wèn)用戶問(wèn)題 -
獲取并解析用戶的輸入 -
檢測(cè)用戶的答案是否合法
主要通過(guò) inquirer.prompt() 來(lái)實(shí)現(xiàn)。prompt 函數(shù)接收一個(gè)數(shù)組,數(shù)組的每一項(xiàng)都是一個(gè)詢問(wèn)項(xiàng),詢問(wèn)項(xiàng)有很多配置參數(shù),下面是常用的配置項(xiàng)。
-
type:提問(wèn)的類型,常用的有 -
輸入框: input; -
確認(rèn): confirm; -
單選組: list; -
多選組: checkbox; -
name:存儲(chǔ)當(dāng)前問(wèn)題答案的變量; -
message:?jiǎn)栴}的描述; -
default:默認(rèn)值; -
choices:列表選項(xiàng),在某些type下可用; -
validate:對(duì)用戶的答案進(jìn)行校驗(yàn); -
filter:對(duì)用戶的答案進(jìn)行過(guò)濾處理,返回處理后的值。
比如我們創(chuàng)建一個(gè)模板文件,大概會(huì)詢問(wèn)用戶:模板文件名稱、模板類型、使用什么框架開(kāi)發(fā)、使用框架對(duì)應(yīng)的哪個(gè)組件庫(kù)開(kāi)發(fā)等等。下面我們來(lái)實(shí)現(xiàn)這個(gè)功能。
在 bin 文件夾中新建 inquirer.js 文件夾,在里面添加如下代碼:
const inquirer = require('inquirer');
function inquirerPrompt(argv) {
const { name } = argv;
return new Promise((resolve, reject) => {
inquirer.prompt([
{
type: 'input',
name: 'name',
message: '模板名稱',
default: name,
validate: function (val) {
if (!/^[a-zA-Z]+$/.test(val)) {
return "模板名稱只能含有英文";
}
if (!/^[A-Z]/.test(val)) {
return "模板名稱首字母必須大寫(xiě)"
}
return true;
},
},
{
type: 'list',
name: 'type',
message: '模板類型',
choices: ['表單', '動(dòng)態(tài)表單', '嵌套表單'],
filter: function (value) {
return {
'表單': "form",
'動(dòng)態(tài)表單': "dynamicForm",
'嵌套表單': "nestedForm",
}[value];
},
},
{
type: 'list',
message: '使用什么框架開(kāi)發(fā)',
choices: ['react', 'vue'],
name: 'frame',
}
]).then(answers => {
const { frame } = answers;
if (frame === 'react') {
inquirer.prompt([
{
type: 'list',
message: '使用什么UI組件庫(kù)開(kāi)發(fā)',
choices: [
'Ant Design',
],
name: 'library',
}
]).then(answers1 => {
resolve({
...answers,
...answers1,
})
}).catch(error => {
reject(error)
})
}
if (frame === 'vue') {
inquirer.prompt([
{
type: 'list',
message: '使用什么UI組件庫(kù)開(kāi)發(fā)',
choices: [ 'Element'],
name: 'library',
}
]).then(answers2 => {
resolve({
...answers,
...answers2,
})
}).catch(error => {
reject(error)
})
}
}).catch(error => {
reject(error)
})
})
}
exports.inquirerPrompt = inquirerPrompt;
其中 inquirer.prompt() 返回的是一個(gè) Promise,我們可以用 then 獲取上個(gè)詢問(wèn)的答案,根據(jù)答案再發(fā)起對(duì)應(yīng)的內(nèi)容。
在 bin/index.js 中引入 inquirerPrompt 。
#!/usr/bin/env node
const yargs = require('yargs');
const { inquirerPrompt } = require("./inquirer");
yargs.command(
['create', 'c'],
'新建一個(gè)模板',
function (yargs) {
return yargs.option('name', {
alias: 'n',
demand: true,
describe: '模板名稱',
type: 'string'
})
},
function (argv) {
inquirerPrompt(argv).then(answers =>{
console.log(answers)
})
}
).argv;
在 app 文件夾目錄下運(yùn)行 pnpm mortal c \-- \--n Input 命令,執(zhí)行結(jié)果如下圖所示:
可以很清楚地看到“在使用什么框架開(kāi)發(fā)”的詢問(wèn)中回答不同,下一個(gè)“使用什么UI組件庫(kù)的開(kāi)發(fā)”的詢問(wèn)可選項(xiàng)不一樣。
回答完成后,可以在下圖中清楚地看到答案格式
4、文件夾拷貝模塊
要生成一個(gè)模板文件,最簡(jiǎn)單的做法就是執(zhí)行腳手架提供的命令后,把腳手架中的模板文件,拷貝到對(duì)應(yīng)的地方。模板文件可以是單個(gè)文件,也可以是一個(gè)文件夾。本小節(jié)先介紹一下模板文件是文件夾時(shí)候如何拷貝。
在 Node.js 中拷貝文件夾并不簡(jiǎn)單,需要用到遞歸,這里推薦使用開(kāi)源庫(kù)copy-dir來(lái)實(shí)現(xiàn)拷貝文件。
運(yùn)行以下命令安裝 copy-dir 。
pnpm add copy-dir --F mortal-cli
在 bin 文件夾中新建 copy.js 文件,在里面添加如下代碼:
const copydir = require('copy-dir');
const fs = require('fs');
function copyDir(from, to, options) {
copydir.sync(from, to, options);
}
function checkMkdirExists(path) {
return fs.existsSync(path)
};
exports.checkMkdirExists = checkMkdirExists;
exports.copyDir = copyDir;
copyDir 方法實(shí)現(xiàn)非常簡(jiǎn)單,難的是如何使用,下面創(chuàng)建一個(gè)場(chǎng)景來(lái)介紹一下如何使用。
我們?cè)?bin 文件夾中新建 template 文件夾,用來(lái)存放模板文件,比如在 template 文件夾中創(chuàng)建一個(gè) form 文件夾來(lái)存放表單模板,這里不介紹表單模板的內(nèi)容,我們隨意在 form 文件夾中創(chuàng)建一個(gè) _index.js_,在里面隨便寫(xiě)些內(nèi)容。其目錄結(jié)構(gòu)如下所示:
|-- mortal
|-- package.json
|-- pnpm-lock.yaml
|-- pnpm-workspace.yaml
|-- examples
| |-- app
| |-- package.json
|-- packages
|-- mortal
|-- package.json
|-- bin
|-- template
|-- form
|-- index.js
|-- copy.js
|-- index.js
|-- inquirer.js
下面來(lái)實(shí)現(xiàn)把 packages/mortal/bin/template/form 這個(gè)文件夾拷貝到 examples/app/src/pages/OrderPage 中 。
在 bin/index.js 修改代碼,修改后的代碼如下所示:
#!/usr/bin/env node
const yargs = require('yargs');
const path = require('path');
const { inquirerPrompt } = require("./inquirer");
const { copyDir, checkMkdirExists } = require("./copy");
yargs.command(
['create', 'c'],
'新建一個(gè)模板',
function (yargs) {
return yargs.option('name', {
alias: 'n',
demand: true,
describe: '模板名稱',
type: 'string'
})
},
function (argv) {
inquirerPrompt(argv).then(answers => {
const { name, type } = answers;
const isMkdirExists = checkMkdirExists(
path.resolve(process.cwd(),`./src/pages/${name}`)
);
if (isMkdirExists) {
console.log(`${name}文件夾已經(jīng)存在`)
} else {
copyDir(
path.resolve(__dirname, `./template/${type}`),
path.resolve(process.cwd(), `./src/pages/${name}`)
)
}
})
}
).argv;
使用拷貝文件方法 copyDir 的難點(diǎn)是參數(shù) from 和 to 的賦值。其中 from 表示要拷貝文件的路徑,to 表示要把文件拷貝到那里的路徑。這里的路徑最好使用絕對(duì)路徑,因?yàn)樵?Node.js 中使用相對(duì)路徑會(huì)出現(xiàn)一系列奇奇怪怪的問(wèn)題。
4.1、腳手架中的路徑處理
我們可以用 Node.js 中的 path 模塊提供的 path.resolve( [from…], to ) 方法將路徑轉(zhuǎn)成絕對(duì)路徑,就是將參數(shù) to 拼接成一個(gè)絕對(duì)路徑,[from … ] 為選填項(xiàng),可以設(shè)置多個(gè)路徑,如 path.resolve('./aaa', './bbb', './ccc') ,使用時(shí)要注意path.resolve 的路徑拼接規(guī)則:
-
從后向前拼接路徑; -
若 to以/開(kāi)頭,不會(huì)拼接到前面的路徑; -
若 to以../開(kāi)頭,拼接前面的路徑,且不含最后一節(jié)路徑; -
若 to以./開(kāi)頭或者沒(méi)有符號(hào),則拼接前面路徑。
從以上拼接規(guī)則來(lái)看,使用 path.resolve 時(shí),要特別注意參數(shù) to 的設(shè)置。
下面來(lái)介紹一下,使用 copyDir 方法時(shí),參數(shù)如何設(shè)置:
-
將 copyDir的參數(shù)from設(shè)置為path.resolve(__dirname, `./template/${type}`),
其中 __dirname 是用來(lái)動(dòng)態(tài)獲取當(dāng)前文件模塊所屬目錄的絕對(duì)路徑。比如在 bin/index.js 文件中使用 __dirname ,__dirname 表示就是 bin/index.js 文件所屬目錄的絕對(duì)路徑 D:\mortal\packages\mortal-cli\bin。
因?yàn)槟0逦募娣旁?bin/template 文件夾中 ,copyDir 是在 bin/index.js 中使用,_bin/template_ 文件夾相對(duì) bin/index.js 文件的路徑是 ./template,所以把 path.resolve 的參數(shù) to 設(shè)置為 ./template/${type},其中 type 是用戶所選的模板類型。
假設(shè) type 的模板類型是 form,那么 path.resolve(__dirname, `./template/form`) 得到的絕對(duì)路徑是 D:\mortal\packages\mortal-cli\bin\template\form。
-
將 copyDir的參數(shù)to設(shè)置為path.resolve(process.cwd(), `${name}`),
其中 process.cwd() 當(dāng)前 Node.js 進(jìn)程執(zhí)行時(shí)的文件所屬目錄的絕對(duì)路徑。比如在 bin 文件夾目錄下運(yùn)行 node index.js 時(shí),process.cwd() 得到的是 D:\mortal\packages\mortal-cli\bin。
運(yùn)行 node index.js 相當(dāng)運(yùn)行 mortal 命令。而在現(xiàn)代前端工程中都是在 package.json 文件中scripts 定義了腳本命令,如下所示:
{
"name": "app",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"mortal": "mortal"
},
"author": "",
"license": "ISC",
"dependencies": {
"mortal-cli": "workspace:*"
}
}
運(yùn)行 pnpm mortal 就相當(dāng)運(yùn)行 mortal 命令,那么執(zhí)行 pnpm mortal 時(shí),當(dāng)前 Node.js 進(jìn)程執(zhí)行時(shí)的文件是 package.json 文件。那么 process.cwd() 得到的是 D:\mortal\examples\app。
因?yàn)橐?packages/mortal/bin/template/form 這個(gè)文件夾拷貝到 examples/app/src/pages/OrderPage 中,且 process.cwd() 的值是D:\mortal\examples\app,_src/pages_ 文件夾相對(duì) examples/app 的路徑是 ./src/pages ,所以把 path.resolve 的參數(shù) to 設(shè)置為 ./src/pages/${name},其中 name 是用戶所輸入的模板名稱。
4.2、目錄守衛(wèi)
在 app 文件夾目錄下運(yùn)行 pnpm mortal create \-- \--name=OrderPage,看能不能成功得把 packages/mortal/bin/template/form 這個(gè)文件夾拷貝到 examples/app/src/pages/OrderPage 中。
報(bào)錯(cuò)了,提示 examples/app/src/pages 文件夾不存在。為了防止這種報(bào)錯(cuò)出現(xiàn),我們要實(shí)現(xiàn)一個(gè)目錄守護(hù)的方法 mkdirGuard ,比如 examples/app/src/pages 文件夾不存在,就創(chuàng)建一個(gè) examples/app/src/pages 文件夾。
在 bin/copy.js 文件中,修改代碼,如下所示:
const copydir = require('copy-dir');
const fs = require('fs');
const path = require('path');
function mkdirGuard(target) {
try {
fs.mkdirSync(target, { recursive: true });
} catch (e) {
mkdirp(target)
function mkdirp(dir) {
if (fs.existsSync(dir)) { return true }
const dirname = path.dirname(dir);
mkdirp(dirname);
fs.mkdirSync(dir);
}
}
}
function copyDir(form, to, options) {
mkdirGuard(to);
copydir.sync(form, to, options);
}
function checkMkdirExists(path) {
return fs.existsSync(path)
};
exports.checkMkdirExists = checkMkdirExists;
exports.mkdirGuard = mkdirGuard;
exports.copyDir = copyDir;
fs.mkdirSync 的語(yǔ)法格式:fs.mkdirSync(path[, options]),創(chuàng)建文件夾目錄。
-
path:文件夾目錄路徑; -
options:recursive表示是否要?jiǎng)?chuàng)建父目錄,true要。
fs.existsSync 的語(yǔ)法格式:fs.existsSync(pach),檢測(cè)目錄是否存在,如果目錄存在返回 true ,如果目錄不存在返回false。
-
path:文件夾目錄路徑。
path.dirname 的語(yǔ)法格式:path.dirname(path),用于獲取給定路徑的目錄名。
-
path:文件路徑。
在 mkdirGuard 方法內(nèi)部,當(dāng)要?jiǎng)?chuàng)建的目錄 target 父級(jí)目錄不存在時(shí),調(diào)用fs.mkdirSync(target),會(huì)報(bào)錯(cuò)走 catch 部分邏輯,在其中遞歸創(chuàng)建父級(jí)目錄,使用 fs.existsSync(dir) 來(lái)判斷父級(jí)目錄是否存在,來(lái)終止遞歸。這里要特別注意 fs.mkdirSync(dir) 創(chuàng)建父級(jí)目錄要在 mkdirp(dirname) 之前調(diào)用,才能形成一個(gè)正確的創(chuàng)建順序,否則創(chuàng)建父級(jí)目錄過(guò)程會(huì)因父級(jí)目錄的父級(jí)目錄不存在報(bào)錯(cuò)。
我們?cè)俅卧?app 文件夾目錄下運(yùn)行 pnpm mortal create \-- \--name=OrderPage,看這次能不能成功得把 packages/mortal/bin/template/form 這個(gè)文件夾拷貝到 examples/app/src/pages/OrderPage 中。
成功添加,添加結(jié)果如下所示:
然后再運(yùn)行 pnpm mortal create \-- \--name=OrderPage 命令,會(huì)發(fā)現(xiàn)控制臺(tái)打印出模板已經(jīng)存在在提示。
這是為了防止用戶修改后的模板文件,運(yùn)行命令后被重新覆蓋到初始狀態(tài)。所以我們引入一個(gè)校驗(yàn)?zāi)0逦募欠翊嬖诘?checkMkdirExists 方法,內(nèi)部采用 fs.existsSync 來(lái)實(shí)現(xiàn)。
5、文件拷貝模塊
文件拷貝分三步來(lái)實(shí)現(xiàn),使用 fs.readFileSync 讀取被拷貝的文件內(nèi)容,然后創(chuàng)建一個(gè)文件,再使用 fs.writeFileSync 寫(xiě)入文件內(nèi)容。
在 bin/copy.js 文件,在里面添加如下代碼:
function copyFile(from, to) {
const buffer = fs.readFileSync(from);
const parentPath = path.dirname(to);
mkdirGuard(parentPath)
fs.writeFileSync(to, buffer);
}
exports.copyFile = copyFile;
接下來(lái)我們使用 copyFile 方法,在 bin/index.js 修改代碼,修改后的代碼如下所示:
#!/usr/bin/env node
const yargs = require('yargs');
const path = require('path');
const { inquirerPrompt } = require("./inquirer");
const { copyDir } = require("./copy");
yargs.command(
['create', 'c'],
'新建一個(gè)模板',
function (yargs) {
return yargs.option('name', {
alias: 'n',
demand: true,
describe: '模板名稱',
type: 'string'
})
},
function (argv) {
inquirerPrompt(argv).then(answers => {
const { name, type } = answers;
const isMkdirExists = checkMkdirExists(
path.resolve(process.cwd(),`./src/pages/${name}/index.js`)
);
if (isMkdirExists) {
console.log(`${name}/index.js文件已經(jīng)存在`)
} else {
copyFile(
path.resolve(__dirname, `./template/${type}/index.js`),
path.resolve(process.cwd(), `./src/pages/${name}/index.js`),
{
name,
}
)
}
})
}
).argv;
copyFile 和 copyDir 使用的區(qū)別在參數(shù),copyFile 要求參數(shù) from 和參數(shù) to 都精確到文件路徑。
在 app 文件夾目錄下運(yùn)行 pnpm mortal create \-- \--name=OrderPage,執(zhí)行結(jié)果如下圖所示:
6、動(dòng)態(tài)文件生成模塊
假設(shè)腳手架中提供的模板文件中某些信息需要根據(jù)用戶輸入的命令參數(shù)來(lái)動(dòng)態(tài)生成對(duì)應(yīng)的模板文件。
比如下面模板文件中 App 要?jiǎng)討B(tài)替換成用戶輸入的命令參數(shù) name 的值,該如何實(shí)現(xiàn)呢?
import React from 'react';
const App = () => {
return (
<div></div>
);
};
export default App;
這里推薦使用開(kāi)源庫(kù)mustache來(lái)實(shí)現(xiàn),運(yùn)行以下命令安裝 copy-dir 。
pnpm add mustache --F mortal-cli
我們?cè)?packages/mortal-cli/bin/template/form 文件夾中創(chuàng)建一個(gè) index.tpl 文件,內(nèi)容如下:
import React from 'react';
const {{name}} = () => {
return (
<div></div>
);
};
export default {{name}};
先寫(xiě)一個(gè) readTemplate 方法來(lái)讀取這個(gè) index.tpl 動(dòng)態(tài)模板文件內(nèi)容。在 bin/copy.js 文件,在里面添加如下代碼:
const Mustache = require('mustache');
function readTemplate(path, data = {}) {
const str = fs.readFileSync(path, { encoding: 'utf8' })
return Mustache.render(str, data);
}
exports.readTemplate = readTemplate;
readTemplate 方法接收兩個(gè)參數(shù),path 動(dòng)態(tài)模板文件的相對(duì)路徑,data 動(dòng)態(tài)模板文件的配置數(shù)據(jù)。
使用 Mustache.render(str, data) 生成模板文件內(nèi)容返回,因?yàn)?Mustache.render 的第一個(gè)參數(shù)類型是個(gè)字符串,所以在調(diào)用 fs.readFileSync 時(shí)要指定 encoding 類型為 utf8,否則 fs.readFileSync 返回 Buffer 類型數(shù)據(jù)。
在寫(xiě)一個(gè) copyTemplate 方法來(lái)拷貝模板文件到對(duì)應(yīng)的地方,跟 copyFile 方法非常相似。在 bin/copy.js 文件,在里面添加如下代碼:
function copyTemplate(from, to, data = {}) {
if (path.extname(from) !== '.tpl') {
return copyFile(from, to);
}
const parentToPath = path.dirname(to);
mkdirGuard(parentToPath);
fs.writeFileSync(to, readTemplate(from, data));
}
path.extname(from) 返回文件擴(kuò)展名,比如 path.extname(index.tpl) 返回 .tpl。
在 bin/index.js 修改代碼,修改后的代碼如下所示:
#!/usr/bin/env node
const yargs = require('yargs');
const path = require('path');
const { inquirerPrompt } = require("./inquirer");
const { copyTemplate } = require("./copy");
yargs.command(
['create', 'c'],
'新建一個(gè)模板',
function (yargs) {
return yargs.option('name', {
alias: 'n',
demand: true,
describe: '模板名稱',
type: 'string'
})
},
function (argv) {
inquirerPrompt(argv).then(answers => {
const { name, type } = answers;
const isMkdirExists = checkMkdirExists(
path.resolve(process.cwd(),`./src/pages/${name}/index.js`)
);
if (isMkdirExists) {
console.log(`${name}/index.js文件已經(jīng)存在`)
} else {
copyTemplate(
path.resolve(__dirname, `./template/${type}/index.tpl`),
path.resolve(process.cwd(), `./src/pages/${name}/index.js`),
{
name,
}
)
}
})
}
).argv;
在 app 文件夾目錄下運(yùn)行 pnpm mortal create \-- \--name=OrderPage,執(zhí)行結(jié)果如下圖所示:
6.1、mustache 簡(jiǎn)介
以上的案例是 mustache 最簡(jiǎn)單的使用,下面來(lái)額外介紹一些常用的使用場(chǎng)景。
首先來(lái)熟悉一下 mustache 的語(yǔ)法,下面來(lái)介紹一些場(chǎng)景來(lái)使用這些語(yǔ)法
-
{{key}} -
{{#key}} {{/key}} -
{{^key}} {{/key}} -
{{.}} -
{{&key}}
6.1.1、簡(jiǎn)單綁定
使用 {{key}} 語(yǔ)法,key 要和 Mustache.render 方法中的第二個(gè)參數(shù)(一個(gè)對(duì)象)的屬性名一致。
例如:
Mustache.render('<span>{{name}}</span>',{name:'張三'})
輸出:
<span>張三</span>
6.1.2、綁定子屬性
例如:
Mustache.render('<span>{{ifno.name}}</span>', { ifno: { name: '張三' } })
輸出:
<span>張三</span>
6.1.3、循環(huán)渲染
如果 key 屬性值是一個(gè)數(shù)組,則可以使用 {{#key}} {{/key}} 語(yǔ)法來(lái)循環(huán)展示。其中 {{#}} 標(biāo)記表示從該標(biāo)記以后的內(nèi)容全部都要循環(huán)展示,{{/}}標(biāo)記表示循環(huán)結(jié)束。
例如:
Mustache.render(
'<span>{{#list}}{{name}}{{/list}}</span>',
{
list: [
{ name: '張三' },
{ name: '李四' },
{ name: '王五' },
]
}
)
輸出:
<span>張三李四王五</span>
如果 list 的值是 ['張三','李四','王五'],要把 {{name}} 替換成 {{.}} 才可以渲染。
Mustache.render(
'<span>{{#list}}{{.}}{{/list}}</span>',
{
list: ['張三','李四','王五']
}
)
6.1.4、循環(huán)中二次處理數(shù)據(jù)
Mustache.render 方法中的第二個(gè)參數(shù)是個(gè)對(duì)象,其屬性值可以是一個(gè)函數(shù),渲染時(shí)候會(huì)執(zhí)行函數(shù)輸出返回值,函數(shù)中可以用 this 獲取第二個(gè)參數(shù)的上下文。
例如:
Mustache.render(
'<span>{{#list}}{{info}}{{/list}}</span>',
{
list: [
{ name: '張三' },
{ name: '李四' },
{ name: '王五' },
],
info() {
return this.name + ',';
}
}
)
輸出:
<span>張三,李四,王五,</span>
6.1.5、條件渲染
使用 {{#key}} {{/key}} 語(yǔ)法 和 {{^key}} {{/key}} 語(yǔ)法來(lái)實(shí)現(xiàn)條件渲染,當(dāng) key 為 false、0、[]、{}、null,既是 key == false 為真,{{#key}} {{/key}} 包裹的內(nèi)容不渲染,{{^key}} {{/key}} 包裹的內(nèi)容渲染
例如:
Mustache.render(
'<span>{{#show}}顯示{{/show}}{{^show}}隱藏{{/show}}</span>',
{
show: false
}
)
輸出:
<span>隱藏</span>
6.1.6、不轉(zhuǎn)義 HTML 標(biāo)簽
使用 {{&key}} 語(yǔ)法來(lái)實(shí)現(xiàn)。
例如:
Mustache.render(
'<span>{{&key}}</span>',
{
key: '<span>標(biāo)題</span>'
}
)
輸出:
<span><span>標(biāo)題</span></span>
7、自動(dòng)安裝依賴模塊
假設(shè)模板是這樣的:
import React from 'react';
import { Button, Form, Input } from 'antd';
const App = () => {
const onFinish = (values) => {
console.log('Success:', values);
};
return (
<Form onFinish={onFinish} autoComplete="off">
<Form.Item label="Username" name="username">
<Input />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit">提交</Button>
</Form.Item>
</Form>
);
};
export default App;
可以看到模板中使用了 react 和 antd 這兩個(gè)第三方依賴,假如使用模板的工程中沒(méi)有安裝這兩個(gè)依賴,我們要實(shí)現(xiàn)在生成模板過(guò)程中就自動(dòng)安裝這兩個(gè)依賴。
我們使用 Node 中 child_process 子進(jìn)程這個(gè)模塊來(lái)實(shí)現(xiàn)。
在 child_process 子進(jìn)程中的最常用的語(yǔ)法是:
child_process.exec(command, options, callback)
-
command:命令,比如pnpm install -
options:參數(shù) -
cwd:設(shè)置命令運(yùn)行環(huán)境的路徑 -
env:環(huán)境變量 -
timeout:運(yùn)行執(zhí)行現(xiàn)在 -
callback:運(yùn)行命令結(jié)束回調(diào),(error, stdout, stderr) =>{ },執(zhí)行成功后error為null,執(zhí)行失敗后error為 Error 實(shí)例,stdout、stderr為標(biāo)準(zhǔn)輸出、標(biāo)準(zhǔn)錯(cuò)誤,其格式默認(rèn)是字符串。
在 bin 文件夾中新建 inquirer.js 文件夾,在里面添加如下代碼:
const path = require('path');
const { exec } = require('child_process');
const LibraryMap = {
'Ant Design': 'antd',
'iView': 'view-ui-plus',
'Ant Design Vue': 'ant-design-vue',
'Element': 'element-plus',
}
function install(cmdPath, options) {
const { frame, library } = options;
const command = `pnpm add ${frame} && pnpm add ${LibraryMap[library]}`
return new Promise(function (resolve, reject) {
exec(
command,
{
cwd: path.resolve(cmdPath),
},
function (error, stdout, stderr) {
console.log('error', error);
console.log('stdout', stdout);
console.log('stderr', stderr)
}
)
})
}
exports.install = install;
在 install 方法中 exec 的參數(shù) command 是 pnpm 安裝依賴命令,安裝多個(gè)依賴時(shí)使用 && 拼接。參數(shù) cwd 是所安裝依賴工程的 package.json 文件路徑,我們可以使用 process.cwd() 獲取。已經(jīng)在上文提到過(guò),process.cwd() 是當(dāng)前Node.js 進(jìn)程執(zhí)行時(shí)的文件所屬目錄的絕對(duì)路徑。
接下來(lái)使用,在 bin/index.js 修改代碼,修改后的代碼如下所示:
#!/usr/bin/env node
const yargs = require('yargs');
const path = require('path');
const { inquirerPrompt } = require("./inquirer");
const { copyTemplate, checkMkdirExists } = require("./copy");
const { install } = require('./manager');
yargs.command(
['create', 'c'],
'新建一個(gè)模板',
function (yargs) {
return yargs.option('name', {
alias: 'n',
demand: true,
describe: '模板名稱',
type: 'string'
})
},
function (argv) {
inquirerPrompt(argv).then(answers => {
const { name, type } = answers;
const isMkdirExists = checkMkdirExists(
path.resolve(process.cwd(),`./src/pages/${name}/index.js`)
);
if (isMkdirExists) {
console.log(`${name}/index.js文件已經(jīng)存在`)
} else {
copyTemplate(
path.resolve(__dirname, `./template/${type}/index.tpl`),
path.resolve(process.cwd(), `./src/pages/${name}/index.js`),
{
name,
}
)
install(process.cwd(), answers);
}
})
}
).argv;
當(dāng)執(zhí)行完 copyTemplate 方法后,就開(kāi)始執(zhí)行 install(process.cwd(), answers) 自動(dòng)安裝模板中所需的依賴。
在 app 文件夾目錄下運(yùn)行 pnpm mortal create \-- \--name=OrderPage,看能不能自動(dòng)安裝依賴。
等命令執(zhí)行完成后,觀察 examples\app\package.json 文件中的 dependencies 值是不是添加了 antd 和 react 依賴。
此外,我們?cè)趫?zhí)行命令中會(huì)發(fā)現(xiàn),如下圖所示的現(xiàn)象,光標(biāo)一直在閃爍,好像卡住了,其中是依賴在安裝。這里我們要引入一個(gè)加載動(dòng)畫(huà),來(lái)解決這個(gè)不友好的現(xiàn)象。
這里推薦使用開(kāi)源庫(kù)ora來(lái)實(shí)現(xiàn)加載動(dòng)畫(huà)。
運(yùn)行以下命令安裝 ora 。
pnpm add [email protected] --F mortal-cli
在 bin/inquirer.js 修改代碼,修改后的代碼如下所示:
const path = require('path');
const { exec } = require('child_process');
const ora = require("ora");
const LibraryMap = {
'Ant Design': 'antd',
'iView': 'view-ui-plus',
'Ant Design Vue': 'ant-design-vue',
'Element': 'element-plus',
}
function install(cmdPath, options) {
const { frame, library } = options;
const command = `pnpm add ${frame} && pnpm add ${LibraryMap[library]}`
return new Promise(function (resolve, reject) {
const spinner = ora();
spinner.start(
`正在安裝依賴,請(qǐng)稍等`
);
exec(
command,
{
cwd: path.resolve(cmdPath),
},
function (error) {
if (error) {
reject();
spinner.fail(`依賴安裝失敗`);
return;
}
spinner.succeed(`依賴安裝成功`);
resolve()
}
)
})
}
exports.install = install;
在 app 文件夾目錄下運(yùn)行 pnpm mortal create \-- \--name=OrderPage,看一下執(zhí)行效果。
8、發(fā)布和安裝
在 packages/mortal 文件夾目錄下運(yùn)行,運(yùn)行以下命令安裝將腳手架發(fā)布到 npm 上。
js
復(fù)制代碼
pnpm publish --F mortal-cli
發(fā)布成功后。我們?cè)谝粋€(gè)任意工程中,執(zhí)行 pnpm add mortal-cli \-D 安裝 mortal-cli 腳手架依賴成功后,在工程中執(zhí)行 pnpm mortal create \-- \--name=OrderPage 命令即可。
結(jié)語(yǔ)
上面只教大家實(shí)現(xiàn)一個(gè)最最簡(jiǎn)單的腳手架。其功能就只有一個(gè)模板文件生成。雖然簡(jiǎn)單,但是這些都是腳手架的入門(mén)功,代碼已經(jīng)上傳到 GitHub[2],大家可以下載下來(lái),自己實(shí)踐一下,光看不練永遠(yuǎn)學(xué)不會(huì)。
學(xué)會(huì)了,可以總結(jié)一些平時(shí)的業(yè)務(wù)代碼,形成最佳實(shí)踐,使用腳手架作為載體展現(xiàn)出來(lái),提升自己的職場(chǎng)競(jìng)爭(zhēng)力。
8、參考資料
https://github.com/532pyh/mortal-cli: https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2F532pyh%2Fmortal-cli
[2]https://github.com/532pyh/mortal-cli: https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2F532pyh%2Fmortal-cli
關(guān)于本文
作者:紅塵煉心
https://juejin.cn/post/7260144602471776311
