這個demo我是模仿Vue-CLI 2.0寫的一個簡單的構建工具,3.0的源碼還沒去看,因此會有不一樣的地方。javascript
已經上傳到github上了html
npm i commander handlebars inquirer metalsmith -D
commander
:用來處理命令行參數vue
handlerbars
:一個簡單高效的語義化模板構建引擎,好比咱們用vue-cli構建項目後命令行會有一些交互行爲,讓你選擇要安裝的包什麼的等等,而Handlerbars.js會根據你的這些選擇回答去渲染模版。java
inquirer
:會根據模版裏面的meta.js或者meta.json文件中的設置,與用戶進行一些簡單的交互以肯定項目的一些細節。node
metalsmith
:一個很是簡單的可插拔的靜態網站生成器,經過添加一些插件對要構建的模版文件進行處理。git
安裝完後就能在package.json
中看到以下的依賴
github
其中template-demo
裏面包含了本次要構建的項目模版templae,和meta.js文件vue-cli
1.bin/dg.js
以後在命令行下面運行shell
node bin/dg.js xxx xxx
就能夠構建項目了。
兩個 xxx的地方 第一個是項目的模版,第二個是要輸入到哪一個目錄下也就是要構建的項目名稱npm
// 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 或者沒有跟參數的話 * 會輸出提示 */ 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') }}) /** * 判斷參數是否爲空 * 若是爲空調用上面註冊的 help命令 * 輸出提示 */ function help () { program.parse(process.argv) //commander 用來處理 命令行裏面的參數, 這邊的process是node的一個全局變量不明白的能夠查一下資料 if (program.args.length < 1) return program.help() } help() /** * 獲取命令行參數 */ let template = program.args[0] // 命令行第一個參數 模版的名字 const rawName = program.args[1] // 第二個參數 項目目錄 /** * 獲取項目和模版的完整路徑 */ const to = path.resolve(rawName) // 構建的項目的 絕對路徑 const tem = path.join(process.cwd(), template) //模版的路徑 cwd是當前運行的腳本是在哪一個路徑下運行 /** * 判斷這個項目路徑是否存在也就是是否存在相同的項目名 * 若是存在提示 是否繼續而後運行 run * 若是不存在 則直接運行 run 最後會建立一個項目目錄 */ if (exists(to)) { inquirer.prompt([ // 這邊就用到了與終端交互的inquirer了 { type: 'confirm', message: 'Continue?', name: 'ok' } ]).then(answers => { if (answers.ok) { run () } }) } else { run () } /** * run函數則是用來調用generate來構建項目 */ function run () { if (exists(tem)) { generate(rawName, tem, to, (err) => { if (err) console.log(err) // 若是構建失敗就調用的回調函數 }) } }
註釋說明 都在代碼裏面了。
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()函數用來獲取 meta.js中的配置 * metalsmith是經過metalsmith.js獲取模版的元數據 * metalmith可讓咱們編寫一些插件來對項目下面的文件進行配置 * 其中第一個use的第一個插件就是用來在終端中輸入一些問題一些選項讓咱們設置一些模版中的細節 * 而這些問題就是 放在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()) // 這兩個插件在下面的代碼中 // 在構建前執行一些函數 metalsmith.clean(false) .source('.') // 默認的source路徑是 ./src 因此這邊要改爲整個 template 這個根據本身要輸出的需求配置 .destination(dest) // 要輸出到哪一個路徑下 這裏就是 咱們的項目地址 .build((err, files) => { // 最後進行構建項目 done(err) // 執行 回掉函數 if (typeof opts.complete === 'function') { const helpers = { chalk } opts.complete(data, helpers) // 判斷meta.js中是否認義了構建完成後要執行的函數 這裏是判斷是否執行自動安裝依賴 } else { console.log('complete is not a function') } }) } /** * 這裏經過這個函數返回一個metalsmith的符合metalsmith插件格式的函數 * 第一個參數fils就是 這個模版下面的所有文件 * 第二個參數ms就是元數據這裏咱們的問題以及回答會已鍵值對的形式存放在裏面用於第二個插件渲染模版 * 第三個參數就是相似 next的用法了 調用done後才能移交給下一個插件運行 * ask函數則在另一個js文件中 */ function askQuestions (prompts) { return (fils, ms, done) => { ask(prompts, ms.metadata(), done) } } /** * render函數則是經過咱們第一個插件收集這些問題以及回答後 * 而後渲染咱們的模版 */ 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
功能就是用來收集咱們在命令行下交互的問題的答案用來渲染模版,只不過我這邊只是簡單的實現,在vue-cli 2.0
中還有對文件的過濾,跳過不符合使用handlebars渲染文件,添加一些handlebars的helpers來制定文件渲染的規則等等
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我也是簡單的實現,有興趣的話能夠查看vue-cli
的源碼
lib/ask.js
// ask.js const async = require('async') // 這是node下一個異步處理的工具 const inquirer = require('inquirer') const promptMapping = { string: 'input' } /** * 這個函數就是 根據meta.js裏面定義的prompts來與用戶進行交互 * 而後收集用戶的交互信息存放在metadate 也就是metalsmith元數據中 * 用於渲染模版使用 */ module.exports = function ask (prompts, metadate, done) { async.eachSeries(Object.keys(prompts), (key, next) => { // 這裏不能簡單的使用數組的 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二次開發的強大的表格)', }, genius: { when: 'ismeta', type: 'list', message: '想看想看?', choices: [ { name: '想', value: '想', short: '想' }, { name: '很想', value: '很想', short: '很想' } ] }, autoInstall: { when: 'ismeta', type: 'confirm', message: '是否自動執行npm install 安裝依賴?', }, }, complete: function(data, { chalk }) { /** * 用於判斷是否執行自動安裝依賴 */ 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) } } }
主要是用於配置交互的問題,和再項目構建完成後執行的 complete 函數,這裏就是 判斷用戶是否 選擇了 自動安裝依賴,若是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的話能夠上網查一下 * 這裏就是 在項目目錄下執行 npm install */ const spwan = spawn( cmd, args, Object.assign( { cwd: process.cwd(), stdio: 'inherit', shell: true, // 在shell下執行 }, options ) ) spwan.on('exit', () => { resolve() }) }) }
執行安裝的具體實現函數。
最後你就能夠在構建工具的根目錄下 執行
node bin/dg.js template-demo demo
來構建項目啦。
若是把dg.js
添加到$PATH
中 就能夠 直接使用dg template-demo demo
來構建項目。
最後咱們能夠看到咱們在命令行回答的問題被渲染到了這裏面來了,根據是否安裝 dg-table
讓這個插件出如今了依賴列表裏面,固然包括模版中的index.html
也被渲染了。這裏圖片就不貼出來了。這個模版只不過是爲了演示沒有其餘意義了。
主要是我比較懶,挺多功能沒實現,還有vue-cli
能夠自動從github上面拉取模版,const download = require('download-git-repo') //用於下載遠程倉庫至本地 支持GitHub、GitLab、Bitbucket
。
若是想更清楚的瞭解內部實現最好仍是看下Vue-cli2.0的源碼。