這個demo我是模仿Vue-CLI 2.0寫的一個簡單的構建工具,3.0的源碼還沒去看蜒秤,所以會有不同的地方衅疙。
先安裝開發(fā)依賴的工具
npm i commander handlebars inquirer metalsmith -D
commander
:用來處理命令行參數(shù)
handlerbars
:一個簡單高效的語義化模板構建引擎膝擂,比如我們用vue-cli構建項目后命令行會有一些交互行為虑啤,讓你選擇要安裝的包什么的等等,而Handlerbars.js會根據(jù)你的這些選擇回答去渲染模版猿挚。
inquirer
:會根據(jù)模版里面的meta.js或者meta.json文件中的設置咐旧,與用戶進行一些簡單的交互以確定項目的一些細節(jié)。
metalsmith
:一個非常簡單的可插拔的靜態(tài)網(wǎng)站生成器绩蜻,通過添加一些插件對要構建的模版文件進行處理铣墨。
安裝完后就能在package.json
中看到如下的依賴
項目目錄結構
其中template-demo
里面包含了本次要構建的項目模版templae,和meta.js文件
代碼編寫
1.bin/dg.js
之后在命令行下面運行
node bin/dg.js xxx xxx
就可以構建項目了办绝。
兩個 xxx的地方 第一個是項目的模版伊约,第二個是要輸入到哪個目錄下也就是要構建的項目名稱
// dg.js
const program = require('commander')
const path = require('path')
const chalk = require('chalk') // 終端字體顏色
const inquirer = require('inquirer')
const exists = require('fs').existsSync // 判斷 路徑是否存在
const generate = require('./lib/generate')
/**
* 注冊一個help的命令
* 當在終端輸入 dg --help 或者沒有跟參數(shù)的話
* 會輸出提示
*/
program.on('--help', () => {{
console.log(' Examples:')
console.log()
console.log(chalk.gray(' # create a new project with an template')) // 會以灰色字體顯示
console.log(' $ dg dgtemplate my-project')
}})
/**
* 判斷參數(shù)是否為空
* 如果為空調用上面注冊的 help命令
* 輸出提示
*/
function help () {
program.parse(process.argv) //commander 用來處理 命令行里面的參數(shù), 這邊的process是node的一個全局變量不明白的可以查一下資料
if (program.args.length < 1) return program.help()
}
help()
/**
* 獲取命令行參數(shù)
*/
let template = program.args[0] // 命令行第一個參數(shù) 模版的名字
const rawName = program.args[1] // 第二個參數(shù) 項目目錄
/**
* 獲取項目和模版的完整路徑
*/
const to = path.resolve(rawName) // 構建的項目的 絕對路徑
const tem = path.join(process.cwd(), template) //模版的路徑 cwd是當前運行的腳本是在哪個路徑下運行
/**
* 判斷這個項目路徑是否存在也就是是否存在相同的項目名
* 如果存在提示 是否繼續(xù)然后運行 run
* 如果不存在 則直接運行 run 最后會創(chuàng)建一個項目目錄
*/
if (exists(to)) {
inquirer.prompt([ // 這邊就用到了與終端交互的inquirer了
{
type: 'confirm',
message: 'Continue?',
name: 'ok'
}
]).then(answers => {
if (answers.ok) {
run ()
}
})
} else {
run ()
}
/**
* run函數(shù)則是用來調用generate來構建項目
*/
function run () {
if (exists(tem)) {
generate(rawName, tem, to, (err) => {
if (err) console.log(err) // 如果構建失敗就調用的回調函數(shù)
})
}
}
注釋說明 都在代碼里面了孕蝉。
2.接下來就是很重要的lib/generate.js
文件了
// generate.js
const Metalsmith = require('metalsmith')
const Handlebars = require('handlebars')
const path = require('path')
const chalk = require('chalk')
const getOptions = require('./options')
const ask = require('./ask')
/**
* 把generate 導出去給dg.js使用
* opts是通過getOptions()函數(shù)用來獲取 meta.js中的配置
* metalsmith是通過metalsmith.js獲取模版的元數(shù)據(jù)
* metalmith可以讓我們編寫一些插件來對項目下面的文件進行配置
* 其中第一個use的第一個插件就是用來在終端中輸入一些問題一些選項讓我們設置一些模版中的細節(jié)
* 而這些問題就是 放在meta.js中
* 第二個use的插件這是渲染模版屡律,這里就是用了handebars.js來渲染模版
*
*/
module.exports = function generate (name, tem, dest, done) {
const opts = getOptions(name, tem)
const metalsmith = Metalsmith(path.join(tem, 'template'))
const data = Object.assign(metalsmith.metadata(), {
destDirName: name,
inPlace: dest === process.cwd()
})
metalsmith.use(askQuestions(opts.prompts)).use(renderTemplateFiles()) // 這兩個插件在下面的代碼中
// 在構建前執(zhí)行一些函數(shù)
metalsmith.clean(false)
.source('.') // 默認的source路徑是 ./src 所以這邊要改成整個 template 這個根據(jù)自己要輸出的需求配置
.destination(dest) // 要輸出到哪個路徑下 這里就是 我們的項目地址
.build((err, files) => { // 最后進行構建項目
done(err) // 執(zhí)行 回掉函數(shù)
if (typeof opts.complete === 'function') {
const helpers = { chalk }
opts.complete(data, helpers) // 判斷meta.js中是否定義了構建完成后要執(zhí)行的函數(shù) 這里是判斷是否執(zhí)行自動安裝依賴
} else {
console.log('complete is not a function')
}
})
}
/**
* 這里通過這個函數(shù)返回一個metalsmith的符合metalsmith插件格式的函數(shù)
* 第一個參數(shù)fils就是 這個模版下面的全部文件
* 第二個參數(shù)ms就是元數(shù)據(jù)這里我們的問題以及回答會已鍵值對的形式存放在里面用于第二個插件渲染模版
* 第三個參數(shù)就是類似 next的用法了 調用done后才能移交給下一個插件運行
* ask函數(shù)則在另外一個js文件中
*/
function askQuestions (prompts) {
return (fils, ms, done) => {
ask(prompts, ms.metadata(), done)
}
}
/**
* render函數(shù)則是通過我們第一個插件收集這些問題以及回答后
* 然后渲染我們的模版
*/
function renderTemplateFiles () {
return (files, ms, done) => {
const keys = Object.keys(files) // 獲取模版下的所有文件名
keys.forEach(key => { // 遍歷對每個文件使用handlerbars渲染
const str = files[key].contents.toString()
let t = Handlebars.compile(str)
let html = t(ms.metadata())
files[key].contents = new Buffer.from(html) // 渲染后重新寫入到文件中
})
done() // 移交給下個插件
}
}
其實generate.js
功能就是用來收集我們在命令行下交互的問題的答案用來渲染模版,只不過我這邊只是簡單的實現(xiàn)降淮,在vue-cli 2.0
中還有對文件的過濾超埋,跳過不符合使用handlebars渲染文件,添加一些handlebars的helpers來制定文件渲染的規(guī)則等等
lib/options.js
// options.js
const path = require('path')
/**
* 這里的options內容比較簡單
* 就是用于用來獲取 meta.js 里面的配置
*/
module.exports = function options (name, dir) {
const metaPath = path.join(dir, 'meta.js')
const req = require(metaPath)
let opts = {}
opts = req
return opts
}
options我也是簡單的實現(xiàn)佳鳖,有興趣的話可以查看vue-cli
的源碼
lib/ask.js
// ask.js
const async = require('async') // 這是node下一個異步處理的工具
const inquirer = require('inquirer')
const promptMapping = {
string: 'input'
}
/**
* 這個函數(shù)就是 根據(jù)meta.js里面定義的prompts來與用戶進行交互
* 然后收集用戶的交互信息存放在metadate 也就是metalsmith元數(shù)據(jù)中
* 用于渲染模版使用
*/
module.exports = function ask (prompts, metadate, done) {
async.eachSeries(Object.keys(prompts), (key, next) => { // 這里不能簡單的使用數(shù)組的 foreach方法 否則只直接跳到最后一個問題
inquirer.prompt([{
type: promptMapping[prompts[key].type] || prompts[key].type,
name: key,
message: prompts[key].message,
choices: prompts[key].choices || [],
}]).then(answers => {
if (typeof answers[key] === 'string') {
metadate[key] = answers[key].replace(/"/g, '\\"')
} else {
metadate[key] = answers[key]
}
next()
}).catch(done)
}, done) // 全部回答完 調用 done移交給下一個插件
}
收集問題的答案用于渲染模版
下面是用于渲染模版的配置中的代碼
為了方便 我把要渲染的模版霍殴,直接跟 構建工具 項目放到了同個文件夾下面,就是上面我截圖的項目結構的 template-demo
里面包含了要渲染的模版 放在 template-demo/template
下面了系吩,還包含了渲染模版的配置文件meta.js
来庭。
// meta.js
const { installDependencies } = require('./utils')
const path = require('path')
/***
* 要交互的問題都放在 prompts中
* when是當什么情況下 用來判斷是否 顯示這個問題
* type是提問的類型
* message就是要顯示的問題
*/
module.exports = {
prompts: {
name: {
when: 'ismeta',
type: 'string',
message: '項目名稱:'
},
description: {
when: 'ismeta',
type: 'string',
message: '項目介紹:'
},
author: {
when: 'ismeta',
type: 'string',
message: '項目作者:'
},
email: {
when: 'ismeta',
type: 'string',
message: '郵箱:'
},
dgtable: {
when: 'ismeta',
type: 'confirm',
message: '是否安裝dg-table(筆者編寫的基于elementui二次開發(fā)的強大的表格)',
},
genius: {
when: 'ismeta',
type: 'list',
message: '想看想看?',
choices: [
{
name: '想',
value: '想',
short: '想'
},
{
name: '很想',
value: '很想',
short: '很想'
}
]
},
autoInstall: {
when: 'ismeta',
type: 'confirm',
message: '是否自動執(zhí)行npm install 安裝依賴穿挨?',
},
},
complete: function(data, { chalk }) {
/**
* 用于判斷是否執(zhí)行自動安裝依賴
*/
const green = chalk.green // 取綠色
const cwd = path.join(process.cwd(), data.inPlace ? '' : data.destDirName)
if (data.autoInstall) {
installDependencies(cwd, 'npm', green) // 這里使用npm安裝
.then(() => {
console.log('依賴安裝完成')
})
.catch(e => {
console.log(chalk.red('Error:'), e)
})
} else {
// printMessage(data, chalk)
}
}
}
主要是用于配置交互的問題月弛,和再項目構建完成后執(zhí)行的 complete 函數(shù),這里就是 判斷用戶是否 選擇了 自動安裝依賴科盛,如果autoInstall
為true就自動安裝依賴
const spawn = require('child_process').spawn // 一個node的子線程
/**
* 安裝依賴
*/
exports.installDependencies = function installDependencies(
cwd,
executable = 'npm',
color
) {
console.log(`\n\n# ${color('正在安裝項目依賴 ...')}`)
console.log('# ========================\n')
return runCommand(executable, ['install'], {
cwd,
})
}
function runCommand(cmd, args, options) {
return new Promise((resolve, reject) => {
/**
* 如果不清楚spaw的話可以上網(wǎng)查一下
* 這里就是 在項目目錄下執(zhí)行 npm install
*/
const spwan = spawn(
cmd,
args,
Object.assign(
{
cwd: process.cwd(),
stdio: 'inherit',
shell: true, // 在shell下執(zhí)行
},
options
)
)
spwan.on('exit', () => {
resolve()
})
})
}
執(zhí)行安裝的具體實現(xiàn)函數(shù)些楣。
最后你就可以在構建工具的根目錄下 執(zhí)行
node bin/dg.js template-demo demo
來構建項目啦老速。
如果把dg.js
添加到$PATH
中 就可以 直接使用dg template-demo demo
來構建項目棒掠。
最后我們可以看到我們在命令行回答的問題被渲染到了這里面來了濒憋,根據(jù)是否安裝
dg-table
讓這個插件出現(xiàn)在了依賴列表里面,當然包括模版中的index.html
也被渲染了。這里圖片就不貼出來了冀泻。這個模版只不過是為了演示沒有其他意義了。
主要是我比較懶蜡饵,挺多功能沒實現(xiàn)弹渔,還有vue-cli
可以自動從github上面拉取模版,const download = require('download-git-repo') //用于下載遠程倉庫至本地 支持GitHub溯祸、GitLab肢专、Bitbucket
。
如果想更清楚的了解內部實現(xiàn)最好還是看下Vue-cli2.0的源碼焦辅。