來源 | https://juejin.cn/post/6899446879487180808
基本上,使用任何成熟的語(yǔ)言都可以開發(fā) cli 工具,作為一個(gè)前端小白,還是 JavaScript 比較順手,因此我們選用nodejs作為開發(fā)語(yǔ)言,開發(fā)一款node-cli工具。類似于腳手架工具,Node工具會(huì)自動(dòng)去詢問你一些預(yù)設(shè)的問題,然后將你回答的結(jié)果結(jié)合一些模板文件,給你生成一個(gè)項(xiàng)目結(jié)構(gòu)。那接下來我們以一個(gè)小型的腳手架工具為例,通過Nodejs完成一個(gè)Node工具,再來去深入體會(huì)一下Node工具的工作過程。那我們知道腳手架工具實(shí)際上就是一個(gè)node-cli應(yīng)用,那創(chuàng)建腳手架就是創(chuàng)建一個(gè)node-cli應(yīng)用,那這里我們具體來操作一下,我們首先進(jìn)入到命令行,通過mkdir去創(chuàng)建一個(gè)工具目錄。mkdir samlpe-clicd sample-cli
在這個(gè)目錄下面我們通過yarn init 方式去初始化一個(gè)package.json文件有了這個(gè)文件之后通過編輯器打開這個(gè)目錄,緊接著我們需要在package.json中添加一個(gè)bin字段,用于去指定一下我們cli應(yīng)用的入口文件, 我們這里叫cli.js{ "name": "sample-cli", "bin": "cli.js", ...}
再然后我們添加這個(gè)cli.js文件,跟以往我們?cè)贜ode中書寫的文件有所不同,cli的入口文件必須要有一個(gè)特定的文件頭, 也就是在這個(gè)文件頂部寫上這樣一句話 #! /usr/bin/env node 我們?cè)谶@個(gè)文件中console.log一句話。#! /usr/bin/env node
console.log('cli working')
如果說你的操作系統(tǒng)是linux或者mac系統(tǒng)你還還需要去修改這個(gè)文件的讀寫權(quán)限,把他修改成755,這樣才可以作為一個(gè)cli的入口文件。我們回到命令行,我們通過yarn link 將這個(gè)模塊映射到全局這時(shí)候我們就可以在命令行使用sample這樣一個(gè)命令, 通過執(zhí)行這個(gè)命令我們的console.log成功打印出來,表示代碼執(zhí)行了。也就意味著我們這個(gè)cli基礎(chǔ)就已經(jīng)ok了。接下來我們實(shí)現(xiàn)一下腳手架的具體業(yè)務(wù),也就是我們腳手架的工作過程。首先我們需要通過命令行交互的的方式去詢問用戶的一些信息,然后緊接著呢根據(jù)用戶反饋回來的結(jié)果我們?nèi)ド晌募?/span>通過命令行交互的方式詢問用戶信息
根據(jù)用戶反饋回來的結(jié)果生成文件
在Node當(dāng)中去發(fā)起命令行交互詢問我們使用inquirer這樣一個(gè)模塊,那我們需要通過npm安裝一下這個(gè)模塊,我這里使用yarn,安裝在依賴文件當(dāng)中。那有了這個(gè)模塊過后就可以在代碼中去載入, inquirer這個(gè)模塊提供一個(gè)叫做prompt的方法用于發(fā)起一個(gè)命令行的詢問。他可以接收一個(gè)數(shù)組參數(shù),數(shù)組中每一個(gè)成員就是一個(gè)問題,可以通過type指定問題輸入方式,然后name指定返回值的鍵,message去指定屏幕上給用戶的一個(gè)提示,在promise的then里面拿到這個(gè)問題接收到用戶的答案。我們這里不著急往下寫,我們先通過console.log去打印一下。const inquirer = require('inquirer');
inquirer.prompt([ { type: 'input', name: 'name', message: 'Project name' }]).then(answer => { console.log(answer);})
回到控制臺(tái),我們命令行執(zhí)行sample-cli, 此時(shí)就會(huì)提示我們需要輸入項(xiàng)目的名稱。這樣就可以看到問題和返回的結(jié)果。這也就證明inquirer確實(shí)可以幫我們發(fā)起命令行交互詢問。那有了inquirer之后接下來我們要考慮的就是動(dòng)態(tài)的去生成我們的項(xiàng)目文件。我們一般會(huì)根據(jù)模板去生成,所以我們?cè)陧?xiàng)目的跟目錄下新建一個(gè)templates目錄,在這個(gè)目錄下我們?nèi)バ陆ㄒ恍┠0濉?/span>由于我們這里是討論腳手架的工作過程,所以我們也不去關(guān)心模板里面有什么,我們就隨便寫點(diǎn)什么。我們可以通過 <%%>去替換詢問過程中得到的答案。我們還可以添加一些其他的模板文件,比如style.cssbody { margin: 0; background-color: red;}
回到cli.js文件, 這時(shí)候我們可以在得到問題答案的位置,根據(jù)用戶回答的問題去生成文件。不過在生成前我們一般會(huì)先將模板路徑和目標(biāo)目錄確定下來。模板的目錄應(yīng)該是項(xiàng)目當(dāng)前目錄的templates,我們可以通過path獲取。const path = require('path');
// 工具當(dāng)前目錄const tmplDir = path.join(__dirname, 'templates');
輸出的目標(biāo)目錄一般是我們命令行在哪個(gè)目錄去執(zhí)行就應(yīng)該是哪個(gè)路徑,也就是cwd目錄const path = require('path');
// 工具當(dāng)前目錄const tmplDir = path.join(__dirname, 'templates');// 命令行所在目錄const destDir = process.cwd();
明確這兩個(gè)目錄,我們就可以通過fs模塊去讀取一下模板目錄下一共有哪些文件。把這些文件全部輸入到我們的目標(biāo)目錄,我們通過fs的readDir方法,這個(gè)方法會(huì)自動(dòng)掃描目錄下的所有文件fs.readdir(tmplDir, (err, files) => { if (err) { throw err; } files.forEach(file => { console.log(file); // 得到每個(gè)文件的相對(duì)路徑 })})
我們可以通過模板引擎去渲染路徑對(duì)應(yīng)的文件,先去安裝一款模板引擎,這里我們使用ejs安裝過后,回到代碼中引入這個(gè)模板引擎, 通過模板引擎提供的renderFile去渲染這個(gè)路徑對(duì)應(yīng)的文件。第一個(gè)參數(shù)是文件的絕對(duì)路徑,第二個(gè)參數(shù)是模板引擎在工作的時(shí)候的數(shù)據(jù)上下文,第三個(gè)參數(shù)是回調(diào)函數(shù),也就是我們?cè)阡秩境晒^后的回調(diào)函數(shù),當(dāng)然如果你在渲染過程中出現(xiàn)了意外那你可以通過throw err的方式把這個(gè)錯(cuò)誤拋出去。我們可以先把result通過打印的方式打印出來看一下。const fs = require('fs');const path = require('path');const inquirer = require('inquirer');const ejs = require('ejs');
// 工具當(dāng)前目錄const tmplDir = path.join(__dirname, 'templates');// 命令行所在目錄const destDir = process.cwd();
inquirer.prompt([ { type: 'input', name: 'name', message: 'Project name' }]).then(answer => { fs.readdir(tmplDir, (err, files) => { if (err) { throw err; } files.forEach(file => { ejs.renderFile(path.join(tmplDir, file), answer, (err, result) => { if (err) { throw err; } console.log(result); }) }) })})
此時(shí)打印出來的這個(gè)結(jié)果其實(shí)是已經(jīng)經(jīng)過模板引擎工作過后的結(jié)果,我們只需要將這個(gè)結(jié)果通過文件寫入的方式寫入到目標(biāo)目錄就可以了,那目標(biāo)目錄應(yīng)該是通過path.join把我們destDir以及我們的file做一個(gè)拼接。內(nèi)容就是我們這里的result。files.forEach(file => { ejs.renderFile(path.join(tmplDir, file), answer, (err, result) => { if (err) { throw err; } fs.writeFileSync(path.join(destDir, file), result); })})
完成過后我們找到一個(gè)新的目錄,使用一下這個(gè)腳手架我們輸入項(xiàng)目名稱過后,就會(huì)發(fā)現(xiàn)他會(huì)自動(dòng)把我們模板里面的文件自動(dòng)生成到對(duì)應(yīng)的目錄里面,至此我們就已經(jīng)完成了一個(gè)非常簡(jiǎn)單,非常小型的一個(gè)腳手架應(yīng)用。那我們也回顧了一下腳手架的工作過程,其實(shí)腳手架的工作原理并不復(fù)雜,但是他的意義卻是很大的,因?yàn)樗_實(shí)在創(chuàng)建項(xiàng)目環(huán)節(jié)大大提高了我們的效率。我們可以將自己的工具發(fā)布至npm上,提供給更多的人使用。至于發(fā)布npm也非常的簡(jiǎn)單,首先我們需要注冊(cè)npm賬號(hào),有兩種方式可以注冊(cè),一種是登錄npm官網(wǎng)https://www.npmjs.com/, 另一種是使用命令npm adduser。依次輸入第二步中第一種方法注冊(cè)的用戶名、密碼和郵箱。注意:如果報(bào)錯(cuò):'You do not have permission to publish "samlpe-cli". Are you logged in as the correct user?'表示包samlpe-cli名字已經(jīng)在包管理器已經(jīng)存在被別人用了,需要更該包名稱,我們可以前往package.json中的name中換一個(gè)名字。{ "name": "sample-cli1", "version": "1.0.0", "bin": "cli.js", ...}
如果發(fā)布時(shí)報(bào)錯(cuò):no_perms Private mode enable, only admin can publish this module:表示當(dāng)前不是原始鏡像,要切換回原始的npm鏡像npm config set registry https://registry.npmjs.org/
如果需要更新你的工具,只要繼續(xù)執(zhí)行npm publish就可以更新發(fā)布了,不過需要注意,每次發(fā)布都需要修改版本號(hào)version的值,同一個(gè)版本不允許發(fā)布兩次。{ "name": "sample-cli1", "version": "1.0.1", "bin": "cli.js", ...}
不過需要注意,只有在發(fā)包的24小時(shí)內(nèi)才允許撤銷發(fā)布的包,超過24小時(shí),就無法撤回了。