仿 vue-cli 搭建屬於自己的腳手架

尤水就下也發表於2019-03-27

腳手架是啥

從前我總覺得腳手架是個很高大上的東西,好像得牛叉?一點的人才寫的出來,可望而不可即。其實並不是因為困難使我們放棄,而是因為放棄才顯得困難(這是個好詞好句?)。只要你肯花個一天半天的時間✊,也能寫出屬於你自己的腳手架。
早前腳手架這個詞是從 vue-cli 這裡認識的,我們通過 npm install -g vue-cli 命令全域性安裝腳手架後, 再執行 vue init webpack project-name 就能初始化好一個自己的專案,真是尼瑪的神奇?。但你有沒有想過為什麼我們執行 vue init 這個命令就能有個自己的專案呢。今天,就讓我們一起來揭開廬山真面目吧!
等等✋,扯了一堆,你好像還沒說下啥是腳手架?emmm... 它就是個工具,方便我們新建專案用的,有了這個專案我們就能直接開發了。其實我們本可以用 git clone url 來新建(複製)專案,再 cuo 一點的方法就是複製貼上整個資料夾,一樣也能達到初始化的目的。腳手架的本質也是從遠端下載一個模板來進行一個新專案。額。所以。。。有什麼不同呢?就高大上啊?。當然不止於此啦,腳手架可是高階版的克隆,它主要是提供了互動式的命令讓我們可以動態的更改模板,然後用一句命令就可以一勞永逸了(當然還是要維護的),這應該是最主要的區別吧,反正現在我是這麼想的?。
好了,本章的目的就是帶領大家寫一個簡易版的腳手架 xr-cli(名字愛取啥取啥),目標是實現一個 xr init template-name project-name 這樣的命令,廢話少說,開始進入正題吧???。

原始碼地址:github.com/lgq627628/x…

前置知識

其實一個簡易版的 xr-cli 的程式碼量並不多,所以這裡我們先來小小介紹一下其中要依賴的包,如果你用過這些工具可以跳過,沒用過的請務必一定要瞟一眼。

commander

這是用來編寫指令和處理命令列的,具體用法如下:

const program = require("commander");
// 定義指令
program
  .version('0.0.1')
  .command('init', 'Generate a new project from a template')
  .action(() => {
    // 回撥函式
  })
// 解析命令列引數
program.parse(process.argv);
複製程式碼

回憶一下,我們曾用過的 vue init 的命令就是這樣宣告的。

inquirer

這是個強大的互動式命令列工具,具體用法如下:

const inquirer = require('inquirer');
inquirer
  .prompt([
    // 一些互動式的問題
  ])
  .then(answers => {
    // 回撥函式,answers 就是使用者輸入的內容,是個物件
  });
複製程式碼

想象一下我們用 vue init webpack project-name 之後是不是會有幾個互動問題,問你檔名啊、作者啊、描述啊、要不要用 eslint 啊等等之類的,就是用這個來寫的。

chalk

這是用來修改控制檯輸出內容樣式的,比如顏色啊,具體用法如下:

const chalk = require('chalk');
console.log(chalk.green('success'));
console.log(chalk.red('error'));
複製程式碼

ora

這是一個好看的載入,就是你下載的時候會有個轉圈圈的那種效果,用法如下:

const ora = require('ora')
let spinner = ora('downloading template ...')
spinner.start()
複製程式碼

download-git-repo

看名字很明顯了,這是用來下載遠端模板的,支援 GitHub、 GitLab 和 Bitbucket 等,用法如下:

const download = require('download-git-repo')
download(repository, destination, options, callback)
複製程式碼

其中 repository 是遠端倉庫地址;destination 是存放下載的檔案路徑,也可以直接寫檔名,預設就是當前目錄;options 是一些選項,比如 { clone:boolean } 表示用 http download 還是 git clone 的形式下載。

目錄搭建

ok,有了上面的知識儲備之後,我們就正式開始擼了。

  1. 首先我們要建立一個資料夾,並取名叫 xr-cli;
  2. 在該目錄下執行 npm init 命令(你應該有安裝 node 吧?),一路回車,就會生成一個生成 package.json 檔案,在 package.json 裡面寫入以下依賴並執行 npm install 安裝,如下:
