<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          通過(guò) Vite 的 create-app 學(xué)習(xí)如何實(shí)現(xiàn)一個(gè)簡(jiǎn)易版 CLI

          共 11055字,需瀏覽 23分鐘

           ·

          2021-03-05 18:33

          前言

          前段時(shí)間,尤雨溪回答了一個(gè)廣大網(wǎng)友都好奇的一個(gè)問(wèn)題:Vite 會(huì)不會(huì)取代 Vue CLI?

          答案是:是的!

          那么,你開(kāi)始學(xué) Vite 了嗎?用過(guò) Vite 的同學(xué)應(yīng)該都熟悉,創(chuàng)建一個(gè) Vite 的項(xiàng)目模版是通過(guò) npm init @vitejs/app 的方式。而 npm init 命令是在 [email protected] 開(kāi)始支持的,實(shí)際上它是先幫你安裝 Vite 的 @vitejs/create-app 包(package),然后再執(zhí)行 create-app 命令。

          至于 @vitejs/create-app  則是在 Vite 項(xiàng)目的 packages/create-app 文件夾下。其整體的目錄結(jié)構(gòu):

          // 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(以下統(tǒng)稱(chēng)為 create-app CLI)具備的能力不多,目前只支持基礎(chǔ)模版的創(chuàng)建,所以全部代碼加起來(lái)只有 160 行,其整體的架構(gòu)圖:

          可以看出確實(shí)非常簡(jiǎn)單,也因此 create-app CLI 是一個(gè)很值得入門(mén)學(xué)習(xí)如何實(shí)現(xiàn)簡(jiǎn)易版 CLI 的例子

          那么,接下來(lái)本文將會(huì)圍繞以下兩個(gè)部分帶著大家一起通過(guò) create-app CLI 來(lái)學(xué)習(xí)如何制作一個(gè)簡(jiǎn)易版的 CLI

          • create-app 中使用到的庫(kù)(minimist、kolorist

          • 逐步拆解、分析 create-app CLI 源碼

          create-app CLI 中使用到的庫(kù)

          create-app CLI 實(shí)現(xiàn)用到的庫(kù)(npm)確實(shí)很有意思,既有我們熟悉的 enquirer(用于命令行的提示),也有不熟悉的 minimistkolorist。那么,后面這兩者又是拿來(lái)干嘛的?下面,我們就來(lái)了解一番~

          minimist

          minimist 是一個(gè)輕量級(jí)的用于解析命令行參數(shù)的工具。說(shuō)起解析命令行的工具,我想大家很容易想到 commander。相比較 commander 而言,minimist 則以輕取勝!因?yàn)樗挥?32.4 kB,commander 則有 142 kB,即也只有后者的約 1/5。

          那么,下面我們就來(lái)看一下 minimist 的基礎(chǔ)使用。

          例如,此時(shí)我們?cè)诿钚兄休斎耄?/p>

          node index.js my-project

          那么,在 index.js 文件中可以使用 minimist 獲取到輸入的 myproject 參數(shù):

          var argv = require('minimist')(process.argv.slice(2));
          console.log(argv._[0]); 
          // 輸出 my-project

          這里的 argv 是一個(gè)對(duì)象,對(duì)象中 _ 屬性的值則是解析 node index.js 后的參數(shù)所形成的數(shù)組。

          kolorist

          kolorist 是一個(gè)輕量級(jí)的使命令行輸出帶有色彩的工具。并且,說(shuō)起這類(lèi)工具,我想大家很容易想到的就是 chalk。不過(guò)相比較 chalk 而言,兩者包的大小差距并不明顯,前者為 49.9 kB,后者為 33.6 kB。不過(guò) kolorist 可能較為小眾,npm 的下載量大大不如后者 chalk,相應(yīng)地 chalk 的 API 也較為詳盡。

          同樣的,下面我們也來(lái)看一下 kolorist 的基礎(chǔ)使用。

          例如,當(dāng)此時(shí)應(yīng)用發(fā)生異常的時(shí)候,需要打印出紅色的異常信息告知用戶(hù)發(fā)生異常,我們可以使用 kolorist 提供的 red 函數(shù):

          import { red } from 'kolorist'

          console.log(red("Something is wrong"))

          又或者,可以使用 kolorist 提供的 stripColors 來(lái)直接輸出帶顏色的字符串:

          import { red, stripColors } from 'kolorist'

          console.log(stripColors(red("Something is wrong"))

          逐步拆解、分析 create-app CLI 源碼

          了解過(guò) CLI 相關(guān)知識(shí)的同學(xué)應(yīng)該知道,我們通常使用的命令是在 package.json 文件的 bin 中配置的。而 create-app CLI 對(duì)應(yīng)的文件根目錄下該文件的 bin 配置會(huì)是這樣:

          // pacakges/create-app/package.json
          "bin": {
            "create-app""index.js",
            "cva""index.js"
          }

          可以看到 create-app 命令則由這里注冊(cè)生效,它指向的是當(dāng)前目錄下的 index.js 文件。并且,值得一提的是這里注冊(cè)了 2 個(gè)命令,也就是說(shuō)我們還可以使用 cva 命令來(lái)創(chuàng)建基于 Vite 的項(xiàng)目模版(想不到吧 ??)。

          create-app CLI 實(shí)現(xiàn)的核心就是在 index.js 文件。那么,下面我們來(lái)看一下 index.js 中代碼的實(shí)現(xiàn)~

          基礎(chǔ)依賴(lài)引入

          上面我們也提及了 create-app CLI 引入了 minimist、enquire、kolorist 等依賴(lài),所以首先是引入它們:

          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')

          其中,fspath 是 Node 內(nèi)置的模塊,前者用于文件相關(guān)操作、后者用于文件路徑相關(guān)操作。接著就是引入 minimist、enquirerkolorist,它們相關(guān)的介紹上面已經(jīng)提及,這里就不重復(fù)論述~

          定義項(xiàng)目模版(顏色)和文件

          /packages/create-app 目錄中,我們可以看出 create-app CLI 為我們提供了 9 種項(xiàng)目基礎(chǔ)模版。并且,在命令行交互的時(shí)候,每個(gè)模版之間的顏色各有不同,即 CLI 會(huì)使用 kolorist 提供的顏色函數(shù)來(lái)為模版定義好對(duì)應(yīng)的顏色

          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 文件的特殊性,每個(gè)項(xiàng)目模版下都是先創(chuàng)建的 _gitignore 文件,在后續(xù)創(chuàng)建項(xiàng)目的時(shí)候再替換掉該文件的命名(替換為 .gitignore)。所以,CLI 會(huì)預(yù)先定義一個(gè)對(duì)象來(lái)存放需要重命名的文件

          const renameFiles = {
            _gitignore'.gitignore'
          }

          定義文件操作相關(guān)的工具函數(shù)

          由于創(chuàng)建項(xiàng)目的過(guò)程中會(huì)涉及和文件相關(guān)的操作,所以 CLI 內(nèi)部定義了 3 個(gè)工具函數(shù):

          copyDir 函數(shù)

          copyDir 函數(shù)用于將某個(gè)文件夾 srcDir 中的文件復(fù)制到指定文件夾 destDir中。它會(huì)先調(diào)用 fs.mkdirSync 函數(shù)來(lái)創(chuàng)建制定的文件夾,然后枚舉從 srcDir 文件夾下獲取的文件名構(gòu)成的數(shù)組,即 fs.readdirSync(srcDir)。

          其對(duì)應(yīng)的代碼如下:

          function copyDir(srcDir, destDir{
            fs.mkdirSync(destDir, { recursivetrue })
            for (const file of fs.readdirSync(srcDir)) {
              const srcFile = path.resolve(srcDir, file)
              const destFile = path.resolve(destDir, file)
              copy(srcFile, destFile)
            }
          }

          copy 函數(shù)

          copy 函數(shù)則用于復(fù)制文件或文件夾 src 到指定文件夾 dest。它會(huì)先獲取 src 的狀態(tài) stat,如果 src 是文件夾的話(huà),即 stat.isDirectory()true 時(shí),則會(huì)調(diào)用上面介紹的 copyDir 函數(shù)來(lái)復(fù)制 src 文件夾下的文件到 dest 文件夾下。反之,src 是文件的話(huà),則直接調(diào)用 fs.copyFileSync 函數(shù)復(fù)制 src 文件到 dest 文件夾下。

          其對(duì)應(yīng)的代碼如下:

          function copy(src, dest{
            const stat = fs.statSync(src)
            if (stat.isDirectory()) {
              copyDir(src, dest)
            } else {
              fs.copyFileSync(src, dest)
            }
          }

          emptyDir 函數(shù)

          emptyDir 函數(shù)用于清空 dir 文件夾下的代碼。它會(huì)先判斷 dir 文件夾是否存在,存在則枚舉該問(wèn)文件夾下的文件,構(gòu)造該文件的路徑 abs,調(diào)用 fs.unlinkSync 函數(shù)來(lái)刪除該文件,并且當(dāng) abs 為文件夾時(shí),則會(huì)遞歸調(diào)用 emptyDir 函數(shù)刪除該文件夾下的文件,然后再調(diào)用 fs.rmdirSync 刪除該文件夾。

          其對(duì)應(yīng)的代碼如下:

          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 實(shí)現(xiàn)核心函數(shù)

          CLI 實(shí)現(xiàn)核心函數(shù)是 init,它負(fù)責(zé)使用前面我們所說(shuō)的那些函數(shù)、工具包來(lái)實(shí)現(xiàn)對(duì)應(yīng)的功能。下面,我們就來(lái)逐點(diǎn)分析 init 函數(shù)實(shí)現(xiàn)的過(guò)程:

          1. 創(chuàng)建項(xiàng)目文件夾

          通常,我們可以使用 create-app my-project 命令來(lái)指定要?jiǎng)?chuàng)建的項(xiàng)目文件夾,即在哪個(gè)文件夾下:

          let targetDir = argv._[0]
          // cwd = process.cwd()
          const root = path.join(cwd, targetDir)
          console.log(`Scaffolding project in ${root}...`)

          其中,argv._[0] 代表 create-app 后的第一個(gè)參數(shù),root 是通過(guò) path.join 函數(shù)構(gòu)建的完整文件路徑。然后,在命令行中會(huì)輸出提示,告述你腳手架(Scaffolding)項(xiàng)目創(chuàng)建的文件路徑:

          Scaffolding project in /Users/wjc/Documents/project/vite-project...

          當(dāng)然,有時(shí)候我們并不想輸入在 create-app 后輸入項(xiàng)目文件夾,而只是輸入 create-app 命令。那么,此時(shí) tagertDir 是不存在的。CLI 則會(huì)使用 enquirer 包的 prompt 來(lái)在命令行中輸出詢(xún)問(wèn):

          ? project name: > vite-project

          你可以在這里輸入項(xiàng)目文件夾名,又或者直接回車(chē)使用 CLI 給的默認(rèn)項(xiàng)目文件夾名。這個(gè)過(guò)程對(duì)應(yīng)的代碼:

          if (!targetDir) {
            const { name } = await prompt({
              type"input",
              name"name",
              message"Project name:",
              initial"vite-project"
            })
            targetDir = name
          }

          接著,CLI 會(huì)判斷該文件夾是否存在當(dāng)前的工作目錄(cwd)下,如果不存在則會(huì)使用 fs.mkdirSync 創(chuàng)建一個(gè)文件夾:

          if (!fs.existsSync(root)) {
            fs.mkdirSync(root, { recursivetrue })
          }

          反之,如果存在該文件夾,則會(huì)判斷此時(shí)文件夾下是否存在文件,即使用 fs.readdirSync(root) 獲取該文件夾下的文件:

          const existing = fs.readdirSync(root)

          這里 existing 會(huì)是一個(gè)數(shù)組,如果此時(shí)數(shù)組長(zhǎng)度不為 0,則表示該文件夾下存在文件。那么 CLI 則會(huì)詢(xún)問(wèn)是否刪除該文件夾下的文件:

          Target directory vite-project is not empty. 
          Remove existing files and continue?(y/n): Y

          你可以選擇通過(guò)輸入 yn 來(lái)告知 CLI 是否要清空該目錄。并且,如果此時(shí)你輸入的是 y,即不清空該文件夾,那么整個(gè) CLI 的執(zhí)行就會(huì)退出。這個(gè)過(guò)程對(duì)應(yīng)的代碼:

          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. 確定項(xiàng)目模版

          在創(chuàng)建好項(xiàng)目文件夾后,CLI 會(huì)獲取 --template 選項(xiàng),即當(dāng)我們輸入這樣的命令時(shí):

          npm init @vitejs/app --template 文件夾名

          如果 --template 選項(xiàng)不存在(即 undefined),則會(huì)詢(xún)問(wèn)要選擇的項(xiàng)目模版:

          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 中只是定義了模版的類(lèi)型,對(duì)比起 packages/create-app 目錄下的項(xiàng)目模版文件夾命名有點(diǎn)差別(缺少 template 前綴)。例如,此時(shí) template 會(huì)等于 vue-ts,那么就需要給 template 拼接前綴和構(gòu)建完整目錄:

          const templateDir = path.join(__dirname, `template-${template}`)

          所以,現(xiàn)在 templateDir 就會(huì)等于當(dāng)前工作目錄 + template-vue-ts。

          3. 寫(xiě)入項(xiàng)目模版文件

          確定完需要?jiǎng)?chuàng)建的項(xiàng)目的模版后,CLI 就會(huì)讀取用戶(hù)選擇的項(xiàng)目模版文件夾下的文件,然后將它們一一寫(xiě)入此時(shí)創(chuàng)建的項(xiàng)目文件夾下:

          可能有點(diǎn)繞,舉個(gè)例子,選擇的模版是 vue-ts,自己要?jiǎng)?chuàng)建的項(xiàng)目文件夾為 vite-project,那么則是將 create-app/template-vue-ts 文件夾下的文件寫(xiě)到 vite-project 文件夾下。

          const files = fs.readdirSync(templateDir)
          for (const file of files.filter((f) => f !== 'package.json')) {
            write(file)
          }

          由于通過(guò) fs.readdirSync 函數(shù)返回的是該文件夾下的文件名構(gòu)成的數(shù)組 ,所以這里會(huì)通過(guò) for of 枚舉該數(shù)組,每次枚舉會(huì)調(diào)用 write 函數(shù)進(jìn)行文件的寫(xiě)入。

          注意此時(shí)會(huì)跳過(guò) package.json 文件,之后我會(huì)講解為什么需要跳過(guò) package.json 文件。

          write 函數(shù)則接受兩個(gè)參數(shù) filecontent,其具備兩個(gè)能力:

          • 對(duì)指定的文件 file 寫(xiě)入指定的內(nèi)容 content,調(diào)用 fs.writeFileSync 函數(shù)來(lái)實(shí)現(xiàn)將內(nèi)容寫(xiě)入文件

          • 復(fù)制模版文件夾下的文件到指定文件夾下,調(diào)用前面介紹的 copy 函數(shù)來(lái)實(shí)現(xiàn)文件的復(fù)制

          write 函數(shù)的定義:

          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 的獲取過(guò)程,會(huì)針對(duì) file 構(gòu)建完整的文件路徑,并且兼容處理 _gitignore 文件的情況。

          在寫(xiě)入模版內(nèi)的這些文件后,CLI 就會(huì)處理 package.json 文件。之所以單獨(dú)處理 package.json 文件的原因是每個(gè)項(xiàng)目模版內(nèi)的 package.jsonname 都是寫(xiě)死的,而當(dāng)用戶(hù)創(chuàng)建項(xiàng)目后,name 都應(yīng)該為該項(xiàng)目的文件夾命名。這個(gè)過(guò)程對(duì)應(yīng)的代碼會(huì)是這樣:

          const pkg = require(path.join(templateDir, `package.json`))
          pkg.name = path.basename(root)
          write('package.json'JSON.stringify(pkg, null2))

          其中,path.basename 函數(shù)則用于獲取一個(gè)完整路徑的最后的文件夾名

          最后,CLI 會(huì)輸出一些提示告訴你項(xiàng)目已經(jīng)創(chuàng)建結(jié)束,以及告訴你接下來(lái)啟動(dòng)項(xiàng)目需要運(yùn)行的命令:

          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()

          結(jié)語(yǔ)

          雖然 Vite 的 create-app CLI 的實(shí)現(xiàn)僅僅只有 160 行的代碼,但是它也較為全面地考慮了創(chuàng)建項(xiàng)目的各種場(chǎng)景,并做對(duì)應(yīng)的兼容處理。簡(jiǎn)而言之,十分小而美。所以,我相信大家經(jīng)過(guò)學(xué)習(xí) Vite 的 create-app CLI 的實(shí)現(xiàn),都應(yīng)該可以隨手甩出(實(shí)現(xiàn))一個(gè) CLI 的代碼 ?? ~

          點(diǎn)贊 ??、在看 ??

          通過(guò)閱讀本篇文章,如果有收獲的話(huà),可以點(diǎn)個(gè)贊在看,這將會(huì)成為我持續(xù)分享的動(dòng)力,感謝~


          瀏覽 41
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  美女被操逼 | 国产精品久久久夜色 | 一级a一级a爰免费免免高潮 | 爱搞搞就要搞搞 | 日日夜干|