vue-cli源码学习2.x

前言

vue-cli的2.9.6版本看完之后,对cli脚手架的整个原理才有了一个比较清晰的认识,其可配置性主要体现在三个方面,即可配置性问答,可配置性文件,可配置性文件内容。文章也将主要整理这三个部分是如何实现的。

流程介绍

  • vue文件开始,根据输入vue init/list 执行不同文件

    • 选择vue init,进入执行 vue-init

    • 根据isLocalPath()判断是 本地模板 还是 官方模板

      • 如果是当前路径的template,则调用getTemplatePath()确保其是绝对路径,并进入 generate()

      • 如果是官方模板,则需要检查版本(向远程发送请求,获取cli当前latest的版本和本地安装的版本)

      • 根据选择获取 模板路径,生成路径,项目名,进入 downloadAndGenerate(),然后download()之后进入generate()
    • 进入generate() 首先通过getOptions()获取模板下meta.js/meta.json的配置(其中包含了不同模板的提问),getOptions()里面设置项目名,以及设置包名验证,然后调用getGitUser()获取git配置的用户名,然后设置作者

    • 然后使用Metalsmith,先调用meta.jsmetalsmith.before的方法,其目的是合并一些参数,比如合并isNotTest,然后在test测试的时候,跳过问答部分

    • metalsmith可传入插件(函数)产生结果,其实和gulp的pipe管道很像

      • 先调用 askQuestions() 获取模板中meta.js中的prompt数据,使用inquirer包来控制问答交互,并将用户输入答案存入metalsmith.metadata()全局变量中,以便后面使用

      • 然后调用 filterFiles() 获取模板中meta.js中的filters数据,然后根据上一步用户输入,来删去模板中用户不需要的文件。其实现方式是:遍历模板filters,然后内层遍历所有模板文件,如果符合某条件,则将该文件删去

      • 调用 renderTemplateFiles() 跳过一些文件生成以及根据Handlebars.registerHelper的定义,替换模板中的部分,使其内容可配置。其中实现方法是:使用handlebars,模板渲染的时候将其中的部分替换,首先定义一些条件,然后在模板相应位置写条件(和ejs模板渲染引擎比较像),然后根据一些选项来控制具体内容是否需要,最后生成最终文件

  • vue list的实现则和vue 1.x版本相同,向远程仓库发起请求,获取对应模板列表,然后输出出来

细节分析

启动

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)

入口文件很简单,使用commondercommand方法,给第二个描述参数,则用户输入命令时执行不同的文件

核心generate()

npm包

先看一下引入的npm包,注释里简单介绍一下功能

1
2
3
4
5
6
7
8
9
10
11
12
13
// 命令行界面高亮
const chalk = require('chalk')
// 传入方法,生成目标文件
const Metalsmith = require('metalsmith')
// 构建语义模板,可以看到下载的模板里面有 if_eq 等字样
const Handlebars = require('handlebars')
// 异步执行
const async = require('async')
// 模板引擎合并库,直接使用了handlebars
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
module.exports = function generate (name, src, dest, done) {
const opts = getOptions(name, src)
// console.log("opts:", opts)
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
})
// console.log("metalsmith1:", metalsmith)
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') {
// 调用meta.js的metalsmith.before的方法,并传入metalsmith,meta.js的配置,以及高亮和打印输出
// 做的工作是合并一个isnotTest,目测应该是做一个不是test的标识【待验证】
opts.metalsmith.before(metalsmith, opts, helpers)
}
// console.log("metalsmith2:", metalsmith)

// 询问问题,
// 将不需要的文件过滤掉
// 跳过一些文件生成以及根据Handlebars.registerHelper的定义,替换模板中的部分,使其内容可配置
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('.') // start from template root instead of `./src` which is Metalsmith's default for `source`
.destination(dest)
.build((err, files) => {
done(err)
if (typeof opts.complete === 'function') {
const helpers = { chalk, logger, files }
// 执行meta.js中的complete方法
// webpack模板中则是排序依赖包,安装依赖包,输出相应信息
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("opts:", opts)
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
})
// console.log("metalsmith1:", metalsmith)
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') {
// 调用meta.js的metalsmith.before的方法,并传入metalsmith,meta.js的配置,以及高亮和打印输出
// 做的工作是合并一个isnotTest,目测应该是做一个不是test的标识【待验证】
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
// 询问问题,
// 将不需要的文件过滤掉
// 跳过一些文件生成以及根据Handlebars.registerHelper的定义,替换模板中的部分,使其内容可配置
metalsmith.use(askQuestions(opts.prompts))
.use(filterFiles(opts.filters))
.use(renderTemplateFiles(opts.skipInterpolation))

这一部分做了三件事,询问问题,过滤文件,模板渲染文件,其实现方式是

  • askQuestions就不详细说了,比较简单

  • filterFiles,通过遍历filters规则,然后内层遍历模板下所有文件,如果和filters的value相匹配,则delete掉文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const fileNames = Object.keys(files)
// console.log("fileNames:", fileNames)
Object.keys(filters).forEach(glob => {
// 遍历filters规则
fileNames.forEach(file => {
// 遍历生成的所有文件,dot允许匹配.开头的文件
if (match(file, glob, { dot: true })) {
// condition是filters的value
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) => {
// skipping files with skipInterpolation option
if (skipInterpolation && multimatch([file], skipInterpolation, { dot: true }).length) {
return next()
}
// str是文件内容
const str = files[file].contents.toString()
console.log("str:", str)
// do not attempt to render files that do not have mustaches
if (!/{{([^{}]+)}}/g.test(str)) {
return next()
}
// 使用handlebars,模板渲染的时候将其中的部分替换
// 比如:如果选择使用router,那么,模板中的main.js文件中就会渲染出import router部分
// {{#router}}
// import router from './router'
// {{/router}}
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
// register handlebars helper
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"}}
// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
{{/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('.') // start from template root instead of `./src` which is Metalsmith's default for `source`
.destination(dest)
.build((err, files) => {
done(err)
if (typeof opts.complete === 'function') {
const helpers = { chalk, logger, files }
// 执行meta.js中的complete方法
// webpack模板中则是排序依赖包,安装依赖包,输出相应信息
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还没有看完,说的不一定对,看完之后会回来修改的)