搭建腳手架的目的就是快速的搭建項目的基本結構並提供項目規範和約定。目前平常工做中經常使用的腳手架有 vue-cli、create-react-app、angular-cli 等等,都是經過簡單的初始化命令,完成內容的快速構建。javascript
腳手架是咱們常用的工具,也是團隊提效的重要手段。因此係統性的掌握腳手架相關知識,對前端開發者來講是很是重要的,即便不少人從此不必定都會參與到各自部門或者公司的基建工做,可是系統性掌握好這個技能也能夠方便咱們後期的源碼閱讀。下面就一塊兒來了解一下吧 😉css
腳手架就是在啓動的時候詢問一些簡單的問題,而且經過用戶回答的結果去渲染對應的模板文件,基本工做流程以下:html
- 經過命令行交互詢問用戶問題
- 根據用戶回答的結果生成文件
例如咱們在使用 vue-cli 建立一個 vue 項目時的時候 👇前端
step1:運行建立命令vue
$ vue create hello-world
複製代碼
step2:詢問用戶問題java
step3:生成符合用戶需求的項目文件node
# 忽略部分文件夾
vue-project
├─ index.html
├─ src
│ ├─ App.vue
│ ├─ assets
│ │ └─ logo.png
│ ├─ components
│ │ └─ HelloWorld.vue
│ ├─ main.js
│ └─ router
│ └─ index.js
└─ package.json
複製代碼
參考上面的流程咱們能夠本身來 搭建一個簡單的腳手架雛形react
目標: 實如今命令行執行 my-node-cli
來啓動咱們的腳手架ios
$ mkdir my-node-cli
$ cd my-node-cli
$ npm init # 生成 package.json 文件
複製代碼
$ touch cli.js # 新建 cli.js 文件
複製代碼
在 package.json 文件中指定入口文件爲 cli.js 👇git
{
"name": "my-node-cli",
"version": "1.0.0",
"description": "",
"main": "cli.js",
"bin": "cli.js", // 手動添加入口文件爲 cli.js
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC"
}
複製代碼
此時項目目錄結構:
my-node-cli
├─ cli.js
└─ package.json
複製代碼
打開 cli.js 進行編輯
#! /usr/bin/env node
// #! 符號的名稱叫 Shebang,用於指定腳本的解釋程序
// Node CLI 應用入口文件必需要有這樣的文件頭
// 若是是Linux 或者 macOS 系統下還須要修改此文件的讀寫權限爲 755
// 具體就是經過 chmod 755 cli.js 實現修改
// 用於檢查入口文件是否正常執行
console.log('my-node-cli working~')
複製代碼
$ npm link # or yarn link
複製代碼
執行完成 ✅
咱們就能夠來測試了,在命令行中輸入 my-node-cli 執行一下
$ my-node-cli
複製代碼
這裏咱們就看到命令行中打印了
my-node-cli working~
複製代碼
完成 ✔,接下來
實現與詢問用戶信息的功能須要引入 inquirer.js 👉 文檔看這裏
$ npm install inquirer --dev # yarn add inquirer --dev
複製代碼
接着咱們在 cli.js 來設置咱們的問題
#! /usr/bin/env node
const inquirer = require('inquirer')
inquirer.prompt([
{
type: 'input', //type: input, number, confirm, list, checkbox ...
name: 'name', // key 名
message: 'Your name', // 提示信息
default: 'my-node-cli' // 默認值
}
]).then(answers => {
// 打印互用輸入結果
console.log(answers)
})
複製代碼
在命令行輸入 my-node-cli 看一下執行結果
這裏咱們就拿到了用戶輸入的項目名稱 { name: 'my-app' }
, 👌
$ mkdir templates # 建立模版文件夾
複製代碼
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>
<!-- ejs 語法 -->
<%= name %>
</title>
</head>
<body>
<h1><%= name %></h1>
</body>
</html>
複製代碼
/* common.css */
body {
margin: 20px auto;
background-color: azure;
}
複製代碼
此時的目錄結構
my-node-cli
├─ templates
│ ├─ common.css
│ └─ index.html
├─ cli.js
├─ package-lock.json
└─ package.json
複製代碼
這裏藉助 ejs 模版引擎將用戶輸入的數據渲染到模版文件上
npm install ejs --save # yarn add ejs --save
複製代碼
完善後到 cli.js 👇
#! /usr/bin/env node
const inquirer = require('inquirer')
const path = require('path')
const fs = require('fs')
const ejs = require('ejs')
inquirer.prompt([
{
type: 'input', //type:input,confirm,list,rawlist,checkbox,password...
name: 'name', // key 名
message: 'Your name', // 提示信息
default: 'my-node-cli' // 默認值
}
]).then(answers => {
// 模版文件目錄
const destUrl = path.join(__dirname, 'templates');
// 生成文件目錄
// process.cwd() 對應控制檯所在目錄
const cwdUrl = process.cwd();
// 從模版目錄中讀取文件
fs.readdir(destUrl, (err, files) => {
if (err) throw err;
files.forEach((file) => {
// 使用 ejs 渲染對應的模版文件
// renderFile(模版文件地址,傳入渲染數據)
ejs.renderFile(path.join(destUrl, file), answers).then(data => {
// 生成 ejs 處理後的模版文件
fs.writeFileSync(path.join(cwdUrl, file) , data)
})
})
})
})
複製代碼
一樣,在控制檯執行一下 my-node-cli ,此時 index.html
、common.css
已經成功建立 ✔
咱們打印一下當前的目錄結構 👇
my-node-cli
├─ templates
│ ├─ common.css
│ └─ index.html
├─ cli.js
├─ common.css .................... 生成對應的 common.css 文件
├─ index.html .................... 生成對應的 index.html 文件
├─ package-lock.json
└─ package.json
複製代碼
打開生成的 index.html 文件看一下
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- ejs 語法 -->
<title>
my-app
</title>
</head>
<body>
<h1>my-app</h1>
</body>
</html>
複製代碼
用戶輸入的 { name: 'my-app' }
已經添加到了生成的文件中了 ✌️
點此打開 👉 my-node-cli 源碼地址
實際生產中搭建一個腳手架或者閱讀其餘腳手架源碼的時候須要瞭解下面這些工具庫 👇
名稱 | 簡介 |
---|---|
commander | 命令行自定義指令 |
inquirer | 命令行詢問用戶問題,記錄回答結果 |
chalk | 控制檯輸出內容樣式美化 |
ora | 控制檯 loading 樣式 |
figlet | 控制檯打印 logo |
easy-table | 控制檯輸出表格 |
download-git-repo | 下載遠程模版 |
fs-extra | 系統fs模塊的擴展,提供了更多便利的 API,並繼承了fs模塊的 API |
cross-spawn | 支持跨平臺調用系統上的命令 |
重點介紹下面這些,其餘工具能夠查看說明文檔
更多用法 👉 中文文檔
簡單案例 👇
// package.json
{
"name": "my-vue",
"version": "1.0.0",
"description": "",
"bin": "./bin/cli.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "T-Roc",
"license": "ISC",
"devDependencies": {
"commander": "^7.2.0"
}
}
複製代碼
目錄結構:
npms-demo
├─ bin
│ └─ cli.js
├─ package-lock.json
└─ package.json
複製代碼
# 安裝依賴
npm install commander # yarn add commander
複製代碼
完善 bin.js 代碼
#! /usr/bin/env node
const program = require('commander')
program
.version('0.1.0')
.command('create <name>')
.description('create a new project')
.action(name => {
// 打印命令行輸入的值
console.log("project name is " + name)
})
program.parse()
複製代碼
npm link
將應用 my-vue
連接到全局my-vue
看一下,命令行中的輸出內容 👇
~/Desktop/cli/npms-demo ->my-vue
Usage: my-vue [options] [command]
Options:
-V, --version output the version number
-h, --help display help for command
Commands:
create <name> create a new project
help [command] display help for command
複製代碼
這個時候就有了 my-vue
命令使用的說明信息,在 Commands 下面出現了咱們剛剛建立的 create 命令 create <name>
,咱們在命令行中運行一下
~/Desktop/cli/npms-demo ->my-vue create my-app
project name is my-app
複製代碼
這個時候控制檯就打印出來 create 命令後面的 <name>
值 my-app
👏
chalk(粉筆)能夠美化咱們在命令行中輸出內容的樣式,例如對重點信息添加顏色
npm install chalk # yarn add chalk
複製代碼
在 npms-demo 項目中打開 bin/cli.js
#! /usr/bin/env node
const program = require('commander')
const chalk = require('chalk')
program
.version('0.1.0')
.command('create <name>')
.description('create a new project')
.action(name => {
// 打印命令行輸入的值
// 文本樣式
console.log("project name is " + chalk.bold(name))
// 顏色
console.log("project name is " + chalk.cyan(name))
console.log("project name is " + chalk.green(name))
// 背景色
console.log("project name is " + chalk.bgRed(name))
// 使用RGB顏色輸出
console.log("project name is " + chalk.rgb(4, 156, 219).underline(name));
console.log("project name is " + chalk.hex('#049CDB').bold(name));
console.log("project name is " + chalk.bgHex('#049CDB').bold(name))
})
program.parse()
複製代碼
在命令行中運行項目 my-vue create my-app
看一下效果
具體的樣式對照表以下 👇
更多用法 👉 文檔地址
inquirer 在腳手架工具中的使用頻率是很是高的,其實在上文腳手架的簡單雛形中,咱們已經使用到了,這裏就不過多介紹了。
更多用法 👉 文檔地址
// 自定義文本信息
const message = 'Loading unicorns'
// 初始化
const spinner = ora(message);
// 開始加載動畫
spinner.start();
setTimeout(() => {
// 修改動畫樣式
// Type: string
// Default: 'cyan'
// Values: 'black' | 'red' | 'green' | 'yellow' | 'blue' | 'magenta' | 'cyan' | 'white' | 'gray'
spinner.color = 'red';
spinner.text = 'Loading rainbows';
setTimeout(() => {
// 加載狀態修改
spinner.stop() // 中止
spinner.succeed('Loading succeed'); // 成功 ✔
// spinner.fail(text?); 失敗 ✖
// spinner.warn(text?); 提示 ⚠
// spinner.info(text?); 信息 ℹ
}, 2000);
}, 2000);
複製代碼
命令行是輸出效果以下
更多用法 👉 文檔地址
在腳手架裏面,能夠用來自動執行 shell 命令,例如:
#! /usr/bin/env node
const spawn = require('cross-spawn');
const chalk = require('chalk')
// 定義須要按照的依賴
const dependencies = ['vue', 'vuex', 'vue-router'];
// 執行安裝
const child = spawn('npm', ['install', '-D'].concat(dependencies), {
stdio: 'inherit'
});
// 監聽執行結果
child.on('close', function(code) {
// 執行失敗
if(code !== 0) {
console.log(chalk.red('Error occurred while installing dependencies!'));
process.exit(1);
}
// 執行成功
else {
console.log(chalk.cyan('Install finished'))
}
})
複製代碼
一樣的在命令行執行一下 my-vue
看一下執行結果
成功安裝 👍
先給咱們的腳手架起個名字吧,正好祝融號登錄了火星,不如就叫:zhurong-cli 😆
.-') _ ('-. .-. _ .-') .-') _
( OO) )( OO ) / ( \( -O ) ( OO ) )
,(_)----. ,--. ,--. ,--. ,--. ,------. .-'),-----. ,--./ ,--,' ,----.
| | | | | | | | | | | /`. '( OO' .-. '| \ | |\ ' .-./-') '--. / | .| | | | | .-') | / | |/ | | | || \| | )| |_( O- ) (_/ / | | | |_|( OO )| |_.' |\_) | |\| || . |/ | | .--, \
/ /___ | .-. | | | | `-' /| . '.' \ | | | || |\ | (| | '. (_/
| || | | |(' '-'(_.-' | |\ \ `' '-' '| | \ | | '--' |
`--------'`--' `--' `-----' `--' '--' `-----' `--' `--' `------' 複製代碼
須要實現哪些基本功能:
zr create <name>
命令啓動項目搭建步驟拆解:
參照前面的例子,先建立一個簡單的 Node-Cli 結構
zhurong-cli
├─ bin
│ └─ cli.js # 啓動文件
├─ README.md
└─ package.json
複製代碼
配置腳手架啓動文件
{
"name": "zhurong-cli",
"version": "1.0.0",
"description": "simple vue cli",
"main": "index.js",
"bin": {
"zr": "./bin/cli.js" // 配置啓動文件路徑,zr 爲別名
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": {
"name": "T-Roc",
"email": "lxp_work@163.com"
},
"license": "MIT"
}
複製代碼
簡單編輯一下咱們的 cli.js
#! /usr/bin/env node
console.log('zhurong-cli working ~')
複製代碼
爲了方便開發調試,使用 npm link
連接到全局
~/Desktop/cli/zhurong-cli ->npm link
npm WARN zhurong-cli@1.0.0 No repository field.
up to date in 1.327s
found 0 vulnerabilities
/usr/local/bin/zr -> /usr/local/lib/node_modules/zhurong-cli/bin/cli.js
/usr/local/lib/node_modules/zhurong-cli -> /Users/Desktop/cli/zhurong-cli
複製代碼
完成以後,接着測試一下
~/Desktop/cli/zhurong-cli ->zr
zhurong-cli working ~ # 打印內容
複製代碼
OK,獲得了咱們想要的打印內容,接下來
簡單分析一下咱們要怎麼作?
如今開始吧 😉
$ npm install commander --save
複製代碼
安裝完成以後 👇
打開 cli.js 進行編輯
#! /usr/bin/env node
const program = require('commander')
program
// 定義命令和參數
.command('create <app-name>')
.description('create a new project')
// -f or --force 爲強制建立,若是建立的目錄存在則直接覆蓋
.option('-f, --force', 'overwrite target directory if it exist')
.action((name, options) => {
// 打印執行結果
console.log('name:',name,'options:',options)
})
program
// 配置版本號信息
.version(`v${require('../package.json').version}`)
.usage('<command> [option]')
// 解析用戶執行命令傳入參數
program.parse(process.argv);
複製代碼
在命令行輸入 zr,檢查一下命令是否建立成功
~/Desktop/cli/zhurong-cli ->zr
Usage: zr <command> [option]
Options:
-V, --version output the version number
-h, --help display help for command
Commands:
create [options] <app-name> create a new project
help [command] display help for command
複製代碼
咱們能夠看到 Commands 下面已經有了 create [options] <app-name>
,接着執行一下這個命令
~/Desktop/cli/zhurong-cli ->zr create
error: missing required argument 'app-name'
~/Desktop/cli/zhurong-cli ->zr create my-project
執行結果 >>> name: my-project options: {}
~/Desktop/cli/zhurong-cli ->zr create my-project -f
執行結果 >>> name: my-project options: { force: true }
~/Desktop/cli/zhurong-cli ->zr create my-project --force
執行結果 >>> name: my-project options: { force: true }
複製代碼
成功拿到命令行輸入信息 👍
建立 lib 文件夾並在文件夾下建立 create.js
// lib/create.js
module.exports = async function (name, options) {
// 驗證是否正常取到值
console.log('>>> create.js', name, options)
}
複製代碼
在 cli.js 中使用 create.js
// bin/cli.js
......
program
.command('create <app-name>')
.description('create a new project')
.option('-f, --force', 'overwrite target directory if it exist') // 是否強制建立,當文件夾已經存在
.action((name, options) => {
// 在 create.js 中執行建立任務
require('../lib/create.js')(name, options)
})
......
複製代碼
執行一下 zr create my-project
,此時在 create.js 正常打印了咱們出入的信息
~/Desktop/cli/zhurong-cli ->zr create my-project
>>> create.js
my-project {}
複製代碼
在建立目錄的時候,須要思考一個問題:目錄是否已經存在?
{ force: true }
時,直接移除原來的目錄,直接建立{ force: false }
時 詢問用戶是否須要覆蓋這裏用到了 fs 的擴展工具 fs-extra,先來安裝一下
# fs-extra 是對 fs 模塊的擴展,支持 promise
$ npm install fs-extra --save
複製代碼
咱們接着完善一下 create.js 內部的實現邏輯
// lib/create.js
const path = require('path')
const fs = require('fs-extra')
module.exports = async function (name, options) {
// 執行建立命令
// 當前命令行選擇的目錄
const cwd = process.cwd();
// 須要建立的目錄地址
const targetAir = path.join(cwd, name)
// 目錄是否已經存在?
if (fs.existsSync(targetAir)) {
// 是否爲強制建立?
if (options.force) {
await fs.remove(targetAir)
} else {
// TODO:詢問用戶是否肯定要覆蓋
}
}
}
複製代碼
詢問部分的邏輯,咱們將在下文繼續完善
若是想添加其餘命令也是一樣的處理方式,這裏就不擴展說明了,示例以下 👇
// bin/cli.js
// 配置 config 命令
program
.command('config [value]')
.description('inspect and modify the config')
.option('-g, --get <path>', 'get value from option')
.option('-s, --set <path> <value>')
.option('-d, --delete <path>', 'delete option from config')
.action((value, options) => {
console.log(value, options)
})
// 配置 ui 命令
program
.command('ui')
.description('start add open roc-cli ui')
.option('-p, --port <port>', 'Port used for the UI Server')
.action((option) => {
console.log(option)
})
複製代碼
咱們先看一下 vue-cli 執行 --help 打印的信息
對比 zr --help
打印的結果,結尾處少了一條說明信息,這裏咱們作補充,重點須要注意說明信息是帶有顏色的,這裏就須要用到咱們工具庫裏面的 chalk 來處理
// bin/cli.js
program
// 監聽 --help 執行
.on('--help', () => {
// 新增說明信息
console.log(`\r\nRun ${chalk.cyan(`zr <command> --help`)} for detailed usage of given command\r\n`)
})
複製代碼
若是此時咱們想給腳手架整個 Logo,工具庫裏的 figlet 就是幹這個的 😎
// bin/cli.js
program
.on('--help', () => {
// 使用 figlet 繪製 Logo
console.log('\r\n' + figlet.textSync('zhurong', {
font: 'Ghost',
horizontalLayout: 'default',
verticalLayout: 'default',
width: 80,
whitespaceBreak: true
}));
// 新增說明信息
console.log(`\r\nRun ${chalk.cyan(`roc <command> --help`)} show details\r\n`)
})
複製代碼
咱們再看看此時的 zr --help
打印出來的是個什麼樣子
看起來仍是挺不錯的,哈哈 😄
這裏召喚咱們的老朋友 inquirer,讓他來幫咱們解決命令行交互的問題
接下來咱們要作的:
這裏解決上一步遺留的問題:
{ force: false }
時 詢問用戶是否須要覆蓋邏輯實際上已經完成,這裏補充一下詢問的內容
首選來安裝一下 inquirer
$ npm install inquirer --save
複製代碼
而後詢問用戶是否進行 Overwrite
// lib/create.js
const path = require('path')
// fs-extra 是對 fs 模塊的擴展,支持 promise 語法
const fs = require('fs-extra')
const inquirer = require('inquirer')
module.exports = async function (name, options) {
// 執行建立命令
// 當前命令行選擇的目錄
const cwd = process.cwd();
// 須要建立的目錄地址
const targetAir = path.join(cwd, name)
// 目錄是否已經存在?
if (fs.existsSync(targetAir)) {
// 是否爲強制建立?
if (options.force) {
await fs.remove(targetAir)
} else {
// 詢問用戶是否肯定要覆蓋
let { action } = await inquirer.prompt([
{
name: 'action',
type: 'list',
message: 'Target directory already exists Pick an action:',
choices: [
{
name: 'Overwrite',
value: 'overwrite'
},{
name: 'Cancel',
value: false
}
]
}
])
if (!action) {
return;
} else if (action === 'overwrite') {
// 移除已存在的目錄
console.log(`\r\nRemoving...`)
await fs.remove(targetAir)
}
}
}
}
複製代碼
咱們來測試一下:
zr create my-project
,效果以下zr create my-project2 --f
,能夠直接看到 my-project2 被移除⚠️注意:爲何這裏只作移除? 由於後面獲取到模板地址後,下載的時候會直接建立項目目錄
模板我已經上傳到遠程倉庫:github.com/zhurong-cli
vue3.0-template 版本信息 👇
vue-template 版本信息 👇
github 提供了
咱們在 lib 目錄下建立一個 http.js 專門處理模板和版本信息的獲取
// lib/http.js
// 經過 axios 處理請求
const axios = require('axios')
axios.interceptors.response.use(res => {
return res.data;
})
/** * 獲取模板列表 * @returns Promise */
async function getRepoList() {
return axios.get('https://api.github.com/orgs/zhurong-cli/repos')
}
/** * 獲取版本信息 * @param {string} repo 模板名稱 * @returns Promise */
async function getTagList(repo) {
return axios.get(`https://api.github.com/repos/zhurong-cli/${repo}/tags`)
}
module.exports = {
getRepoList,
getTagList
}
複製代碼
咱們專門新建一個 Generator.js 來處理項目建立邏輯
// lib/Generator.js
class Generator {
constructor (name, targetDir){
// 目錄名稱
this.name = name;
// 建立位置
this.targetDir = targetDir;
}
// 核心建立邏輯
create(){
}
}
module.exports = Generator;
複製代碼
在 create.js 中引入 Generator 類
// lib/create.js
...
const Generator = require('./Generator')
module.exports = async function (name, options) {
// 執行建立命令
// 當前命令行選擇的目錄
const cwd = process.cwd();
// 須要建立的目錄地址
const targetAir = path.join(cwd, name)
// 目錄是否已經存在?
if (fs.existsSync(targetAir)) {
...
}
// 建立項目
const generator = new Generator(name, targetAir);
// 開始建立項目
generator.create()
}
複製代碼
接着來寫詢問用戶選擇模版都邏輯
// lib/Generator.js
const { getRepoList } = require('./http')
const ora = require('ora')
const inquirer = require('inquirer')
// 添加加載動畫
async function wrapLoading(fn, message, ...args) {
// 使用 ora 初始化,傳入提示信息 message
const spinner = ora(message);
// 開始加載動畫
spinner.start();
try {
// 執行傳入方法 fn
const result = await fn(...args);
// 狀態爲修改成成功
spinner.succeed();
return result;
} catch (error) {
// 狀態爲修改成失敗
spinner.fail('Request failed, refetch ...')
}
}
class Generator {
constructor (name, targetDir){
// 目錄名稱
this.name = name;
// 建立位置
this.targetDir = targetDir;
}
// 獲取用戶選擇的模板
// 1)從遠程拉取模板數據
// 2)用戶選擇本身新下載的模板名稱
// 3)return 用戶選擇的名稱
async getRepo() {
// 1)從遠程拉取模板數據
const repoList = await wrapLoading(getRepoList, 'waiting fetch template');
if (!repoList) return;
// 過濾咱們須要的模板名稱
const repos = repoList.map(item => item.name);
// 2)用戶選擇本身新下載的模板名稱
const { repo } = await inquirer.prompt({
name: 'repo',
type: 'list',
choices: repos,
message: 'Please choose a template to create project'
})
// 3)return 用戶選擇的名稱
return repo;
}
// 核心建立邏輯
// 1)獲取模板名稱
// 2)獲取 tag 名稱
// 3)下載模板到模板目錄
async create(){
// 1)獲取模板名稱
const repo = await this.getRepo()
console.log('用戶選擇了,repo=' + repo)
}
}
module.exports = Generator;
複製代碼
測試一下,看看如今是個什麼樣子
我選擇了默認的 vue-template,此時
成功拿到模板名稱 repo 的結果 ✌️
過程和 3.3 同樣
// lib/generator.js
const { getRepoList, getTagList } = require('./http')
...
// 添加加載動畫
async function wrapLoading(fn, message, ...args) {
...
}
class Generator {
constructor (name, targetDir){
// 目錄名稱
this.name = name;
// 建立位置
this.targetDir = targetDir;
}
// 獲取用戶選擇的模板
// 1)從遠程拉取模板數據
// 2)用戶選擇本身新下載的模板名稱
// 3)return 用戶選擇的名稱
async getRepo() {
...
}
// 獲取用戶選擇的版本
// 1)基於 repo 結果,遠程拉取對應的 tag 列表
// 2)用戶選擇本身須要下載的 tag
// 3)return 用戶選擇的 tag
async getTag(repo) {
// 1)基於 repo 結果,遠程拉取對應的 tag 列表
const tags = await wrapLoading(getTagList, 'waiting fetch tag', repo);
if (!tags) return;
// 過濾咱們須要的 tag 名稱
const tagsList = tags.map(item => item.name);
// 2)用戶選擇本身須要下載的 tag
const { tag } = await inquirer.prompt({
name: 'tag',
type: 'list',
choices: tagsList,
message: 'Place choose a tag to create project'
})
// 3)return 用戶選擇的 tag
return tag
}
// 核心建立邏輯
// 1)獲取模板名稱
// 2)獲取 tag 名稱
// 3)下載模板到模板目錄
async create(){
// 1)獲取模板名稱
const repo = await this.getRepo()
// 2) 獲取 tag 名稱
const tag = await this.getTag(repo)
console.log('用戶選擇了,repo=' + repo + ',tag='+ tag)
}
}
module.exports = Generator;
複製代碼
測試一下,執行 zr create my-project
選擇好了以後,看看打印結果
到此詢問的工做就結束了,能夠進行模板下載了
下載遠程模版須要使用 download-git-repo 工具包,實際上它也在咱們上面列的工具菜單上,可是在使用它的時候,須要注意一個問題,就是它是不支持 promise的,因此咱們這裏須要使用 使用 util 模塊中的 promisify 方法對其進行 promise 化
$ npm install download-git-repo --save
複製代碼
進行 promise 化處理
// lib/Generator.js
...
const util = require('util')
const downloadGitRepo = require('download-git-repo') // 不支持 Promise
class Generator {
constructor (name, targetDir){
...
// 對 download-git-repo 進行 promise 化改造
this.downloadGitRepo = util.promisify(downloadGitRepo);
}
...
}
複製代碼
接着,就是模板下載部分的邏輯了
// lib/Generator.js
...
const util = require('util')
const path = require('path')
const downloadGitRepo = require('download-git-repo') // 不支持 Promise
// 添加加載動畫
async function wrapLoading(fn, message, ...args) {
...
}
class Generator {
constructor (name, targetDir){
...
// 對 download-git-repo 進行 promise 化改造
this.downloadGitRepo = util.promisify(downloadGitRepo);
}
...
// 下載遠程模板
// 1)拼接下載地址
// 2)調用下載方法
async download(repo, tag){
// 1)拼接下載地址
const requestUrl = `zhurong-cli/${repo}${tag?'#'+tag:''}`;
// 2)調用下載方法
await wrapLoading(
this.downloadGitRepo, // 遠程下載方法
'waiting download template', // 加載提示信息
requestUrl, // 參數1: 下載地址
path.resolve(process.cwd(), this.targetDir)) // 參數2: 建立位置
}
// 核心建立邏輯
// 1)獲取模板名稱
// 2)獲取 tag 名稱
// 3)下載模板到模板目錄
// 4)模板使用提示
async create(){
// 1)獲取模板名稱
const repo = await this.getRepo()
// 2) 獲取 tag 名稱
const tag = await this.getTag(repo)
// 3)下載模板到模板目錄
await this.download(repo, tag)
// 4)模板使用提示
console.log(`\r\nSuccessfully created project ${chalk.cyan(this.name)}`)
console.log(`\r\n cd ${chalk.cyan(this.name)}`)
console.log(' npm run dev\r\n')
}
}
module.exports = Generator;
複製代碼
完成這塊,一個簡單的腳手架就完成了 ✅
來試一下效果如何,執行 zr create my-project
這個時候,咱們就能夠看到模板就已經建立好了 👏👏👏
zhurong-cli
├─ bin
│ └─ cli.js
├─ lib
│ ├─ Generator.js
│ ├─ create.js
│ └─ http.js
├─ my-project .............. 咱們建立的項目
│ ├─ public
│ │ ├─ favicon.ico
│ │ └─ index.html
│ ├─ src
│ │ ├─ assets
│ │ │ └─ logo.png
│ │ ├─ components
│ │ │ └─ HelloWorld.vue
│ │ ├─ App.vue
│ │ └─ main.js
│ ├─ README.md
│ ├─ babel.config.js
│ └─ package.json
├─ README.md
├─ package-lock.json
└─ package.json
複製代碼
上面都是在本地測試,實際在使用的時候,可能就須要發佈到 npm 倉庫,經過 npm 全局安裝以後,直接到目標目錄下面去建立項目,如何發佈呢?
{
"name": "zhurong-cli",
"version": "1.0.4",
"description": "",
"main": "index.js",
"bin": {
"zr": "./bin/cli.js"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"files": [
"bin",
"lib"
],
"author": {
"name": "T-Roc",
"email": "lxp_work@163.com"
},
"keywords": [
"zhurong-cli",
"zr",
"腳手架"
],
"license": "MIT",
"dependencies": {
"axios": "^0.21.1",
"chalk": "^4.1.1",
"commander": "^7.2.0",
"download-git-repo": "^3.0.2",
"figlet": "^1.5.0",
"fs-extra": "^10.0.0",
"inquirer": "^8.0.0",
"ora": "^5.4.0"
}
}
複製代碼
npm publish
進行發佈,更新到時候,注意修改版本號這樣就發佈成功了,咱們打開 npm 網站搜索一下 🔍
已經能夠找到它了,這樣咱們就能夠經過 npm 或者 yarn 全局安裝使用了
點此打開 👉 zhurong-cli 源碼地址
Yeoman 最初發佈於 2012 年,它是一款高效、開源的 Web 應用腳手架(scaffolding)軟件,意在精簡軟件的開發過程。腳手架軟件用於實現項目中多種不一樣的工具和接口的協同使用,優化項目的生成過程。容許建立任何類型的應用程序(Web,Java,Python,C#等)。
Yeoman 其實是三個工具的總和:
使用 Yeoman 搭建腳手架很是簡單,Yeoman 提供了 yeoman-generator
讓咱們快速生成一個腳手架模板,咱們能夠經過各種 Generator 實現任何類型的項目搭建,下面咱們來試一下 🤓
Yeoman 是一套構建系統,在這裏咱們搭建腳手架須要使用的就是 yo 👇
$ npm install yo --global # or yarn global add yo
複製代碼
yo 搭配不一樣 generator-xxx
能夠建立對應的項目,例如 generator-webapp
、generator-node
、generator-vue
等等,這裏咱們使用 generator-node
來演示操做。
$ npm install generator-node --global # or yarn global add generator-node
複製代碼
$ mkdir yo-project
$ cd yo-project
$ yo node
複製代碼
這樣咱們就經過 yo + generator-node
快捷搭建一個 node 項目,目錄結構以下 👇
yo-project
├─ .editorconfig
├─ .eslintignore
├─ .travis.yml
├─ .yo-rc.json
├─ LICENSE
├─ README.md
├─ lib
│ ├─ __tests__
│ │ └─ testCli.test.js
│ └─ index.js
├─ package-lock.json
└─ package.json
複製代碼
如何查找本身須要的 generator 呢?咱們能夠去官網 generators 列表搜索 👉 點此進入
這種使用方式真的很是的簡單方便,可是它的問題也很明顯--不夠靈活,畢竟不一樣的團隊在使用的技術棧上都有所差別,若是咱們想搭建本身想要的項目結構要怎麼處理呢? 接着往下看 👇
自定義 Generator 實際上就是建立一個特定結構的 npm 包,這個特定的結構是這樣的 👇
generator-xxx ............ 自定義項目目錄
├─ generators ............ 生成器目錄
│ └─ app ................ 默認生成器目錄
│ └─ index.js ........ 默認生成器實現
└─ package.json .......... 模塊包配置文件
複製代碼
或者這樣的 👇
generator-xxx
├─ app
│ └─ index.js
├─ router
│ └─ index.js
└─ package.json
複製代碼
這裏咱們須要注意的是,項目的名稱必須是 generator-<name>
格式,才能夠正常被 yo 識別出來,例如上面舉例使用的 generator-node。
$ mkdir generator-simple # 建立項目
$ cd generator-simple # 進入項目目錄
複製代碼
$ npm init # or yarn init
複製代碼
一路 enter 以後咱們就生成好了 package.json,不過咱們還須要額外檢查一下:
name
屬性值須是 "generator-<name>"keyword
中必須包含 yeoman-generatorfiles
屬性要指向項目的模板目錄。完成上面的工做以後咱們看一下 package.json 是個什麼樣子
{
"name": "generator-simple",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [
"yeoman-generator"
],
"files": [
"generators"
],
"author": "ITEM",
"license": "MIT"
}
複製代碼
⚠️注意:這裏若是使用的是第二種目錄結構,那麼 package.json 中須要作點修改 🔧
{
"files": [
"app",
"router"
]
}
複製代碼
yeoman-generator
yeoman-generator
是 Yeoman 提供的一個 Generator 基類,讓咱們在建立自定義 Generator 的時候更加便捷。
$ npm install yeoman-generator --save # or yarn add yeoman-generator
複製代碼
在介紹 Generator 基類以前,咱們先來實現一個簡單的 🌰
首先打開核心入口文件,編輯內容以下 👇
// ~/generators/app/index.js
// 此文件做爲 Generator 的核心入口
// 須要導出一個繼承自 Yeoman Generator 的類型
// Yeoman Generator 在工做時會自動調用咱們在此類型中定義的一些生命週期方法
// 咱們在這些方法中能夠經過調用父類提供的一些工具方法實現一些功能,例如文件寫入
const Generator = require('yeoman-generator');
module.exports = class extends Generator {
// add your own methods
method1() {
console.log('I am a custom method');
}
method2() {
console.log('I am a custom method2');
}
};
複製代碼
完成以後,咱們經過 npm link
的方式把項目連接到全局
$ npm link # or yarn link
複製代碼
這樣咱們就能夠在全局去訪問到 generator-simple 項目了,咱們來試一下
$ yo simple
複製代碼
看一下控制檯的輸出
I am a custom method1
I am a custom method2
複製代碼
OK,是咱們想要的結果 😎
⚠️ 注意,若是運行yo simple
出現下面的錯誤
This generator (simple:app)
requires yeoman-environment at least 3.0.0, current version is 2.10.3,
try reinstalling latest version of 'yo' or use '--ignore-version-check' option
複製代碼
能夠這樣處理:
方案一
# 卸載當前版本
npm uninstall yeoman-generator
# 安裝低版本的包
npm i yeoman-generator@4.13.0
# 執行
yo simple
複製代碼
方案二
# 全局安裝模塊
npm i -g yeoman-environment
# 新的執行方式(yoe沒有打錯)
yoe run simple
複製代碼
從上面的小 🌰 咱們能夠看到咱們自定義方法是自動順序執行,Generator 基類也提供了一些順序執行的方法,相似於生命週期同樣,咱們看一下有哪些 👇
initializing
-- 初始化方法(檢查狀態、獲取配置等)prompting
-- 獲取用戶交互數據(this.prompt())configuring
-- 編輯和配置項目的配置文件default
-- 若是 Generator 內部還有不符合任意一個任務隊列任務名的方法,將會被放在 default 這個任務下進行運行writing
-- 填充預置模板conflicts
-- 處理衝突(僅限內部使用)install
-- 進行依賴的安裝(eg:npm,bower)end
-- 最後調用,作一些 clean 工做
咱們藉助 Generator 提供的方法,咱們對入口文件改造一下
// ~/generators/app/index.js
const Generator = require('yeoman-generator');
module.exports = class extends Generator {
// yo 會自動調用該方法
writing () {
// 咱們使用 Generator 提供的 fs 模塊嘗試往目錄中寫入文件
this.fs.write(
// destinationPath() 基於項目地址
this.destinationPath('temp.txt'), // 寫入地址
Math.random().toString() // 寫入內容
)
}
};
複製代碼
運行一下看看
$ yo simple
複製代碼
這個時候,控制檯輸出出 create temp.txt
,咱們打印一下目錄結構
generator-simple
├─ generators
│ └─ app
│ └─ index.js
├─ package-lock.json
├─ package.json
└─ temp.txt .............. writing 中建立的文件
複製代碼
打開新建立的 temp.txt 瞅瞅
0.8115477932475306
複製代碼
能夠看到文件中寫入了一串隨機數。
在實際使用的時候,咱們須要經過模板去建立多個文件,這個時候咱們就須要這樣處理 👇
首先,建立模板文件目錄 ./generators/app/templates/
,並在文件夾中新增一個模板文件 temp.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- yo 支持 ejs 語法 -->
<title><%= title %></title>
</head>
<body>
<% if (success) { %>
<h1>這裏是模版文件<%= title %></h1>
<% } %>
</body>
</html>
複製代碼
而後,修改一下入口文件 👇
// ~/generators/app/index.js
const Generator = require('yeoman-generator');
module.exports = class extends Generator {
// yo 會自動調用該方法
writing () {
// 咱們使用 Generator 提供的 fs 模塊嘗試往目錄中寫入文件
// this.fs.write(
// this.destinationPath('temp.txt'),
// Math.random().toString()
// )
// 模版文件路徑,默認指向 templates
const tempPath = this.templatePath('temp.html')
// 輸出目標路徑
const output = this.destinationPath('index.html')
// 模板數據上下文
const context = { title: 'Hello ITEM ~', success: true}
this.fs.copyTpl(tempPath, output, context)
}
};
複製代碼
完成以後,yo simple
運行一下,這樣咱們就在根目錄下獲得了 index.html
,打開看看 🤓
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- 支持 ejs 語法 -->
<title>Hello ITEM ~</title>
</head>
<body>
<h1>這裏是模版文件Hello ITEM ~</h1>
</body>
</html>
複製代碼
ejs 寫入的變量,都已經被數據成功替換了 ✌️
接下來,咱們要如何經過命令行交互獲取用戶自定義的一些數據,例如:項目名稱、版本號等等。
這個就須要藉助 Generator 提供的 Promting 來處理命令行的一些交互
// ~/generators/app/index.js
const Generator = require('yeoman-generator');
module.exports = class extends Generator {
// 在此方法中能夠調用父類的 prompt() 方法與用戶進行命令行詢問
prompting(){
return this.prompt([
{
type: 'input', // 交互類型
name: 'name',
message: 'Your project name', // 詢問信息
default: this.appname // 項目目錄名稱,這裏是 generator-simple
}
])
.then(answers => {
console.log(answers) // 打印輸入內容
this.answers = answers // 存入結果,能夠在後面使用
})
}
// yo 會自動調用該方法
writing () {
......
}
};
複製代碼
保存以後,再運行 yo simple
咱們看到命令行詢問了 Your Project name ?
,在用戶輸入完成以後,咱們拿到了 anwsers,這樣咱們就能夠在接下來的流程裏面去使用這個結果。
// ~/generators/app/index.js
...
// 模板數據上下文
writing () {
...
// 模板數據上下文
const context = { title: this.answers.name, success: true}
this.fs.copyTpl(tempPath, output, context)
}
...
複製代碼
再運行一下 yo simple
,查看輸出的 index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- 支持 ejs 語法 -->
<title>my-project</title>
</head>
<body>
<h1>這裏是模版文件my-project</h1>
</body>
</html>
複製代碼
咱們能夠看到用戶輸入的內容 { name: 'my-project' }
已經顯示在咱們的 index.html 裏面了 👌
點此打開 👉 generator-simple 源碼地址
yeoman 就介紹到這裏,接下來咱們來看另一款腳手架工具 -- plop 👇
plop 小在體積輕量,美在簡單易用
更多使用方法 👉 plop 使用文檔
咱們能夠將其直接集成到項目中,解決一下重複性的活着須要標準化的建立工做,下面咱們就來作個小案例,好比
咱們已經約定好了組件的建立規範:
plop 的使用過程大體能夠拆解爲:
下面進入 coding 環節
首先用咱們的 zhurong-cli 初始化一個 vue 項目
# 全局安裝
$ npm install zhurong-cli -g
# 建立 vue 項目
$ zr create plop-demo
複製代碼
咱們這裏爲了團隊統一使用,plop 直接就集成到項目之中
$ npm install plop --save-dev
複製代碼
項目目錄下面建立 plop 的配置文件 plopfile.js
// ./plopfile.js
module.exports = plop => {
plop.setGenerator('component', {
// 描述
description: 'create a component',
// 詢問組件的名稱
prompts: [
{
type: 'input',
name: 'name',
message: 'Your component name',
default: 'MyComponent'
}
],
// 獲取到回答內容後續的動做
actions: [
//每個對象都是一個動做
{
type: 'add', // 表明添加文件
// 被建立文件的路徑及名稱
// name 爲用戶輸入的結果,使用 {{}} 使用變量
// properCase: plop 自帶方法,將 name 轉換爲大駝峯
path: 'src/components/{{ properCase name }}/index.vue',
// 模板文件地址
templateFile: 'plop-templates/component.vue.hbs'
},
{
type: 'add',
path: 'src/components/{{ properCase name }}/index.scss',
templateFile: 'plop-templates/component.scss.hbs'
},
{
type: 'add',
path: 'src/components/{{ properCase name }}/README.md',
templateFile: 'plop-templates/README.md.hbs'
}
]
})
}
複製代碼
上面用到 properCase 方法將 name 轉化爲大駝峯,其餘格式還包括 👇
camelCase
: changeFormatToThissnakeCase
: change_format_to_thisdashCase/kebabCase
: change-format-to-thisdotCase
: change.format.to.thispathCase
: change/format/to/thisproperCase/pascalCase
: ChangeFormatToThislowerCase
: change format to thissentenceCase
: Change format to this,constantCase
: CHANGE_FORMAT_TO_THIStitleCase
: Change Format To This咱們看到上面已經引用了模板文件,實際上咱們還沒建立,接着建立一下
項目文件夾下面建立 plop-templates 文件夾,裏面建立對應的模板文件
plop-templates
├─ README.md.hbs ............... 說明文檔模板
├─ component.scss.hbs .......... 組件樣式模板
└─ component.vue.hbs ........... 組件模板
複製代碼
模板引擎咱們用到是 Handlebars ,更多語法說明 👉 Handlebars 中文網
編輯 component.scss.hbs
{{!-- ./plop-templates/component.scss.hbs --}} {{!-- dashCase/kebabCase: change-format-to-this --}} {{!-- name: 輸入模板名稱 --}} .{{ dashCase name }} { } 複製代碼
編輯 component.vue.hbs
{{!-- ./plop-templates/component.vue.hbs --}} <template> <div class="{{ dashCase name }}">{{ name }}</div> </template> <script> export default { name: '{{ properCase name }}', } </script> <style lang="scss"> @import "./index.scss"; </style> 複製代碼
編輯 README.md.hbs
{{!-- ./plop-templates/README.md.hbs --}} 這裏是組件 {{ name }} 的使用說明 複製代碼
補充說明:
dashCase
:變爲橫線連接 aa-bb-ccproperCase
:變爲大駝峯 AaBbCc...
{{}}
包裹打開 package.json
// scripts 中 增長一條命令
...
"scripts": {
...
"plop": "plop"
},
...
複製代碼
此時咱們就可使用 npm run plop
來建立組件了
很快組件就建立完成了 ✅
此時看一下 components 文件夾下面
components
├─ MyApp
│ ├─ README.md
│ ├─ index.scss
│ └─ index.vue
└─ HelloWorld.vue
複製代碼
已經建立了 MyApp 的組件了,裏面的文件咱們也打開看看
打開 MyApp/index.vue
<template>
<div class="my-app">my-app</div>
</template>
<script> export default { name: 'MyApp', } </script>
<style lang="scss"> @import "./index.scss"; </style>
複製代碼
打開 MyApp/index.scss
.my-app {
}
複製代碼
打開 MyApp/README.md
這裏是組件 my-app 的使用說明
複製代碼
點此打開 👉 plop-demo 源碼地址
不知道你們看完這篇文章,學廢了嗎 😂
本篇文章整理了好久,但願對你們的學習有所幫助 😁
另外也但願你們能夠 點贊 評論 關注 支持一下,您的支持就是寫做的動力 😘
預告一下,下一篇將帶來 👉 打包與構建工具相關的知識體系
參考文章:
github.com/CodeLittleP…
cli.vuejs.org/zh/guide/cr…
yeoman.io/authoring/i…
www.jianshu.com/p/93211004c…