"dependencies": {
    "chalk": "^2.4.2",
    "commander": "^2.19.0",
    "download-git-repo": "^1.1.0",
    "inquirer": "^6.2.2",
    "ora": "^3.2.0"
}
複製程式碼
  1. 新建一個 bin 資料夾,並在 bin 目錄下新建一個無字尾名的 xr 檔案,並寫上:
#!/usr/bin/env node
console.log('hello');
複製程式碼

這個檔案就是我們整個腳手架的入口檔案,我們用 node ./bin/xr 執行一下,就能在控制檯列印出 hello,如下圖:

仿 vue-cli 搭建屬於自己的腳手架
這裡要注意開頭的 #!/usr/bin/env node 這個語句必須加上,主要是為了讓系統看到這一行的時候,會沿著該路徑去查詢 node 並執行,主要是為了相容 Mac ,確保可執行。

bin 目錄初始化

當前,bin 目錄下就只有一個檔案,就是入口檔案 xr。所以現在我們先來編寫這個檔案,由於內容較少,我們直接看程式碼:

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

// 定義當前版本
// 定義使用方法
// 定義四個指令
program
  .version(require('../package').version)
  .usage('<command> [options]')
  .command('add', 'add a new template')
  .command('delete', 'delete a template')
  .command('list', 'list all the templates')
  .command('init', 'generate a new project from a template')
  
// 解析命令列引數
program.parse(process.argv)
複製程式碼

這個檔案的主要作用就是定義指令,現在我們用 node ./bin/xr 執行一下,就能看到如下結果:

仿 vue-cli 搭建屬於自己的腳手架
當然,你可能會覺得每次輸入 node ./bin/xr 這個命令有點麻煩,沒關係,我們可以在 package.json 裡面寫入已下內容:

// bin 用來指定每個命令所對應的可執行檔案的位置
"bin": {
    "xr": "bin/xr"
}
複製程式碼

然後在根目錄下執行 npm link(就是把命令掛載到全域性的意思),這樣我們每次只要輸入 xr,就可以直接執行了,so cool,就像下面這樣:

仿 vue-cli 搭建屬於自己的腳手架
是不是好像有點樣子了呢???,那就讓我們繼續完善下 bin 目錄吧!ok,讓我們在 bin 目錄下再新建四個檔案,分別對應上面的四個指令,然後分別處理四個指令要做的事情,如下圖:
仿 vue-cli 搭建屬於自己的腳手架
同樣的,我們修改一下 package.json 裡面的 bin 內容,如下:

"bin": {
    "xr": "bin/xr",
    "xr-add": "bin/xr-add",
    "xr-delete": "bin/xr-delete",
    "xr-list": "bin/xr-list",
    "xr-init": "bin/xr-init"
}
複製程式碼

然後執行 npm unlink 解綁全域性命令,再執行 npm link 重新把命令繫結到全域性,就像下面這樣:

仿 vue-cli 搭建屬於自己的腳手架
最後順便在根目錄下新建一個 template.json 檔案,裡面的內容就是一個 {}

編寫具體指令

好了,一切準備就緒,接下來就讓我們來寫下具體的四個指令吧。

xr-add

這個內容也是比較少,直接看程式碼:

#!/usr/bin/env node

// 互動式命令列
const inquirer = require('inquirer')
// 修改控制檯字串的樣式
const chalk = require('chalk')
// node 內建檔案模組
const fs = require('fs')
// 讀取根目錄下的 template.json
const tplObj = require(`${__dirname}/../template`)

// 自定義互動式命令列的問題及簡單的校驗
let question = [
  {
    name: "name",
    type: 'input',
    message: "請輸入模板名稱",
    validate (val) {
      if (val === '') {
        return 'Name is required!'
      } else if (tplObj[val]) {
        return 'Template has already existed!'
      } else {
        return true
      }
    }
  },
  {
    name: "url",
    type: 'input',
    message: "請輸入模板地址",
    validate (val) {
      if (val === '') return 'The url is required!'
      return true
    }
  }
]

