從0到1搭建一個自己的腳手架
什麼是腳手架
顧名思義,腳手架就是幫助我們配置一些環境、工具,能夠讓我們方便的直接開始開發,專注於我們的業務,不用花時間去配置開發環境。例如常見的vue-cli,我們只需要通過npm i vue-cli -g
全局安裝腳手架後再進行vue init
即可初始化一個自己的項目,真是高大上呢。
接下來我們就要實現一個屬於自己的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
chalk
修改控制檯輸出內容樣式,在這裏可以發揮一下你的藝術細菌了~
const chalk = require('chalk');
console.log(chalk.green('success'));
console.log(chalk.red('error'));
當然它有很多顏色選擇,還有背景色,不一一列舉了,到github上一看就知道了~
inquirer
一個用來設計交互式命令行的工具,非常強大
const inquirer = require('inquirer');
inquirer
.prompt([
// 交互式的問題,例如名字,是否使用ts
])
.then(answers => {
// 回調函數,answers 就是用戶輸入的內容,是個對象
});
類比我們在進行完vue init
後他是不是會問你用不用ts啊,eslint,CSS預處理器等等,就是它完成的
ora
介個東西,就是爲了美觀,下載的時候會有轉圈特效。
const ora = require('ora')
let spinner = ora('downloading template ...')
spinner.start()
同樣的,它也有很多樣式,自行查閱文檔
download-git-repo
顧名思義,這是用來下載遠程倉庫的,也就是我們的模版
const download = require('download-git-repo')
download(repository, destination, options, callback)
repository 是遠程倉庫地址;destination 是存放下載的文件路徑,也可以寫文件名,默認當前目錄;options 是選項,比如 { clone:boolean } 表示用 http download 還是 git clone 。
目錄搭建
當我們瞭解了上面的知識,就開始我們的工作吧~
- 創建一個文件夾,這裏我叫
moe-cli
- 在該目錄下執行
npm init -y
,生成package.json
文件,在文件裏複製下面的依賴,然後執行npm i
"dependencies": {
"chalk": "^3.0.0",
"commander": "^5.0.0",
"download-git-repo": "^3.0.2",
"inquirer": "^7.1.0",
"ora": "^4.0.3"
}
- 新建一個bin文件夾,在bin文件夾下新建一個文件
moe.js
,這個文件夾就是我們腳手架的入口文件,我們可以嘗試寫幾句代碼執行一下:
#!/usr/bin/env node
console.log('hello')
相信你們已經注意到了開頭的#!/usr/bin/env node
,它的作用是當 系統看到這行時,能夠沿着該路徑查找node並執行,主要是爲了兼容mac電腦,確保執行
bin目錄初始化
現在bin目錄下只有這個文件,即入口文件,所以我們先完善這歌文件:
#!/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/moe
太過麻煩,我們可以在package.json
裏定義一個命令:
"bin": {
"moe": "bin/moe.js"
},
bin用來指定每個命令所對應的可執行文件地位置
然後在根目錄下執行npm link
將命令掛載到全局,這樣我們只要輸入moe,就能直接運行了~
是不是非常的斯國一呢😄,接下來我們完善一下bin目錄,將我們入口文件中定義過的四個指令都對應的建立一個js文件,如圖:
同樣的,我們也得修改一下package.json
裏的bin
:
"bin": {
"moe": "bin/moe.js",
"moe-add": "bin/moe-add.js",
"moe-delete": "bin/moe-delete.js",
"moe-list": "bin/moe-list.js",
"moe-init": "bin/moe-init.js"
},
然後先npm unlink
解綁一下,再執行npm link
重新綁定命令。最後我們在根目錄下新建一個template.json
文件,內容爲{}
,作爲我們存放模版的倉庫。
編寫命令
這裏提供代碼,寫好了註釋,代碼不是很難,對照註釋一看就能明白,不需要過多講解
moe-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')
})
})
它的目的是要添加模板並進行存儲,存儲位置就是我們定義過的template.json了。我們執行看一下效果:
注意這裏的模版地址,不需要寫全部鏈接,如圖:
有些人可能會問,我們在入口文件定義program.command('add').action(() => {})
的時候沒有寫action這個回調函數,爲什麼能夠執行moe add
?
其實當我們執行moe add
時,commander會嘗試在入口文件的目錄內尋找可執行文件,找到形如program-command
的命令來執行(moe-add
),下面幾個命令也是同樣的道理
moe-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')
})
})
運行看結果:
moe-list
這個最簡單了
#!/usr/bin/env node
const tplObj = require(`${__dirname}/../template`)
console.log(tplObj)
運行看結果:
moe-init
這是最重要的一步,我們通過這個命令來初始化一個項目,說的通俗易懂點就是通過它下載我們存儲過的模版。
#!/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.cyan('\n Generation completed!'))
console.log(chalk.cyan('\n To get started'))
console.log(chalk.cyan(`\n cd ${projectName} \n`))
}
)
運行看結果:
我們的根目錄下會多出一個test文件夾
至此,簡單的腳手架就做好了!如果你看到這的話,能否留下一個👍鼓勵一下窩~
番外
我們腳手架搭建好了,是不是可以嘗試着發到npm上?想想就刺激~
- 刪除 test 文件夾。
- 在根目錄下新建 README.md 文件,寫上聲明之類的。
- 在根目錄下新建 .npmignore 文件,並寫入 /node_modules,發佈的時候忽略 node_modules 文件夾。
- 去 npm 官網註冊個賬號,同時搜索一下 moe-cli 這個名字,npm不允許重名的包出現。
一切準備就緒後,我們在根目錄下執行npm login
登錄npm帳號
注意紅框標註的地方!一定是npm源
執行npm publish
一鍵發佈
然後過一會你就可以看到你的包了,我這裏報錯是因爲我爲了寫這篇博客把我發過的包刪掉了,需要等待24小時以後才能重新發布。
接下來你就可以嘗試使用npm安裝你發佈的包,並進行使用,是不是很爽!我這裏沒有辦法做演示,所以你可以自己動手操作一下。
最後
上面的操作,如果你熟悉了,再去看看vue-cli的源碼,可能就不會那麼閉塞了,當然它的功能比我們要完善的多~