vue-cli源码学习2.x
前言 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.renderconst 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 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 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 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 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 9 10 11 12 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 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还没有看完,说的不一定对,看完之后会回来修改的)