一步一步搭建腳手架

咱們在使用 vue-clicreate-react-app 的時候,只要執行一個簡單的命令 vue init app 或是 create-react-app app 就是快速建立出一個可直接使用的項目模板,極大地提升了開發效率。javascript

本文提供了一個開發簡易腳手架的過程。vue

準備工做

第三方工具

  • comandertj 大神出品的nodejs命令行解決方案,用於捕獲控制檯輸入的命令;
  • chalk:命令行文字配色工具;
  • cross-spawn:跨平臺的 node spawn/spawnSync 解決方案;
  • fs-extranodejs fs 的增強版,新增了API的同時,也包含了原fsAPI
  • handlebars:一個字符串模板工具,能夠將信息填充到模板的指定位置;
  • inquirer:交互式命令行用戶界面集合,用於使用者補充信息或是選擇操做;
  • log-symbols:不一樣日誌級別的彩色符號標誌,包含了 infosuccesswarningerror 四級;
  • ora:動態加載操做符號;

初始化項目

首先,這仍然是一個 nodejs 的工程項目,因此咱們新建一個名爲 scaffold-demo 的文件夾,並使用 npm init 來初始化項目。此時,項目中只有一個 package.json 文件,內容以下:java

{
  "name": "scaffold-demo",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}
複製代碼

而後咱們刪除 "main": "index.js",加入 "private": falsenode

main:是程序的主要入口點,就是說若是有其餘用戶 installrequrie 這個包,那麼將返回該文件 export 出來的對象。react

private:是爲了保護私有庫的手段,當你的庫是私有庫的時候,加入 "private": true,那麼npm將會拒絕發佈這個庫。git

咱們在使用其餘腳手架時,在控制檯中輸入一段簡短的命令就能快速建立一個項目模板,那麼他們是如何使用命令行來操做運行項目的呢,答案就在 npmpackage.jsonbin 字段值中。github

bin 字段接受一個 k-v 的Map,其中 key 表示命令名稱,value表示命令執行的入口文件。當設置了bin字段後,一旦安裝了你的 packagenpm將會這個命令註冊到全局中,並連接對應的文件,而後用戶就能夠直接使用該命令了。vue-cli

詳見:npm bin 官方文檔npm

咱們須要在 package.json 文件中加入如下內容,其中這個 sd 就是咱們命令:json

"bin": {
  "sd": "./main.js"
},
複製代碼

而後在項目中新建 main.js 文件,內容以下:

#!/usr/bin/env node

console.log('Hello Bin')
複製代碼

其中 #!/usr/bin/env node 的做用就是這行代碼是當系統運行到這一行的時候,去 env 中查找 node 配置,而且調用對應的解釋器來運行以後的 node 程序。

而後咱們執行命令 npm link 或是 npm install -g,這樣將本項目的命令註冊到了全局中,而後在命令行中執行 sd 就能看到結果 Hello Bin

first-npm

npm link :將當前 package 連接到全局執行環境。

npm install -g:將當前 package 全局安裝到本地。

對應的解除命令爲: npm unlink 或是 npm uninstall -g

正式開始

如今咱們已經可以完成最基礎的命令行操做了,繼續構建咱們簡易腳手架。

1. 捕獲命令信息

在上文,咱們設置了bin信息,可是隻有一個命令名稱信息,可是在其餘腳手架中,咱們能夠輸入多個字段,如 create-react-app appcreate-reate-app 表示命令,app表示建立的項目的名稱。而這種捕獲命令行的操做咱們能夠藉助 comander 來完成。

實際上,vuereact 的腳手架也是藉助 comander 完成的。

咱們將 main.js 作以下修改:

#!/usr/bin/env node
const program = require('commander')

program
  .command('init <name>')
  .description('初始化模板')
  .action(name => {
    console.log('Hello ' + name)
  })

program.parse(process.argv)
複製代碼

而後在命令行輸入 sd init firstApp,就能看到返回 Hello firstApp了。

npm-firstApp

在上述代碼中,command 函數表示當前命令的一個子命令,能夠設置多個,緊隨的 description 用於描述該命令,action 表示輸入命令後須要執行的操做。其中 command 中的尖括號(<>)表示該參數爲必須輸入的,中括號([])表示爲可選的。 program.parse(process.argv) 必需要,若是沒有則不會起做用。

更詳細例子參考官網的例子:github.com/tj/commande…

2. 複製項目模板至指定目錄

在本文中咱們採用的本地項目模板複製的方式,即本腳手架中包含了所須要初始項目的模板文件,位於Template文件夾下(這個目錄開發者能夠隨意修改)。

若是想使用在線模板的方式,能夠藉助工具 download-git-repo,將 copy 換成下載便可。

