前段時間,尤雨溪回答了一個廣大網友都好奇的一個問題:Vite 會不會取代 Vue CLI?javascript
答案是:是的!前端
那麼,你開始學 Vite 了嗎?用過 Vite 的同窗應該都熟悉,建立一個 Vite 的項目模版是經過 npm init @vitejs/app
的方式。而 npm init
命令是在 npm@6.1.0
開始支持的,實際上它是先幫你安裝 Vite 的 @vitejs/create-app
包(package),而後再執行 create-app
命令。vue
至於 @vitejs/create-app
則是在 Vite 項目的 packages/create-app
文件夾下。其總體的目錄結構:java
// packages/create-app |———— template-lit-element |———— template-lit-element-ts |———— template-preact |———— template-preact-ts |———— template-react |———— template-react-ts |———— template-vanilla |———— template-vue |———— template-vue-ts index.js package.json
Vite 的 create-app
CLI(如下統稱爲 create-app
CLI)具有的能力很少,目前只支持基礎模版的建立,因此所有代碼加起來只有 160 行,其總體的架構圖:node
能夠看出確實很是簡單,也所以 create-app
CLI 是一個很值得入門學習如何實現簡易版 CLI 的例子。react
那麼,接下來本文將會圍繞如下兩個部分帶着你們一塊兒經過 create-app
CLI 來學習如何製做一個簡易版的 CLI:git
create-app
中使用到的庫(minimist
、kolorist
)create-app
CLI 源碼create-app
CLI 實現用到的庫(npm)確實頗有意思,既有咱們熟悉的 enquirer
(用於命令行的提示),也有不熟悉的 minimist
和 kolorist
。 那麼,後面這二者又是拿來幹嗎的?下面,咱們就來了解一番~npm
minimist
是一個輕量級的用於解析命令行參數的工具。提及解析命令行的工具,我想你們很容易想到 commander
。相比較 commander
而言,minimist
則以輕取勝!由於它只有 32.4 kB,commander
則有 142 kB,即也只有後者的約 1/5。json
那麼,下面咱們就來看一下 minimist
的基礎使用。前端工程化
例如,此時咱們在命令行中輸入:
node index.js my-project
那麼,在 index.js
文件中可使用 minimist
獲取到輸入的 myproject
參數:
var argv = require('minimist')(process.argv.slice(2)); console.log(argv._[0]); // 輸出 my-project
這裏的 argv
是一個對象,對象中 _
屬性的值則是解析 node index.js
後的參數所造成的數組。
kolorist
是一個輕量級的使命令行輸出帶有色彩的工具。而且,提及這類工具,我想你們很容易想到的就是 chalk
。不過相比較 chalk
而言,二者包的大小差距並不明顯,前者爲 49.9 kB,後者爲 33.6 kB。不過 kolorist
可能較爲小衆,npm 的下載量大大不如後者 chalk
,相應地 chalk
的 API 也較爲詳盡。
一樣的,下面咱們也來看一下 kolorist
的基礎使用。
例如,當此時應用發生異常的時候,須要打印出紅色的異常信息告知用戶發生異常,咱們可使用 kolorist
提供的 red
函數:
import { red } from 'kolorist' console.log(red("Something is wrong"))
又或者,可使用 kolorist
提供的 stripColors
來直接輸出帶顏色的字符串:
import { red, stripColors } from 'kolorist' console.log(stripColors(red("Something is wrong"))
瞭解過 CLI 相關知識的同窗應該知道,咱們一般使用的命令是在 package.json
文件的 bin
中配置的。而 create-app
CLI 對應的文件根目錄下該文件的 bin
配置會是這樣:
// pacakges/create-app/package.json "bin": { "create-app": "index.js", "cva": "index.js" }
能夠看到 create-app
命令則由這裏註冊生效,它指向的是當前目錄下的 index.js
文件。而且,值得一提的是這裏註冊了 2 個命令,也就是說咱們還可使用 cva
命令來建立基於 Vite 的項目模版(想不到吧 😲)。
而 create-app
CLI 實現的核心就是在 index.js
文件。那麼,下面咱們來看一下 index.js
中代碼的實現~
上面咱們也說起了 create-app
CLI 引入了 minimist
、enquire
、kolorist
等依賴,因此首先是引入它們:
const fs = require('fs') const path = require('path') const argv = require('minimist')(process.argv.slice(2)) const { prompt } = require('enquirer') const { yellow, green, cyan, magenta, lightRed, stripColors } = require('kolorist')
其中,fs
和 path
是 Node 內置的模塊,前者用於文件相關操做、後者用於文件路徑相關操做。接着就是引入 minimist
、enquirer
和 kolorist
,它們相關的介紹上面已經說起,這裏就不重複論述~
從 /packages/create-app
目錄中,咱們能夠看出 create-app
CLI 爲咱們提供了 9 種項目基礎模版。而且,在命令行交互的時候,每一個模版之間的顏色各有不一樣,即 CLI 會使用 kolorist
提供的顏色函數來爲模版定義好對應的顏色:
const TEMPLATES = [ yellow('vanilla'), green('vue'), green('vue-ts'), cyan('react'), cyan('react-ts'), magenta('preact'), magenta('preact-ts'), lightRed('lit-element'), lightRed('lit-element-ts') ]
其次,因爲 .gitignore
文件的特殊性,每一個項目模版下都是先建立的 _gitignore
文件,在後續建立項目的時候再替換掉該文件的命名(替換爲 .gitignore
)。因此,CLI 會預先定義一個對象來存放須要重命名的文件:
const renameFiles = { _gitignore: '.gitignore' }
因爲建立項目的過程當中會涉及和文件相關的操做,因此 CLI 內部定義了 3 個工具函數:
copyDir 函數
copyDir
函數用於將某個文件夾 srcDir
中的文件複製到指定文件夾 destDir
中。它會先調用 fs.mkdirSync
函數來建立制定的文件夾,而後枚舉從 srcDir
文件夾下獲取的文件名構成的數組,即 fs.readdirSync(srcDir)
。
其對應的代碼以下:
function copyDir(srcDir, destDir) { fs.mkdirSync(destDir, { recursive: true }) for (const file of fs.readdirSync(srcDir)) { const srcFile = path.resolve(srcDir, file) const destFile = path.resolve(destDir, file) copy(srcFile, destFile) } }
copy 函數
copy
函數則用於複製文件或文件夾 src
到指定文件夾 dest
。它會先獲取 src
的狀態 stat
,若是 src
是文件夾的話,即 stat.isDirectory()
爲 true
時,則會調用上面介紹的 copyDir
函數來複制 src
文件夾下的文件到 dest
文件夾下。反之,src
是文件的話,則直接調用 fs.copyFileSync
函數複製 src
文件到 dest
文件夾下。
其對應的代碼以下:
function copy(src, dest) { const stat = fs.statSync(src) if (stat.isDirectory()) { copyDir(src, dest) } else { fs.copyFileSync(src, dest) } }
emptyDir 函數
emptyDir
函數用於清空 dir
文件夾下的代碼。它會先判斷 dir
文件夾是否存在,存在則枚舉該問文件夾下的文件,構造該文件的路徑 abs
,調用 fs.unlinkSync
函數來刪除該文件,而且當 abs
爲文件夾時,則會遞歸調用 emptyDir
函數刪除該文件夾下的文件,而後再調用 fs.rmdirSync
刪除該文件夾。
其對應的代碼以下:
function emptyDir(dir) { if (!fs.existsSync(dir)) { return } for (const file of fs.readdirSync(dir)) { const abs = path.resolve(dir, file) if (fs.lstatSync(abs).isDirectory()) { emptyDir(abs) fs.rmdirSync(abs) } else { fs.unlinkSync(abs) } } }
CLI 實現核心函數是 init
,它負責使用前面咱們所說的那些函數、工具包來實現對應的功能。下面,咱們就來逐點分析 init
函數實現的過程:
1. 建立項目文件夾
一般,咱們可使用 create-app my-project
命令來指定要建立的項目文件夾,即在哪一個文件夾下:
let targetDir = argv._[0] // cwd = process.cwd() const root = path.join(cwd, targetDir) console.log(`Scaffolding project in ${root}...`)
其中,argv._[0]
表明 create-app
後的第一個參數,root
是經過 path.join
函數構建的完整文件路徑。而後,在命令行中會輸出提示,告述你腳手架(Scaffolding)項目建立的文件路徑:
Scaffolding project in /Users/wjc/Documents/project/vite-project...
固然,有時候咱們並不想輸入在 create-app
後輸入項目文件夾,而只是輸入 create-app
命令。那麼,此時 tagertDir
是不存在的。CLI 則會使用 enquirer
包的 prompt
來在命令行中輸出詢問:
? project name: > vite-project
你能夠在這裏輸入項目文件夾名,又或者直接回車使用 CLI 給的默認項目文件夾名。這個過程對應的代碼:
if (!targetDir) { const { name } = await prompt({ type: "input", name: "name", message: "Project name:", initial: "vite-project" }) targetDir = name }
接着,CLI 會判斷該文件夾是否存在當前的工做目錄(cwd
)下,若是不存在則會使用 fs.mkdirSync
建立一個文件夾:
if (!fs.existsSync(root)) { fs.mkdirSync(root, { recursive: true }) }
反之,若是存在該文件夾,則會判斷此時文件夾下是否存在文件,即便用 fs.readdirSync(root)
獲取該文件夾下的文件:
const existing = fs.readdirSync(root)
這裏 existing
會是一個數組,若是此時數組長度不爲 0,則表示該文件夾下存在文件。那麼 CLI 則會詢問是否刪除該文件夾下的文件:
Target directory vite-project is not empty. Remove existing files and continue?(y/n): Y
你能夠選擇經過輸入 y
或 n
來告知 CLI 是否要清空該目錄。而且,若是此時你輸入的是 y
,即不清空該文件夾,那麼整個 CLI 的執行就會退出。這個過程對應的代碼:
if (existing.length) { const { yes } = await prompt({ type: 'confirm', name: 'yes', initial: 'Y', message: `Target directory ${targetDir} is not empty.\n` + `Remove existing files and continue?` }) if (yes) { emptyDir(root) } else { return } }
2. 肯定項目模版
在建立好項目文件夾後,CLI 會獲取 --template
選項,即當咱們輸入這樣的命令時:
npm init @vitejs/app --template 文件夾名
若是 --template
選項不存在(即 undefined
),則會詢問要選擇的項目模版:
let template = argv.t || argv.template if (!template) { const { t } = await prompt({ type: "select", name: "t", message: "Select a template:", choices: TEMPLATES }) template = stripColors(t) }
因爲,TEMPLATES
中只是定義了模版的類型,對比起 packages/create-app
目錄下的項目模版文件夾命名有點差異(缺乏 template
前綴)。例如,此時 template
會等於 vue-ts
,那麼就須要給 template
拼接前綴和構建完整目錄:
const templateDir = path.join(__dirname, `template-${template}`)
因此,如今 templateDir
就會等於當前工做目錄 + template-vue-ts
。
3. 寫入項目模版文件
肯定完須要建立的項目的模版後,CLI 就會讀取用戶選擇的項目模版文件夾下的文件,而後將它們一一寫入此時建立的項目文件夾下:
可能有點繞,舉個例子,選擇的模版是vue-ts
,本身要建立的項目文件夾爲vite-project
,那麼則是將create-app/template-vue-ts
文件夾下的文件寫到vite-project
文件夾下。
const files = fs.readdirSync(templateDir) for (const file of files.filter((f) => f !== 'package.json')) { write(file) }
因爲經過 fs.readdirSync
函數返回的是該文件夾下的文件名構成的數組 ,因此這裏會經過 for of
枚舉該數組,每次枚舉會調用 write
函數進行文件的寫入。
注意此時會跳過package.json
文件,以後我會講解爲何須要跳過package.json
文件。
而 write
函數則接受兩個參數 file
和 content
,其具有兩個能力:
file
寫入指定的內容 content
,調用 fs.writeFileSync
函數來實現將內容寫入文件copy
函數來實現文件的複製write
函數的定義:
const write = (file, content) => { const targetPath = renameFiles[file] ? path.join(root, renameFiles[file]) : path.join(root, file) if (content) { fs.writeFileSync(targetPath, content) } else { copy(path.join(templateDir, file), targetPath) } }
而且,值得一提的是 targetPath
的獲取過程,會針對 file
構建完整的文件路徑,而且兼容處理 _gitignore
文件的狀況。
在寫入模版內的這些文件後,CLI 就會處理 package.json
文件。之因此單獨處理 package.json
文件的緣由是每一個項目模版內的 package.json
的 name
都是寫死的,而當用戶建立項目後,name
都應該爲該項目的文件夾命名。這個過程對應的代碼會是這樣:
const pkg = require(path.join(templateDir, `package.json`)) pkg.name = path.basename(root) write('package.json', JSON.stringify(pkg, null, 2))
其中,
path.basename
函數則用於獲取一個完整路徑的最後的文件夾名
最後,CLI 會輸出一些提示告訴你項目已經建立結束,以及告訴你接下來啓動項目須要運行的命令:
console.log(`\nDone. Now run:\n`) if (root !== cwd) { console.log(` cd ${path.relative(cwd, root)}`) } console.log(` npm install (or \`yarn\`)`) console.log(` npm run dev (or \`yarn dev\`)`) console.log()
雖然 Vite 的 create-app
CLI 的實現僅僅只有 160 行的代碼,可是它也較爲全面地考慮了建立項目的各類場景,並作對應的兼容處理。簡而言之,十分小而美。因此,我相信你們通過學習 Vite 的 create-app
CLI 的實現,都應該能夠隨手甩出(實現)一個 CLI 的代碼 😎 ~
經過閱讀本篇文章,若是有收穫的話,能夠點個贊,這將會成爲我持續分享的動力,感謝~
我是五柳,喜歡創新、搗鼓源碼,專一於源碼(Vue 三、Vite)、前端工程化、跨端等技術學習和分享,歡迎關注個人 微信公衆號:Code center。