從零搭建一個前端cli腳手架併發布到npm

lzg9527發表於2021-09-26

在日常開發中,我們會根據經驗沉澱出一些專案模板,在不同在專案中可以進行復用。如果是每次都是通過拷貝程式碼到新專案的話,這樣會比較麻煩,而且容易出錯,此時我們就會想能不能將一些模板整合到腳手架(類似vue-cli, create-react-app)中,這樣我們進行初始化建立就能使用了呢?

比如本人有以下兩套開發模板

基於vue-cli4和vant搭建的移動端開發模板 vue-cli4-vant
基於vue-cli4和ant-design-vue構建的後臺管理系統簡單模板 vue-cli4-vant

希望執行類似 aaa init bbb ccc 這樣的命令快速初始化一個專案,無需自己從零開始一步步配置,大大提高了開發效率。

腳手架原始碼地址 點選這裡

安裝使用

npm install cm-vcli -g

cm -h

為什麼需要腳手架?

  1. 減少重複性的工作,不再從零建立一個專案,或者複製貼上另一個專案的程式碼 。
  2. 根據動態互動生成專案結構和配置檔案,具備更高的靈活性和人性化定製的能力 。
  3. 有利於多人開發協作,避免了人工傳遞檔案的繁瑣。
  4. 可以整合多套開發模板,根據專案需要選擇合適的模板。

第三方庫的支援

實現一個腳手架,通常需要以下工具,後續我們將會一一介紹。

  • commander: 命令列工具
  • download-git-repo: 用來下載遠端模板
  • inquirer: 互動式命令列工具
  • ora: 顯示 loading 動畫
  • chalk: 修改控制檯輸出內容樣式
  • log-symbols: 顯示出 √ 或 × 等的圖示
  • handlebars.js 使用者提交的資訊動態填充到檔案中

構建步驟

  1. 新建一個資料夾,命名為 cm-cli(我的腳手架命名),在該目錄下執行 npm init -y 進行初始化,此時就會生產一個 package.json 檔案。
  2. 安裝第三方工具庫
npm install chalk commander download-git-repo inquirer ora log-symbols
  1. 在根目錄下新建一個 bin 資料夾,並在 bin 目錄下新建一個無字尾名的 cm 檔案,並寫上:
#!/usr/bin/env node
console.log('hello world')

這個檔案就是整個腳手架的入口檔案,我們用 node ./bin/cm 執行一下,在控制檯就會列印出 hello world。

當然,每次輸入node ./bin/cm 這個命令有點麻煩,我們可以在 package.json 進行命令配置

"bin": {
  "cm": "bin/cm"
}

此時我們執行 npm link將命令掛載到全域性,然後再輸入 cm 就可以到達剛才node ./bin/cm 的效果了。

  1. 定義多個命令

我們再 bin 下面的 cm 資料夾來定義多個命令,此時就用到 commander 了。首先我們來看一下 commander 的用法

  • usage(): 設定 usage 值
  • command(): 定義一個命令名字
  • description(): 設定 description 值
  • option(): 定義引數,需要設定“關鍵字”和“描述”,關鍵字包括“簡寫”和“全寫”兩部分,以”,”,”|”,”空格”做分隔。
  • parse(): 解析命令列引數 argv
  • action(): 註冊一個 callback 函式
  • version() : 終端輸出版本號

根據日常開發需要,我們建立以下幾個腳手架命令

  • add 新增一個專案模板
  • delete 刪除一個專案模板
  • list 列舉所以專案模板
  • init 初始化一個專案模板

我們先來編寫一下 cm 檔案

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

program.usage('<command>')

program.version(require('../package').version)

program
  .command('add')
  .description('add a new template')
  .action(() => {
    require('../commands/add')
  })

program
  .command('delete')
  .description('delete a template')
  .action(() => {
    require('../commands/delete')
  })

program
  .command('list')
  .description('List the templateList')
  .action(() => {
    require('../commands/list')
  })

program
  .command('init')
  .description('init a project')
  .action(() => {
    require('../commands/init')
  })

program.parse(process.argv)

然後執行一下 cm -h,就會看到以下的效果

cm.png

此時我們再改一下 package.json 的配置

"bin": {
  "cm-add": "bin/cm-add",
  "cm-delete": "bin/cm-delete",
  "cm-list": "bin/cm-list",
  "cm-init": "bin/cm-init"
}

然後執行 npm unlink 解綁全域性命令,再執行 npm link 重新把命令繫結到全域性,這樣就可以直接使用 cm add 等命令了。

編寫指令

在這裡會用到 inquirer 進行命令列互動,我們先來看下 inquirer 的用法,它有以下引數可以配置

  • type:表示提問的型別,包括:input, confirm, list, rawlist, expand, checkbox, password, editor;
  • name: 儲存當前問題回答的變數;
  • message:問題的描述;
  • default:預設值;
  • choices:列表選項,在某些 type 下可用,並且包含一個分隔符(separator);
  • validate:對使用者的回答進行校驗;
  • filter:對使用者的回答進行過濾處理,返回處理後的值;
  • when:根據前面問題的回答,判斷當前問題是否需要被回答;
  • prefix:修改 message 預設字首;
  • suffix:修改 message 預設字尾。

語法結構如下:

const inquirer = require('inquirer')

const question = [
  // 具體互動內容
]

inquirer.prompt(question).then((answers) => {
  console.log(answers) // 返回的結果
})

cm add

新增一個專案模板

  1. 通過命令列互動,讓使用者輸入模板名稱和模板的地址
  2. 將使用者輸入的模板資訊新增寫入到template.json檔案中
  3. 列印出所有的專案模板