本文的 template 內容見文末的代碼倉庫。

而後咱們將 action 中的邏輯替換成以下內容:

action(async name => {
	// 判斷用戶是否輸入應用名稱,若是沒有設置爲 myApp
  const projectName = name || 'myApp'
  // 獲取 template 文件夾路徑
  const sourceProjectPath = __dirname + '/template'
  // 獲取命令所在文件夾路徑
  // path.resolve(name) == process.cwd() + '/' + name
  const targetProjectPath = path.resolve(projectName)

  // 建立一個空的文件夾
  fs.emptyDirSync(targetProjectPath)

  try {
    // 將模板文件夾中的內容複製到目標文件夾(目標文件夾爲命令輸入所在文件夾)
    fs.copySync(sourceProjectPath, targetProjectPath)
    console.log('已經成功拷貝 Template 文件夾下全部文件!')
  } catch (err) {
    console.error('項目初始化失敗,已退出!')
    return
  }
}
複製代碼

3. 確認目標文件夾是否存在(命令行交互)

咱們已經完成了最基礎簡單的目標文件複製的過程,可是在實際過程當中,頗有可能存在用戶輸入的文件夾已經存在了的狀況,因此咱們須要詢問用戶是要覆蓋原文件夾內容仍是退出從新操做。這一塊的操做咱們使用 inquirer 來完成,inquirer 能夠提供命令行的用戶交互功能。

咱們在建立空文件夾以前加入一下判斷文件是否存在的代碼。

// 判斷文件夾是否存在
if (fs.existsSync(targetProjectPath)) {
  console.log(`文件夾 ${projectName} 已經存在!`)
  try {
    // 若存在,則詢問用戶是否覆蓋當前文件夾的內容,yes 則覆蓋,no 則退出。
    const { isCover } = await inquirer.prompt([
      { name: 'isCover', message: '是否要覆蓋當前文件夾的內容', type: 'confirm' }
    ])
    if (!isCover) {
      return
    }
  } catch (error) {
    console.log('項目初始化失敗,已退出!')
    return
  }
}
複製代碼

請注意這裏使用了 async - await

app-exist

4. 美化命令行 console

如今的命令行都是單調的白色字,咱們使用 chalklog-symbols 來實現命令行的美化。主要代碼以下:

主要改了 console 部分的代碼,使用 log-symbols 添加輸出標識, chalk 改變文字顏色。

action(async name => {
  // 判斷用戶是否輸入應用名稱,若是沒有設置爲 myApp
  const projectName = name || 'myApp'
  // 獲取 template 文件夾路徑
  const sourceProjectPath = __dirname + '/template'
  // 獲取命令所在文件夾路徑
  // path.resolve(name) == process.cwd() + '/' + name
  const targetProjectPath = path.resolve(projectName)

  // 判斷文件夾是否存在及其後續邏輯
  if (fs.existsSync(targetProjectPath)) {
    console.log(symbols.info, chalk.blue(`文件夾 ${projectName} 已經存在!`))
    try {
      const { isCover } = await inquirer.prompt([
        { name: 'isCover', message: '是否要覆蓋當前文件夾的內容', type: 'confirm' }
      ])
      if (!isCover) {
        return
      }
    } catch (error) {
      console.log(symbols.fail, chalk.red('項目初始化失敗,已退出!'))

      return
    }
  }
  // 建立一個空的文件夾
  fs.emptyDirSync(targetProjectPath)

  try {
    // 將模板文件夾中的內容複製到目標文件夾(目標文件夾爲命令輸入所在文件夾)
    fs.copySync(sourceProjectPath, targetProjectPath)
    console.log(symbols.success, chalk.green('已經成功拷貝 Template 文件夾下全部文件!'))
  } catch (err) {
    console.error(symbols.fail, chalk.red('項目初始化失敗,已退出!'))
    return
  }
})
複製代碼

美化前:

console-normal

美化後:

console-beautify

5. 修改 package.json

有些時候,咱們須要根據用戶輸入來修改填充 package.json,就像 npm init 的時候輸入的信息。在這裏咱們使用 inquirer 獲取用戶輸入,使用 handlebars 來將用戶輸入填充到 package.json 中去。

在拷貝文件夾後加入如下代碼:

// 獲取項目的描述及做者名稱等信息
const { projectDescription, projectAuthor } = await inquirer.prompt([
  { name: 'projectDescription', message: '請輸入項目描述' },
  { name: 'projectAuthor', message: '請輸入做者名字' }
])

const meta = {
  projectAuthor,
  projectDescription,
  projectName
}

// 獲取拷貝後的模板項目中的 `package.json`
const targetPackageFile = targetProjectPath + '/package.json'
if (fs.pathExistsSync(targetPackageFile)) {
  // 讀取文件,並轉換成字符串模板
  const content = fs.readFileSync(targetPackageFile).toString()
  // 利用 handlebars 將須要的內容寫入到模板中
  const result = handlebars.compile(content)(meta)
  fs.writeFileSync(targetPackageFile, result)
} else {
  console.log('package.json 文件不存在:' + targetPackageFile)
}
複製代碼

至此,咱們的簡易腳手架已經基本搭建完成了,可以在指定文件夾生成項目模板文件。可是,咱們若是使用 create-react-app 的話,就會發現只要你一執行命令就會它幫你自動安裝依賴,並且也會自動初始化 Git

如今咱們就來完成這兩個功能。

6. 安裝依賴

// 經過執行命令 yarn --version 的方式,來判斷本機是否已經安裝了 yarn
// 若是安裝了,後續就使用yarn,不然就使用 npm;
function canUseYarn() {
  try {
    spawn('yarnpkg', ['--version'])
    return true
  } catch (error) {
    return false
  }
}

function tryYarn(root) {
  return new Promise((resolve, reject) => {
    let child
    const isUseYarn = canUseYarn()
    if (isUseYarn) {
      // 這裏就至關於命令行中執行 `yarn`
      child = spawn('yarnpkg', ['--cwd', root], { stdio: 'inherit' })
    } else {
      // 這裏就至關於命令行中執行 `npm install`
      child = spawn('npm', ['install'], { cwd: root, stdio: 'inherit' })
    }
		// 當命令執行完成的時候,判斷是否執行成功,並輸出相應的輸出。
    child.on('close', code => {
      if (code !== 0) {
        reject(console.log(symbols.error, chalk.red(isUseYarn ? 'yarn' : 'npm' + ' 依賴安裝失敗...')))
        return
      }
      resolve(console.log(symbols.success, chalk.green(isUseYarn ? 'yarn' : 'npm' + ' 依賴安裝完成!')))
    })
  })
}
複製代碼

這裏須要注意的是執行命令語句 spawn('yarnpkg', ['--cwd', root], { stdio: 'inherit' })

上述語句至關於命令行中執行 yarn,可是咱們必須加上 '--cwd' 來將其執行路徑修改成命令所在的目錄,由於 spawn 默認執行目錄是腳手架目錄。同時又由於 spawn 是開了一個子線程,因此若是你不使用 { stdio: 'inherit' },那麼你將看不到 yarn 安裝的過程。

參考博客:Node.js child_process模塊解讀

stdio 選項用於配置父進程和子進程之間創建的管道,因爲 stdio 管道有三個(stdin, stdout, stderr)所以 stdio 的三個可能的值實際上是數組的一種簡寫

  • pipe 至關於 ['pipe', 'pipe', 'pipe'](默認值)
  • ignore 至關於 ['ignore', 'ignore', 'ignore']
  • inherit 至關於 [process.stdin, process.stdout, process.stderr]

而後在修改 package.json 代碼後面添加如下代碼便可。

// 安裝依賴
await tryYarn(targetProjectPath)
複製代碼

7. 初始化 Git

而後咱們進行git的初始化,即執行 git init

function tryInitGit(root) {
  // 本來模板中,咱們就存放了 gitignore 模板文件,須要將其內容複製到新建的 .gitignore 文件中
  try {
    // 若是項目中存在了 .gitignore 文件,那麼這個 API 會執行失敗,跳入 catch 分支進行合併操做
    fs.moveSync(path.join(root, 'gitignore'), path.join(root, '.gitignore'))
  } catch (error) {
    const content = fs.readFileSync(path.join(root, 'gitignore'))
    fs.appendFileSync(path.join(root, '.gitignore'), content)
  } finally {
    // 移除 gitignore 模板文件
    fs.removeSync(path.join(root, 'gitignore'))
  }

  try {
    spawn('git', ['init'], { cwd: root })
    spawn('git', ['add .'], { cwd: root })
    spawn('git', ['commit', '-m', 'Initial commit from New App'], { cwd: root })
    console.log(symbols.success, chalk.green('Git 初始化完成!'))
  } catch (error) {
    console.log(symbols.error, chalk.red('Git 初始化失敗...'))
  }
}
複製代碼

而後在安裝依賴以後加入如下代碼:

// 初始化 git
tryInitGit(targetProjectPath)
複製代碼

completed

總結

本文代碼倉庫:github.com/Huanqiang/s…

本文總結了我的在搭建簡易腳手架的過程,功能過於簡單,算是一個小小的開端吧。

最後不禁感嘆 nodejs 仍是很是之強悍的!

相關文章
相關標籤/搜索