inquirer
  .prompt(question).then(answers => {
    // answers 就是使用者輸入的內容,是個物件
    let { name, url } = answers;
    // 過濾 unicode 字元
    tplObj[name] = url.replace(/[\u0000-\u0019]/g, '')
    // 把模板資訊寫入 template.json 檔案中
    fs.writeFile(`${__dirname}/../template.json`, JSON.stringify(tplObj), 'utf-8', err => {
      if (err) console.log(err)
      console.log('\n')
      console.log(chalk.green('Added successfully!\n'))
      console.log(chalk.grey('The latest template list is: \n'))
      console.log(tplObj)
      console.log('\n')
    })
  })
複製程式碼

這個檔案主要目的就是新增模板並儲存起來,上面的註釋應該都寫的挺清楚了。我們執行 xr add 來看看效果:

仿 vue-cli 搭建屬於自己的腳手架
這裡的模板名稱(自己隨便取)相當於 vue init webpack project-name 當中的 webpack;模板地址要注意一下,像下面這樣寫就可以,這裡以 github 為例:
仿 vue-cli 搭建屬於自己的腳手架
這裡補充一下 xr add 怎麼對應到 xr-add 的:我們前面在定義 program.command('add').action(() => {}) 的時候沒有寫 action 這個回撥函式,而當我們執行 xr add 的時候,commander 會嘗試在入口指令碼的目錄中搜尋可執行檔案,找到形如 program-command(這裡就是 xr-add)的命令來執行,大概是這麼個意思,下面的命令也是一樣的道理。

xr-delete

如果你理解了上面的那個步驟,這步對你來說應該也是灑灑水啦!上程式碼:

#!/usr/bin/env node

const inquirer = require('inquirer')
const chalk = require('chalk')
const fs = require('fs')
const tplObj = require(`${__dirname}/../template`)

let question = [
  {
    name: "name",
    message: "請輸入要刪除的模板名稱",
    validate (val) {
      if (val === '') {
        return 'Name is required!'
      } else if (!tplObj[val]) {
        return 'Template does not exist!'
      } else  {
        return true
      }
    }
  }
]

inquirer
  .prompt(question).then(answers => {
    let { name } = answers;
    delete tplObj[name]
    // 更新 template.json 檔案
    fs.writeFile(`${__dirname}/../template.json`, JSON.stringify(tplObj), 'utf-8', err => {
      if (err) console.log(err)
      console.log('\n')
      console.log(chalk.green('Deleted successfully!\n'))
      console.log(chalk.grey('The latest template list is: \n'))
      console.log(tplObj)
      console.log('\n')
    })
  })
複製程式碼

應該很好理解,就不過多解釋了,我們直接執行 xr delete 看下效果:

仿 vue-cli 搭建屬於自己的腳手架

xr-list

這個更簡單了,兩行程式碼搞定:

#!/usr/bin/env node

const tplObj = require(`${__dirname}/../template`)
console.log(tplObj)
複製程式碼

是不是簡單到爆?。我們執行 xr list 看看效果:

仿 vue-cli 搭建屬於自己的腳手架
因為剛才一新增一刪除,所以目前沒有模板,就輸出 {}

xr-init

這應該是最主要(但不難)的一步了,畢竟我們寫到現在還沒有通過命令初始化過一個專案呢?。所以這步的重點就是執行 download 方法,並傳入相應引數,具體看程式碼:

#!/usr/bin/env node

const program = require('commander')
const chalk = require('chalk')
const ora = require('ora')
const download = require('download-git-repo')
const tplObj = require(`${__dirname}/../template`)

program
  .usage('<template-name> [project-name]')
program.parse(process.argv)
// 當沒有輸入引數的時候給個提示
if (program.args.length < 1) return program.help()

// 好比 vue init webpack project-name 的命令一樣,第一個引數是 webpack,第二個引數是 project-name
let templateName = program.args[0]
let projectName = program.args[1]
// 小小校驗一下引數
if (!tplObj[templateName]) {
  console.log(chalk.red('\n Template does not exit! \n '))
  return
}
if (!projectName) {
  console.log(chalk.red('\n Project should not be empty! \n '))
  return
}

