这个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的源码。