Vite創建項目的實現步驟

前言

隨著 Vite2 的發佈並日趨穩定,現在越來越多的項目開始嘗試使用它。我們使用 Vite 是一般會用下面這些命令去創建一個項目:

// 使用 npm
npm init @vitejs/app
// 使用 yarn
yarn create @vitejs/app

 
// 想指定項目名稱和使用某個特定框架的模版時,可以像下面這樣
// npm
npm init @vitejs/app my-vue-app --template vue
// yarn
yarn create @vitejs/app my-vue-app --template vue

運行這些命令後就會生成一個項目文件夾,對於大多數人可能覺得隻要能正常創建一個項目就夠瞭,但我出於好奇,為什麼運行這些命令就會生成一個項目文件夾。這裡以 yarn 為例創建項目進行說明。

yarn create 做瞭什麼

可能很多人會疑惑,為什麼很多項目的創建方式都是使用yarn create這個命令進行創建。除瞭這裡的 Vite,我們創建 React 項目也是這樣:yarn create react-app my-app
那這個命令到底做瞭什麼,它其實做瞭兩件事:

yarn global add create-react-app
create-react-app my-app

關於yarn create的更多內容可以看這裡

源碼解析

yarn create @vitejs/app命令運行後就會執行@vitejs/create-app裡的代碼。我們先看看這文件的項目結構

template 開頭的文件夾都是各個框架和對應的typescript版本的項目模板,我們不用太關心,創建項目的邏輯都在 index.js 文件裡。下面就來看看這裡面都做瞭什麼

項目依賴

首先是依賴的引入

const fs = require('fs')
const path = require('path')
const argv = require('minimist')(process.argv.slice(2))
const prompts = require('prompts')
const {
  yellow,
  green,
  cyan,
  blue,
  magenta,
  lightRed,
  red
} = require('kolorist')

fs、path是Nodejs內置模塊,minimist、prompts、kolorist則分別是第三方依賴庫。

  • minimist:是一個用於解析命令行參數的工具。文檔
  • prompts:是一個命令行交互的工具。文檔
  • kolorist:是一個使命令行輸出帶有色彩的工具。文檔

模版配置

接下來不同框架模版的配置文件,最後生成一個模版名稱的數組。

// 這裡隻寫瞭vue和react框架的配置,其他的都是差的不多,感興趣可以去看源碼。
const FRAMEWORKS = [
  ......
  
  {
    name: 'vue',
    color: green,
    variants: [
      {
        name: 'vue',
        display: 'JavaScript',
        color: yellow
      },
      {
        name: 'vue-ts',
        display: 'TypeScript',
        color: blue
      }
    ]
  },
  {
    name: 'react',
    color: cyan,
    variants: [
      {
        name: 'react',
        display: 'JavaScript',
        color: yellow
      },
      {
        name: 'react-ts',
        display: 'TypeScript',
        color: blue
      }
    ]
  },
  
  ......
]

// 輸出模版名稱列表
const TEMPLATES = FRAMEWORKS.map(
  (f) => (f.variants && f.variants.map((v) => v.name)) || [f.name]
).reduce((a, b) => a.concat(b), [])

其次,由於 .gitignore 文件的特殊性,每種框架項目模版下都是先創建的 _gitignore 文件,在後續創建項目的時候再替換為 .gitignore。所以,代碼裡會預先定義一個對象來存放需要重命名的文件:

const renameFiles = {
  _gitignore: '.gitignore'
}

工具函數

在開始講的核心函數之前,先來看看代碼中定義的工具函數。最重要的是與文件操作相關的三個函數。

copy

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

copy函數則用於復制文件或文件夾 src 到指定文件夾 dest。它會先獲取 src 的狀態 stat,如果 src 是文件夾的話,即stat.isDirectory()為 true 時,則會調用下面將介紹的copyDir函數來復制 src 文件夾下的文件到 dest 文件夾下。反之,src 是文件的話,則直接調用 fs.copyFileSync 函數復制 src 文件到 dest 文件夾下。

copyDir

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