url = tplObj[templateName]

console.log(chalk.white('\n Start generating... \n'))
// 出現載入圖示
const spinner = ora("Downloading...");
spinner.start();
// 執行下載方法並傳入引數
download (
  url,
  projectName,
  err => {
    if (err) {
      spinner.fail();
      console.log(chalk.red(`Generation failed. ${err}`))
      return
    }
    // 結束載入圖示
    spinner.succeed();
    console.log(chalk.green('\n Generation completed!'))
    console.log('\n To get started')
    console.log(`\n    cd ${projectName} \n`)
  }
)
複製程式碼

ok,我們執行一下 xr init simple test,記得先執行一下 xr add

仿 vue-cli 搭建屬於自己的腳手架
現在我們就可以在左側的目錄中看到 test 專案了,如下圖:
仿 vue-cli 搭建屬於自己的腳手架
至此,一個小小的腳手架就做完了。???此處應該有鮮花和掌聲???

釋出到 npm

既然以上命令都執行成功了,那接下來我們就把它釋出到 npm 上吧(寫都寫了,不能浪費?)。

  • 刪除 test 資料夾,它就本地測試用的,用完就拋棄它(當然做人不能這樣)
  • 在根目錄下新建 README.md 檔案,隨便寫點使用說明,假裝正經一下
  • 在根目錄下新建 .npmignore 檔案,並寫入 /node_modules,意思就是釋出的時候忽略 node_modules 資料夾,
  • 去 npm 官網註冊個賬號(很簡單的),同時搜尋一下 xr-cli 這個名字,看看有沒有人用,有的話就換一個羅
    仿 vue-cli 搭建屬於自己的腳手架
    現在讓我們回到專案根目錄,執行 npm login 登入 npm 賬號,再執行 npm publish 釋出,就像下面這樣:
    仿 vue-cli 搭建屬於自己的腳手架
    沒錯,就是這樣兩個簡單的命令,我們就釋出成功啦,真是可喜可賀???。大概過一分鐘左右(反正挺快的),我們再去 npm 官網搜下 xr-cli,就可以看到自己的腳手架啦,哈哈哈哈,賊開心???。
    仿 vue-cli 搭建屬於自己的腳手架

這裡補充說明一點:根據規範,只有在發包的24小時內才允許撤銷釋出的包,所以為了不汙染 npm 網站,如果只是測試的話就執行 npm unpublish --force 刪除吧,畢竟我們都是有素質的人。

小試牛刀

別急,還沒有結束?‍♀️。發都發出去了,怎麼也得驗證一波撒。嗯,說的有道理,無法反駁,那就趕緊驗收吧!這裡我們記得先用 npm unlink 解綁一下命令,不然會相互影響。下面我們開啟終端,輸入 npm i xr-cli -g 全域性安裝一下腳手架,然後執行 xr,如果出現下圖中的模樣就說明已經安裝成功了。

仿 vue-cli 搭建屬於自己的腳手架
接下來進入到桌面,執行 xr init simple xr-test,不一會就可以在桌面上看到自己的專案啦。
仿 vue-cli 搭建屬於自己的腳手架
仿 vue-cli 搭建屬於自己的腳手架
6️⃣6️⃣6️⃣,大讚無疆,大。。贊。。。無疆!!!

結語

上面的操作只要你熟悉了幾遍之後,再去看看 vue-cli 的原始碼結構,你就會有種撥開雲霧見月明的感覺(它只是比我們這個腳手架完善很多很多很多而已???)。
當然了,這只是渣渣版本。你可以往裡面新增更多的東西,比如自動化構建和動態模板啊(其實動態模板是個大頭),然後嘗試寫下更多更好的互動和功能,這樣你就也能擁有一個屬於自己的腳手架啦,心動不如行動,還等什麼呢,不要998,只要有鍵盤,趕緊敲吧同志們,Let's go!?

相關文章