在日常開發中,我們會根據經驗沉澱出一些專案模板,在不同在專案中可以進行復用。如果是每次都是通過拷貝程式碼到新專案的話,這樣會比較麻煩,而且容易出錯,此時我們就會想能不能將一些模板整合到腳手架(類似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
為什麼需要腳手架?
- 減少重複性的工作,不再從零建立一個專案,或者複製貼上另一個專案的程式碼 。
- 根據動態互動生成專案結構和配置檔案,具備更高的靈活性和人性化定製的能力 。
- 有利於多人開發協作,避免了人工傳遞檔案的繁瑣。
- 可以整合多套開發模板,根據專案需要選擇合適的模板。
第三方庫的支援
實現一個腳手架,通常需要以下工具,後續我們將會一一介紹。
- commander: 命令列工具
- download-git-repo: 用來下載遠端模板
- inquirer: 互動式命令列工具
- ora: 顯示 loading 動畫
- chalk: 修改控制檯輸出內容樣式
- log-symbols: 顯示出 √ 或 × 等的圖示
- handlebars.js 使用者提交的資訊動態填充到檔案中
構建步驟
- 新建一個資料夾,命名為
cm-cli
(我的腳手架命名),在該目錄下執行npm init -y
進行初始化,此時就會生產一個package.json
檔案。 - 安裝第三方工具庫
npm install chalk commander download-git-repo inquirer ora log-symbols
- 在根目錄下新建一個 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
的效果了。
- 定義多個命令
我們再 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
,就會看到以下的效果
此時我們再改一下 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
新增一個專案模板
- 通過命令列互動,讓使用者輸入模板名稱和模板的地址
- 將使用者輸入的模板資訊新增寫入到
template.json
檔案中 - 列印出所有的專案模板
看一下程式碼
#!/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
,並輸入專案模板名稱和地址,就能看到以下效果了
cm detele
刪除一個專案模板,這個就好了解,步驟如下
- 通過命令列互動,讓使用者輸入要刪除的專案模板名稱
- 刪除使用者輸入的模板資料,然後再將更新的資料寫入到
template.json
檔案中 - 列印出所有的專案模板
程式碼如下
#!/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
,輸入要刪除的模板,就能看到以下效果了
cm list
列舉所有的專案模板,這個就更簡單了,直接上程式碼
#!/usr/bin/env node
const { showTable } = require(`${__dirname}/../util/showTable`)
const templateList = require(`${__dirname}/../template`)
showTable(templateList)
此時,我們執行一下cm list
,輸入要刪除的模板,就能看到以下效果了
cm init
初始化一個專案模板,這是最重要的一部分,步驟如下
- 通過命令列互動,讓使用者模板的名稱和專案的名稱
- 校驗模板是否存在,專案名稱是否填寫
- 開始下載模板,顯示載入圖示
- 完成模板下載,隱藏載入圖示
先來看一下程式碼
#!/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 資料夾,就是新拉取的專案模板。
至此,一個前端腳手架就正式完成了。下面我們把它釋出到 npm 上。
釋出 npm
釋出流程
- 執行
npm login
登陸 npm 賬號,如果沒有賬號的先註冊一個 - 執行
npm publish
進行釋出
釋出到 npm 的腳手架名稱就是 package.json 的 name 值,要注意的是釋出名稱不能重複。
釋出完之後,我們來驗證一下。
- 執行
npm unlink
解綁一下全域性命令 - 執行
npm install cm-vcli -g
全域性安裝腳手架 - 執行
cm -h
此時如果看到以下的效果,就說明腳手架已經發布並安裝成功了。