前言 vue-cli 的 2.9.6 版本看完之后,对 cli 脚手架的整个原理才有了一个比较清晰的认识,其可配置性主要体现在三个方面,即可配置性问答,可配置性文件,可配置性文件内容。文章也将主要整理这三个部分是如何实现的。
流程介绍
细节分析 启动 1 2 3 4 5 6 7 8 9 program .version (require ("../package" ).version ) .usage ("<command> [options]" ) .command ("init" , "generate a new project from a template" ) .command ("list" , "list available official templates" ) .command ("build" , "prototype a new project" ) .command ("create" , "(for v3 warning only)" ) program.parse (process.argv )
入口文件很简单,使用commonder
的command
方法,给第二个描述参数,则用户输入命令时执行不同的文件
核心 generate() npm 包 先看一下引入的 npm 包,注释里简单介绍一下功能
1 2 3 4 5 6 7 8 9 10 11 12 13 const chalk = require ("chalk" )const Metalsmith = require ("metalsmith" )const Handlebars = require ("handlebars" )const async = require ("async" )const render = require ("consolidate" ).handlebars .render const path = require ("path" )const multimatch = require ("multimatch" )
generate() 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 module .exports = function generate (name, src, dest, done ) { const opts = getOptions (name, src) console .log ("src:" , src) const metalsmith = Metalsmith (path.join (src, "template" )) const data = Object .assign (metalsmith.metadata (), { destDirName : name, inPlace : dest === process.cwd (), noEscape : true , }) opts.helpers && Object .keys (opts.helpers ).map ((key ) => { Handlebars .registerHelper (key, opts.helpers [key]) }) const helpers = { chalk, logger } if (opts.metalsmith && typeof opts.metalsmith .before === "function" ) { opts.metalsmith .before (metalsmith, opts, helpers) } metalsmith .use (askQuestions (opts.prompts )) .use (filterFiles (opts.filters )) .use (renderTemplateFiles (opts.skipInterpolation )) if (typeof opts.metalsmith === "function" ) { opts.metalsmith (metalsmith, opts, helpers) } else if (opts.metalsmith && typeof opts.metalsmith .after === "function" ) { opts.metalsmith .after (metalsmith, opts, helpers) } metalsmith .clean (false ) .source ("." ) .destination (dest) .build ((err, files ) => { done (err) if (typeof opts.complete === "function" ) { const helpers = { chalk, logger, files } opts.complete (data, helpers) } else { logMessage (opts.completeMessage , data) } }) return data }
代码很长,我们分开来讲,细节就不再多说,主要说核心实现。
生成文件前期 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 const opts = getOptions (name, src)console .log ("src:" , src)const metalsmith = Metalsmith (path.join (src, "template" ))const data = Object .assign (metalsmith.metadata (), { destDirName : name, inPlace : dest === process.cwd (), noEscape : true , }) opts.helpers && Object .keys (opts.helpers ).map ((key ) => { Handlebars .registerHelper (key, opts.helpers [key]) }) const helpers = { chalk, logger }if (opts.metalsmith && typeof opts.metalsmith .before === "function" ) { opts.metalsmith .before (metalsmith, opts, helpers) }
这一部分做的事情主要是读取模板下meta.js
文件中的配置信息,其中包含的信息有
metalsmith
全局变量(比如 isNotTest,应该就是在 test 时候跳过问答部分),通过opts.metalsmith.before(metalsmith, opts, helpers)
合并进来
helpers
即 handlesbar 的渲染模板,通过Handlebars.registerHelper(key, opts.helpers[key])
合并进来
prompts
即 inquire 需要使用的模板配置问题(后面讲)
filters
即根据回答,讲不需要文件删除的部分(后面讲)
complete
生成文件时调用(后面讲)
生成文件中期 1 2 3 4 5 6 7 metalsmith .use (askQuestions (opts.prompts )) .use (filterFiles (opts.filters )) .use (renderTemplateFiles (opts.skipInterpolation ))
这一部分做了三件事,询问问题,过滤文件,模板渲染文件,其实现方式是
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const fileNames = Object .keys (files)Object .keys (filters).forEach ((glob ) => { fileNames.forEach ((file ) => { if (match (file, glob, { dot : true })) { const condition = filters[glob] if (!evaluate (condition, data)) { delete files[file] } } }) })
renderTemplateFiles
,遍历每个文件,异步处理其中的内容,将所有可配置部分根据注册的模板以及回答,来选择文件内容如何生成。可以看下面的 handlebars 介绍
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 return (files, metalsmith, done ) => { const keys = Object .keys (files) const metalsmithMetadata = metalsmith.metadata () async .each ( keys, (file, next ) => { if ( skipInterpolation && multimatch ([file], skipInterpolation, { dot : true }).length ) { return next () } const str = files[file].contents .toString () console .log ("str:" , str) if (!/{{([^{}]+)}}/g .test (str)) { return next () } render (str, metalsmithMetadata, (err, res ) => { if (err) { err.message = `[${file} ] ${err.message} ` return next (err) } files[file].contents = new Buffer (res) next () }) }, done ) }
handlebars 1 2 3 4 5 6 7 8 Handlebars .registerHelper ("if_eq" , function (a, b, opts ) { return a === b ? opts.fn (this ) : opts.inverse (this ) }) Handlebars .registerHelper ("unless_eq" , function (a, b, opts ) { return a === b ? opts.inverse (this ) : opts.fn (this ) })
注册模板渲染,作用是实现模板文件内容的可配置。举个例子,如果选择时,选择引入vue-router
,那么这个时候,main.js
肯定要引入,这个时候就可以根据handlebars
注册的模板进行有选择性渲染。
当然上面的注册并没有注册 router 的,因为不同模板不一样,并非每一个模板都需要 router,那么是如何实现可配置性呢?就是根据用户选择,来选择是否需要渲染。
这里贴一下 webpack 的模板就明白了
1 2 3 4 5 6 7 8 9 {{#if_eq build "standalone" }} {{/if_eq}} import Vue from 'vue' import App from './App' {{#router}} import router from './router' {{/router}}
生成文件后期 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 metalsmith .clean (false ) .source ("." ) .destination (dest) .build ((err, files ) => { done (err) if (typeof opts.complete === "function" ) { const helpers = { chalk, logger, files } opts.complete (data, helpers) } else { logMessage (opts.completeMessage , data) } })
这一部分就是生成文件的部分了,在经过了前面三个函数的处理之后,此时的文件已经基本成型,内容也已经是配置后的了,这一部分还会调用一下meta.js
中的complete
部分,比如这里的complete
就是先给依赖排序,然后执行npm install
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 complete : function (data, { chalk } ) { const green = chalk.green sortDependencies (data, green) const cwd = path.join (process.cwd (), data.inPlace ? '' : data.destDirName ) if (data.autoInstall ) { installDependencies (cwd, data.autoInstall , green) .then (() => { return runLintFix (cwd, data, green) }) .then (() => { printMessage (data, green) }) .catch (e => { console .log (chalk.red ('Error:' ), e) }) } else { printMessage (data, chalk) } }
总结 vue cli 2.9.6 是 2.x 的最后一个版本,其核心内容就是 generater()的部分,其包含了三大核心内容,可配置性问答,可配置性文件,可配置性文件内容。正是由于此,才另 vue cli 2.x 和 1.x 相比,更加的灵活。
但是虽然如此,2.x 和 1.x 还是都没有逃出其核心原理是直接下载远程模板,其配置性也都是在模板的基础上进行的。还是不够灵活,那 vue 3.x 则使用插件式,令模板更加灵活(当然这里 vue cli 3.x 还没有看完,说的不一定对,看完之后会回来修改的)