看一下程式碼

#!/usr/bin/env node

const inquirer = require('inquirer')
const fs = require('fs')
const templateList = require(`${__dirname}/../template`)
const { showTable } = require(`${__dirname}/../util/showTable`)
const symbols = require('log-symbols')
const chalk = require('chalk')
chalk.level = 1

let question = [
  {
    name: 'name',
    type: 'input',
    message: '請輸入模板名稱',
    validate(val) {
      if (!val) {
        return 'Name is required!'
      } else if (templateList[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) => {
  let { name, url } = answers
  templateList[name] = url.replace(/[\u0000-\u0019]/g, '') // 過濾 unicode 字元
  fs.writeFile(`${__dirname}/../template.json`, JSON.stringify(templateList), 'utf-8', (err) => {
    if (err) console.log(chalk.red(symbols.error), chalk.red(err))
    console.log('\n')
    console.log(chalk.green(symbols.success), chalk.green('Add a template successfully!\n'))
    console.log(chalk.green('The latest templateList is: \n'))
    showTable(templateList)
  })
})

在這裡還用到以下兩個第三方庫,原來美化相互效果:

  • chalk:用來修改控制檯輸出內容樣式的,比如顏色
  • log-symbols: 顯示出 √ 或 × 等的圖示

此時,執行 cm add ,並輸入專案模板名稱和地址,就能看到以下效果了

cmadd.png

cm detele

刪除一個專案模板,這個就好了解,步驟如下

  1. 通過命令列互動,讓使用者輸入要刪除的專案模板名稱
  2. 刪除使用者輸入的模板資料,然後再將更新的資料寫入到template.json檔案中
  3. 列印出所有的專案模板

程式碼如下

#!/usr/bin/env node

const inquirer = require('inquirer')
const fs = require('fs')
const templateList = require(`${__dirname}/../template`)
const { showTable } = require(`${__dirname}/../util/showTable`)
const symbols = require('log-symbols')
const chalk = require('chalk')
chalk.level = 1

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

inquirer.prompt(question).then((answers) => {
  let { name } = answers
  delete templateList[name]
  fs.writeFile(`${__dirname}/../template.json`, JSON.stringify(templateList), 'utf-8', (err) => {
    if (err) console.log(chalk.red(symbols.error), chalk.red(err))
    console.log('\n')
    console.log(chalk.green(symbols.success), chalk.green('Deleted successfully!\n'))
    console.log(chalk.green('The latest templateList is: \n'))
    showTable(templateList)
  })
})

此時,我們執行一下cm delete ,輸入要刪除的模板,就能看到以下效果了

cmdelete.png

cm list

列舉所有的專案模板,這個就更簡單了,直接上程式碼

#!/usr/bin/env node
const { showTable } = require(`${__dirname}/../util/showTable`)
const templateList = require(`${__dirname}/../template`)

showTable(templateList)

此時,我們執行一下cm list ,輸入要刪除的模板,就能看到以下效果了

cmlist.png

cm init

初始化一個專案模板,這是最重要的一部分,步驟如下

  1. 通過命令列互動,讓使用者模板的名稱和專案的名稱
  2. 校驗模板是否存在,專案名稱是否填寫
  3. 開始下載模板,顯示載入圖示
  4. 完成模板下載,隱藏載入圖示

先來看一下程式碼

#!/usr/bin/env node

const program = require('commander')
const ora = require('ora')
const download = require('download-git-repo')
const templateList = require(`${__dirname}/../template`)
const symbols = require('log-symbols')
const chalk = require('chalk')
chalk.level = 1

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

// 第一個引數是 webpack,第二個引數是 project-name
let templateName = program.args[0]
let projectName = program.args[1]

if (!templateList[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
}

let url = templateList[templateName]
console.log(url)

console.log(chalk.green('\n Start generating... \n'))
// 出現載入圖示
const spinner = ora('Downloading...')
spinner.start()

download(`direct:${url}`, `./${projectName}`, { clone: true }, (err) => {
  if (err) {
    spinner.fail()
    console.log(chalk.red(symbols.error), chalk.red(`Generation failed. ${err}`))
    return
  }
  // 結束載入圖示
  spinner.succeed()
  console.log(chalk.green(symbols.success), chalk.green('Generation completed!'))
  console.log('\n To get started')
  console.log(`\n    cd ${projectName} \n`)
})

這裡用到 download-git-repo 下載遠端模板,它的使用方法如下

const download = require('download-git-repo')
download(repository, destination, options, callback)
  • repository 是遠端倉庫地址
  • destination 是存放下載的檔案路徑,也可以直接寫檔名,預設就是當前目錄
  • options 是一些選項,比如 { clone:boolean } 表示用 http download 還是 git clone 的形式下載。
  • callback 是回撥函式

此時,我們執行一下cm init app demo ,就能看到以下效果了,根目錄下就多了一個 demo 資料夾,就是新拉取的專案模板。

cminit.png

至此,一個前端腳手架就正式完成了。下面我們把它釋出到 npm 上。

釋出 npm

釋出流程

  1. 執行 npm login 登陸 npm 賬號,如果沒有賬號的先註冊一個
  2. 執行 npm publish 進行釋出

釋出到 npm 的腳手架名稱就是 package.json 的 name 值,要注意的是釋出名稱不能重複。

釋出完之後,我們來驗證一下。

  1. 執行 npm unlink 解綁一下全域性命令
  2. 執行 npm install cm-vcli -g 全域性安裝腳手架
  3. 執行 cm -h

此時如果看到以下的效果,就說明腳手架已經發布並安裝成功了。

cm.png

相關文章