copyDir函數用於將某個文件夾 srcDir 中的文件復制到指定文件夾 destDir 中。它會先調用 fs.mkdirSync函數來創建制定的文件夾,然後調用fs.readdirSync從 srcDir 文件夾下獲取的文件並遍歷逐個復制;最後在調用copy函數進行復制,這裡用到瞭遞歸,因為可能存在文件夾裡的文件還是文件夾。

emptyDir

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

emptyDir函數用於清空 dir 文件夾下的代碼。它會先判斷 dir 文件夾是否存在,存在則遍歷該問文件夾下的文件,構造該文件的路徑 abs,當 abs 為文件夾時,會遞歸調用 emptyDir 函數刪除該文件夾下的文件,然後再調用fs.rmdirSync刪除該文件夾;當 abs 是文件時,則調用fs.unlinkSync函數來刪除該文件。

核心函數

接下來就是核心功能實現的init函數。

命令行交互並創建文件夾

首先是獲取命令行參數

let targetDir = argv._[0]
let template = argv.template || argv.t

const defaultProjectName = !targetDir ? 'vite-project' : targetDir

argv._[0] 代表 @vitejs/app 後的第一個參數
template則是要使用的模版名稱
defaultProjectName則是我們創建的項目名稱。
接下來就是使用prompts包來在命令行中輸出詢問,像下面這樣:

具體代碼如下:

// 關於命令行交互的部分代碼沒有全部放在這裡,感興趣的可以去看源碼
let result = {}

result = await prompts(
  [
    {
      type: targetDir ? null : 'text',
      name: 'projectName',
      message: 'Project name:',
      initial: defaultProjectName,
      onState: (state) =>
        (targetDir = state.value.trim() || defaultProjectName)
    },
    ......
    
  ]
)

const { framework, overwrite, packageName, variant } = result

const root = path.join(cwd, targetDir)

if (overwrite) {
  emptyDir(root)
} else if (!fs.existsSync(root)) {
  fs.mkdirSync(root)
}

template = variant || framework || template

// 輸出項目文件夾路徑
console.log(`\nScaffolding project in ${root}...`)

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

選擇完成後會返回我們選擇的結果result
root是通過path.join函數構建的完整文件路徑
overwrite是針對已存在我們要創建的同名文件時,是否要重寫,如果重寫,則調用前面的emptyDir函數清空該文件夾,如果不存在該文件夾,則調用fs.mkdirSync創建文件夾
templateDir選擇的模版文件夾名稱

寫入文件

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

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

const pkg = require(path.join(templateDir, `package.json`))

pkg.name = packageName

write('package.json', JSON.stringify(pkg, null, 2))

const pkgManager = /yarn/.test(process.env.npm_execpath) ? 'yarn' : 'npm'

// 輸出一些提示告訴你項目已經創建結束,以及告訴你接下來啟動項目需要運行的命令
console.log(`\nDone. Now run:\n`)
if (root !== cwd) {
console.log(`  cd ${path.relative(cwd, root)}`)
}
console.log(`  ${pkgManager === 'yarn' ? `yarn` : `npm install`}`)
console.log(`  ${pkgManager === 'yarn' ? `yarn dev` : `npm run dev`}`)
console.log()

write函數則接受兩個參數 file 和 content,它有兩個功能:

  • 對指定的文件 file 寫入指定的內容 content,調用fs.writeFileSync函數來實現將內容寫入文件。
  • 復制模版文件夾下的文件到指定文件夾下,調用前面介紹的copy函數來實現文件的復制。

然後調用fs.readdirSync讀取模版文件夾裡的文件,遍歷逐一復制到項目文件夾(其中要過濾的 package.json 文件,因為其中的 name 字段要修改);最後再寫入 package.json 文件。

小結

Vite 的create-app包的實現隻有320行左右的代碼,但它考慮到各種場景的兼容處理;在學習完之後,自己去實現一個這樣的CLI工具也不是什麼難事。

到此這篇關於Vite創建項目的實現步驟的文章就介紹到這瞭,更多相關Vite創建項目內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: