前言
vue-cli是vue项目的脚手架工具,使用vue-cli可以快速创建项目。vue-cli的2.x版本和3.x版本在使用上有很大的不同,本篇文章旨在通过vue-cli源码学习了解脚手架的构成实现,所以选择相对简单的vue-cli的2.x版本(具体版本是2.9.6)。
在2.x版本最常用的命令是vue和vue init,下面的流程梳理主要就是针对这两个命令来梳理的。
vue命令的逻辑
在vue-cli项目的package.json文件中定义了命令对应的文件位置,具体如下:
"bin": {
"vue": "bin/vue",
"vue-init": "bin/vue-init",
"vue-list": "bin/vue-list"
}
vue命令对应的bin/vue文件的源码比较简单,这里就直接贴出来具体如下:
#!/usr/bin/env node
const program = require('commander')
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)
commander是Node.js命令行的解决方案(即Node.js下实现自定义命令),点此查看commander中文文档。
这里简单介绍下常用相关方法:
- version:定义版本
- usage:帮助信息输出
- command:使用独立的可执行文件作为子命令
- option:定义选项
- parse:解析参数用于匹配对应选项或命令
当在控制台中输入vue命令,会输出相关的命令和帮助信息,如下:
这里定义了子命令init等, Commander 将会尝试在vue脚本的目录中搜索program-command形式的可执行文件例如init会查找vue-init等。
vue init命令的逻辑
vue-cli脚手架工具中可以使用vue init或vue-init命令来初始化项目,命令最终都对应bin/vue-init文件。
vue-init文件的逻辑流程主要如下:
vue-init文件逻辑中有几个重要的依赖包:
- commander:命令行处理
- download-git-repo:Node.js下git仓库下载处理
- inquirer:Node下用户与命令行交互处理
- ora:终端加载优化处理
vue init语法是:
vue init <template-name> <project-name>
vue官方提供了不同的模板(对应着不同的构建工具等),这些模块仓库都位于vuejs-templates中,简单概括对应了两类:webpack模板和browserify模板。
当然个人也可以自己的模块仓库,通过自有模块来初始化项目,语法如下:
vue init username/repo <project-name>
当webpack模板来初始化项目,其执行逻辑简要概括如下:
- 判断当前目录是否存在相同名称的目录,存在需要与用户交互处理
- 使用download-git-repo来下载对应的模块仓库,会在用户目录下创建.vue-templates目录,该目录下存在webpack模板仓库
- 编译webpack模板仓库生成最终的文件
run函数
该函数的逻辑是检查、下载和编译模板的入口,实际上其逻辑主要就分成3部分:
检查部分的逻辑
实际上检查部分的逻辑主要是:
- 创建是否是本地模板
vue脚手架支持依据本地模板来初始化项目,源码中判断本地模块是通过路径来进行的,即脚手架对于以./、/或window环境下[a-zA-Z]:的开头的路径都看成是使用本地模板
- 对于非本地模板检查相关信息
- 首先会检查对应node版本和vue-cli是否存在新版本
- 其次会检查模板路径是否存在 / 来判断是否使用官方模板还是非官方模板
下载部分的逻辑
该部分就是调用download-git-repo库提供的方法下载对应的git模板仓库,其源码具体如下:
// 模板下载优化
const spinner = ora('downloading template')
spinner.start()
// 本地模板存在就删除掉重新下载
if (exists(tmp)) rm(tmp)
// 下载
download(template, tmp, { clone }, err => {
// 下载成功后逻辑
spinner.stop()
if (err) logger.fatal('Failed to download repo ' + template + ': ' + err.message.trim())
})
模板编译
模板编译是在模板成功下载过后立即执行的,调用的是自定义模块generate下的generate方法,该模块负责模板编译的具体逻辑。generate模块依赖几个重要的第三方NPM包,具体如下:
- handlebars:轻量级语义化模板(点此查看handlebars中文文档)
- consolidate:模板引擎整合库,支持目前比较流行的模板库
- metalsmith:静态站点生成器
首先明确下为什么需要handlebars包?
因为官方vuejs-templates仓库提供的模板是使用handlebars来编写的
接下来具体看看generate模块的具体逻辑,其逻辑主要有如下几点:
获取options
实际上获取options的逻辑比较简单,直接贴源码:
module.exports = function options (name, dir) {
// 模板位置的实际路径
const opts = getMetadata(dir)
// 默认项目名
setDefault(opts, 'name', name)
setValidateName(opts)
// 默认用户名
const author = getGitUser()
if (author) {
setDefault(opts, 'author', author)
}
return opts
}
该函数主要的逻辑是getMetadata,实际上这是脚手架模板仓库的必要设置,即模板仓库必须存在meta.json或meta.js的。
function getMetadata (dir) {
const json = path.join(dir, 'meta.json')
const js = path.join(dir, 'meta.js')
// 获取对应文件内容
return opts
}
这里以官方模板vuejs-templates/webpack为例,这里看看其meta.js的设置,该文件主要包含下面几项:
- metalsmith对象
- helpers对象
- prompts对象
- filters对象
- complete函数
这里meta.js的配置信息是用在后续的逻辑中,这里暂时先不展开,后续逻辑在针对性的说明。
helpers处理
Handlebars是一种简单的模板语言,它使用模板和输入对象来生成 HTML 或其他文本格式。
在generate模块中关于Handlebars的逻辑都是调用Handlebars.registerHelper函数。
Handlebars.registerHelper是提供用户自定义助手的功能
generate模板中的定义的助手代码有:
- if_eq
- unless_eq
- if_or
- template_version
为模板逻辑服务,具体可以看Handlebars的使用和官方模板的具体使用,这里具体就不展开的
metalsmith相关逻辑
使用第三方库metalsmith读取模板仓库中template目录,并对模板的meta数据做相关处理。
Metalsmith是静态站点生成器,其工作原理简单概括只是三个简单步骤:
- 读取源目录中的所有文件
- 调用一系列处理文件的插件
- 将结果写入目标目录
Metalsmith核心逻辑只有读取源目录文件以及输出处理后的文件,所有对于文件的处理都是通过创建来完成的。
从Metalsmith官网对于其工作原理的说明,可以看出与构建工具Glup等相似。
vue脚手架使用Metalsmith原因:
Metalsmith可以自定义插件来实现一些流程性工作
实际上脚手架中对于Metalsmith的使用逻辑就是自定义流程,具体流程如下:
// metalsmith.use就是使用插件
metalsmith
.use(askQuestions(opts.prompts))
.use(filterFiles(opts.filters))
.use(renderTemplateFiles(
opts.skipInterpolation
))
自定义插件askQuestions的处理
vue init命令执行后首先会下载模板之后会有一系列与用户交互的处理,askQuestions插件就是使用inquirer库处理与用户交互的,处理信息定义在meta.js中,即获取options中的prompts对象。
具体的交互信息内容简单如下:
- Project name:项目名
- Project description:项目描述
- Author:作者
- Vue build
- Install vue-router
- ESLint相关
- 测试相关
- 自动安装依赖方式选择
自定义插件filterFiles的处理
过滤相关文件,主要是eslint相关、测试相关,这需要根据之前与用户交互的结果来看是否删除对应文件或目录(官方模板默认是包含所有的文件的)。
自定义插件renderTemplateFiles的处理
该自定义插件逻辑就是处理所有模块文件,该插件使用consolidate库来调用对应的模板引擎来处理每一个文件,vue官方模板是采用Handlebars模板语言的,所以也是采用对应Handlebars的模板引擎来处理的。
输出处理后的模板文件相关逻辑
具体源码如下:
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 }
opts.complete(data, helpers)
} else {
logMessage(opts.completeMessage, data)
}
})
这部分的逻辑就是定义输出目录的路径和具体的编译逻辑,这里涉及到metalsmith的使用,这里就不展开。
需要注意complete函数的逻辑,在模板文件编译输出后会执行complete函数的逻辑,实际上该函数逻辑主要就是自动按照依赖,complete函数主要逻辑如下:
complete: function(data, { chalk }) {
// 安装依赖
if (data.autoInstall) {
installDependencies(cwd, data.autoInstall, green);
} else {
printMessage(data, chalk)
}
},
vue init命令的执行逻辑至此完成。
总结
vue init webpack vue-demo
基于上面命令,这里总结下vue脚手架初始化项目的整体流程:
- 基于第三方库commander建立脚手架提供的自定义命令
- 判断模板类型是本地模板、官方模板还是自定义模板,做相关处理,这里使用官方模板
- 使用第三方库download-git-repo下载官方模板vuejs-templates的webpack模块仓库
- 开启对webpack模板仓库的编译,输出编译后的模板到指定目录
- 首先读取webpack模板仓库的meta.js或meta.json文件,获取到对应的options对象
- 然后使用metalsmith整合流程:使用ininquirer处理与用户交互、过滤文件、使用consolidate调用模板引擎编译所有模板文件
- metalsmith输出处理后的文件到指定目录下
通过对vue-cli脚手架的分析,知道了目前在Node.js环境下如何实现脚手架,同时也对vue官方模板等有了较为细致的了解,这对个人定制vue项目模板是非常有帮助的。