一,前言
由于最近需要做一款企业内部使用的 vueCodeBase,用做公司项目初始化脚手架;
常规的 vue init template <project-name> 脚手架不能满足需要;
因此,希望能够实现一款定制化的基于 template 模板之上的脚手架工具;
本篇,将会对 vue-cli 源码做简单分析,目的是了解脚手架生成过程,方便自定义脚手架;
相关内容:
vue-cli 2.9.6 源码: https://github.com/vuejs/vue-cli/tree/v2
vue 官方模板源码:https://github.com/vuejs-templates
vuejs-templates(webpack)文档:https://vuejs-templates.github.io/webpack/
二,vue-cli 使用和源码下载
1,安装 vue-cli
npm 安装 vue-cli:
2,创建工程
使用 vue init 命令,基于 webpack 模板创建工程:
vue init webpack <project-name>
复制代码
通过以上两个命令,可以得到一个基于 webpack 模板生成的项目脚手架;
3,源码下载
clone vue-cli branch v2:
git clone -b v2 https://github.com/vuejs/vue-cli
复制代码
vue-cli 源码项目结构如下:
三,vue-cli 相关命令分析
1,命令路径
查看 vue-cli 安装成功后的输出信息:
查看上图文件:/usr/local/lib/node_modules/vue-cli/bin:
查看 package.json 文件:
bin 中指定命令对应的可执行文件位置;
备注:在 vue-cli 2.X 版本中,不支持 vue-build、vue-create;
2,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)
复制代码
执行 vue 命令,查看输出内容:
3,vue-init 命令
vue init webpack vueCodeBase:
注意:交互式命令获取参数并不是在 vue-cli 中实现的,而是在模板项目 mate.js
本篇主要介绍 vue-cli 相关命令源码,即 vue init 实现;
vue-init 源码:
#!/usr/bin/env node
// 从仓库下载代码-GitHub,GitLab,Bitbucket
const download = require('download-git-repo')
// 创建子命令,切割命令行参数并执行
const program = require('commander')
// 检查文件是否存在
const exists = require('fs').existsSync
// 路径模块
const path = require('path')
// loading
const ora = require('ora')
// 获取主目录路径
const home = require('user-home')
// 绝对路径转换为相对路径
const tildify = require('tildify')
// 命令行字体颜色
const chalk = require('chalk')
// 交互式命令行,可在控制台提问
const inquirer = require('inquirer')
// 包装rm -rf命令,删除文件和文件夹
const rm = require('rimraf').sync
// 日志
const logger = require('../lib/logger')
// 自动生成
const generate = require('../lib/generate')
// 检查版本
const checkVersion = require('../lib/check-version')
// 警告
const warnings = require('../lib/warnings')
const localPath = require('../lib/local-path')
// 是否本地方法
const isLocalPath = localPath.isLocalPath
// 模板路径方法
const getTemplatePath = localPath.getTemplatePath
/**
* Usage.
* 从命令中获取参数
* program.args[0] 模板类型
* program.args[1] 自定义项目名称
* program.clone clone
* program.offline 离线
*/
program
.usage('<template-name> [project-name]')
.option('-c, --clone', 'use git clone')
.option('--offline', 'use cached template')
/**
* Help.
*/
program.on('--help', () => {
console.log(' Examples:')
console.log()
console.log(chalk.gray(' # create a new project with an official template'))
console.log(' $ vue init webpack my-project')
console.log()
console.log(chalk.gray(' # create a new project straight from a github template'))
console.log(' $ vue init username/repo my-project')
console.log()
})
/**
* Help.
*/
function help () {
program.parse(process.argv)
if (program.args.length < 1) return program.help()
}
help()
/**
* Settings.
*/
// 模板类型:获取第一个参数,如:webpack
let template = program.args[0]
// 是否有“/”符号
const hasSlash = template.indexOf('/') > -1
// 自定义项目名称,如:my-project
const rawName = program.args[1]
// rawName存在或者为“.”的时候,视为在当前目录下构建
const inPlace = !rawName || rawName === '.'
// path.relative():根据当前工作目录返回相对路径
const name = inPlace ? path.relative('../', process.cwd()) : rawName
// 合并路径
const to = path.resolve(rawName || '.')
// 检查参数是否clone
const clone = program.clone || false
// path.join():使用平台特定分隔符,将所有给定的路径连接在一起,然后对结果路径进行规范化
// 如 : /Users/admin/.vue-templates/webpack
const tmp = path.join(home, '.vue-templates', template.replace(/[\/:]/g, '-'))
// 是否线下:如果是线下,直接使用当前地址,否则去线上仓库下载
if (program.offline) {
console.log(`> Use cached template at ${chalk.yellow(tildify(tmp))}`)
// 设置为离线地址
template = tmp
}
/**
* Padding.
*/
console.log()
process.on('exit', () => {
console.log()
})
// 目录存在时询问,通过后执行run函数,否则直接执行run函数
if (inPlace || exists(to)) {
inquirer.prompt([{
type: 'confirm',
message: inPlace
// 是否在当前目录下构建项目
? 'Generate project in current directory?'
// 构建目录已存在,是否继续
: 'Target directory exists. Continue?',
name: 'ok'
}]).then(answers => {
if (answers.ok) {
run()
}
}).catch(logger.fatal)
} else {
run()
}
/**
* Check, download and generate the project.
*/
function run () {
// 本地模板
if (isLocalPath(template)) {
// 获取绝对路径
const templatePath = getTemplatePath(template)
// 存在-使用本地模板生成
if (exists(templatePath)) {
generate(name, templatePath, to, err => {
if (err) logger.fatal(err)
console.log()
logger.success('Generated "%s".', name)
})
// 本地模板不存在-报错
} else {
logger.fatal('Local template "%s" not found.', template)
}
// 非本地模板
} else {
// 版本检查
checkVersion(() => {
// 不包含“/”,去官网下载
if (!hasSlash) {
// use official templates
const officialTemplate = 'vuejs-templates/' + template
// 模板名是否带"#"
if (template.indexOf('#') !== -1) {
downloadAndGenerate(officialTemplate)
} else {
if (template.indexOf('-2.0') !== -1) {
warnings.v2SuffixTemplatesDeprecated(template, inPlace ? '' : name)
return
}
// warnings.v2BranchIsNowDefault(template, inPlace ? '' : name)
// 下载官方模板
downloadAndGenerate(officialTemplate)
}
// 包含“/”,去自己的仓库下载
} else {
downloadAndGenerate(template)
}
})
}
}
/**
* Download a generate from a template repo.
* 从模板仓库下载代码
* @param {String} template
*/
function downloadAndGenerate (template) {
// loading
const spinner = ora('downloading template')
spinner.start()
// Remove if local template exists
if (exists(tmp)) rm(tmp)
// download-git-repo:从仓库下载代码-GitHub,GitLab,Bitbucket
// template:模板名 tmp:模板路径 clone:是否采用git clone模板 err:错误信息
download(template, tmp, { clone }, err => {
spinner.stop()
// error!!
if (err) logger.fatal('Failed to download repo ' + template + ': ' + err.message.trim())
generate(name, tmp, to, err => {
// error!!
if (err) logger.fatal(err)
console.log()
// success
logger.success('Generated "%s".', name)
})
})
}
复制代码
4,vue init <template-name> <project-name> 执行过程分析
1,获取参数
/**
* Usage.
* 从命令中获取参数
* program.args[0] 模板类型
* program.args[1] 自定义项目名称
* program.clone clone
* program.offline 离线
*/
program
.usage('<template-name> [project-name]')
.option('-c, --clone', 'use git clone')
.option('--offline', 'use cached template')
复制代码
2,获取模板路径:
// rawName存在或者为“.”的时候,视为在当前目录下构建
const inPlace = !rawName || rawName === '.'
// path.relative():根据当前工作目录返回相对路径
const name = inPlace ? path.relative('../', process.cwd()) : rawName
// 合并路径
const to = path.resolve(rawName || '.')
// 检查参数是否clone
const clone = program.clone || false
// path.join():使用平台特定分隔符,将所有给定的路径连接在一起,然后对结果路径进行规范化
// 如 : /Users/admin/.vue-templates/webpack
const tmp = path.join(home, '.vue-templates', template.replace(/[\/:]/g, '-'))
复制代码
3,run 函数:区分本地和离线,下载模板
// 本地路径存在
if (isLocalPath(template)) {
// 获取绝对路径
const templatePath = getTemplatePath(template)
// 本地下载...
} else {
// 不包含“/”,去官网下载
if (!hasSlash) {
const officialTemplate = 'vuejs-templates/' + template
// 包含“/”,去自己的仓库下载
} else {
}
}
复制代码
引用的开源项目:
commander:命令行工具开发库
https://github.com/tj/commander.js/
generate命令调用lib/generate文件,使用了metalsmith:控制输出内容
https://github.com/segmentio/metalsmith
复制代码
5,vue-list 命令
vue-list 比较简单,主要是获取并显示官方 git 仓库中模板信息列表
vue-list 源码:
#!/usr/bin/env node
const logger = require('../lib/logger')
const request = require('request')
const chalk = require('chalk')
/**
* Padding.
*/
console.log()
process.on('exit', () => {
console.log()
})
/**
* List repos.
* 仓库列表:https://api.github.com/users/vuejs-templates/repos
*/
request({
url: 'https://api.github.com/users/vuejs-templates/repos',
headers: {
'User-Agent': 'vue-cli'
}
}, (err, res, body) => {
if (err) logger.fatal(err)
const requestBody = JSON.parse(body)
if (Array.isArray(requestBody)) {
console.log(' Available official templates:')
console.log()
requestBody.forEach(repo => {
console.log(
// 黄色星星符号
' ' + chalk.yellow('★') +
// 仓库名使用蓝色字体
' ' + chalk.blue(repo.name) +
' - ' + repo.description)
})
} else {
console.error(requestBody.message)
}
})
复制代码
6,vue-build 命令
vue-build 命令在 vue-cli 2.x 不支持:
vue-build 源码:
#!/usr/bin/env node
const chalk = require('chalk')
console.log(chalk.yellow(
'\n' +
' We are slimming down vue-cli to optimize the initial installation by ' +
'removing the `vue build` command.\n' +
' Check out Poi (https://github.com/egoist/poi) which offers the same functionality!' +
'\n'
))
复制代码
翻译一下:
我们通过删除“vue build”命令来减少 vue-cli,从而优化初始安装。
签出 Poi(https://github.com/egoist/poi),提供相同的功能!
7,vue-create 命令
vue create 是 Vue CLI 3 命令;
提示卸载 vue-cli,安装 @vue/cli,升级到 Vue CLI 3;
vue-create 源码:
#!/usr/bin/env node
const chalk = require('chalk')
console.log()
console.log(
` ` +
chalk.yellow(`vue create`) +
' is a Vue CLI 3 only command and you are using Vue CLI ' +
require('../package.json').version + '.'
)
console.log(` You may want to run the following to upgrade to Vue CLI 3:`)
console.log()
console.log(chalk.cyan(` npm uninstall -g vue-cli`))
console.log(chalk.cyan(` npm install -g @vue/cli`))
console.log()
复制代码
四,下载模板 download-git-repo
通过对 vue-init 命令执行过程的分析,了解到命令的各种参数作用;
可以通过命令来指定线上/线下,指定仓库获取 Vue 脚手架模板;
// 下载模板
download(template, tmp, { clone }, err => {
// 渲染模板
generate(name, tmp, to, err => {
})
})
复制代码
https://github.com/flipxfx/download-git-repo
https://github.com/flipxfx/download-git-repo/blob/master/index.js
1,下载模板核心逻辑
在 vue-cli 中,使用 download-git-repo 的 download 方法从模板仓库下载模板:
/**
* Download `repo` to `dest` and callback `fn(err)`.
*
* @param {String} repo 仓库
* @param {String} dest 目标
* @param {Object} opts 参数
* @param {Function} fn 回调
*/
function download (repo, dest, opts, fn) {
// 回调
if (typeof opts === 'function') {
fn = opts
opts = null
}
// clone?
opts = opts || {}
var clone = opts.clone || false
// 规范仓库字符串(根据type转换为github.com,gitlab.com,bitbucket.com)
repo = normalize(repo)
// 构建下载模板的URL地址(区分github,gitlab,bitbucket )
var url = repo.url || getUrl(repo, clone)
// clone
if (clone) {
// 非官方库下载 var gitclone = require('git-clone')
gitclone(url, dest, { checkout: repo.checkout, shallow: repo.checkout === 'master' }, function (err) {
if (err === undefined) {
rm(dest + '/.git')
fn()
} else {
fn(err)
}
})
// 官方模板库:var downloadUrl = require('download')
} else {
downloadUrl(url, dest, { extract: true, strip: 1, mode: '666', headers: { accept: 'application/zip' } })
.then(function (data) {
fn()
})
.catch(function (err) {
fn(err)
})
}
}
复制代码
vue-cli 调用的这个下载方法,最终执行 gitclone 或 downloadUrl 对代码进行下载;
再此之前,先对仓库地址进行了一系列处理;
2,创建 repo 对象
规范仓库字符串(根据type转换为github.com,gitlab.com,bitbucket.com)
对 string 类型的仓库地址 repo 进行处理,转为 repo 对象:
/**
* Normalize a repo string.
* @param {String} repo 字符串类型的仓库地址
* @return {Object} 返回仓库地址对象
*/
function normalize (repo) {
// direct类型匹配
var regex = /^(?:(direct):([^#]+)(?:#(.+))?)$/
var match = regex.exec(repo)
if (match) {
var url = match[2]
var checkout = match[3] || 'master'
return {
type: 'direct',
url: url,
checkout: checkout
}
} else {
// 其他类型匹配
regex = /^(?:(github|gitlab|bitbucket):)?(?:(.+):)?([^\/]+)\/([^#]+)(?:#(.+))?$/
match = regex.exec(repo)
var type = match[1] || 'github'
var origin = match[2] || null
var owner = match[3]
var name = match[4]
var checkout = match[5] || 'master'
// 如果origin为空,尝试根据type进行补全
if (origin == null) {
if (type === 'github')
origin = 'github.com'
else if (type === 'gitlab')
origin = 'gitlab.com'
else if (type === 'bitbucket')
origin = 'bitbucket.com'
}
// 返回 repo 仓库对象
return {
type: type, // 仓库类型
origin: origin, // 仓库host
owner: owner, // 仓库所有者
name: name, // 工程名
checkout: checkout // 分支
}
}
}
复制代码
3,download 规则
download-git-repo 的 download 规则:
下载范例:
download('gitlab:mygitlab.com:flipxfx/download-git-repo-fixture#my-branch', 'test/tmp', function (err) {
console.log(err ? 'Error' : 'Success')
})
复制代码
Shorthand:
^(?:(github|gitlab|bitbucket):)?(?:(.+):)?([^\/]+)\/([^#]+)(?:#(.+))?$
flipxfx/download-git-repo-fixture
bitbucket:flipxfx/download-git-repo-fixture#my-branch
gitlab:mygitlab.com:flipxfx/download-git-repo-fixture#my-branch
复制代码
direct:
^(?:(direct):([^#]+)(?:#(.+))?)$
direct:https://gitlab.com/flipxfx/download-git-repo-fixture/repository/archive.zip
direct:https://gitlab.com/flipxfx/download-git-repo-fixture.git
direct:https://gitlab.com/flipxfx/download-git-repo-fixture.git#my-branch
复制代码
4,构建下载模板 url
构建下载模板的 URL 地址(支持 github,gitlab,bitbucket ):
使用上一步转换出来的 repo 仓库对象,进一步转换得到模板的 url 地址:
/**
* Return a zip or git url for a given `repo`.
* 得到下载模板的最终地址
* @param {Object} repo 仓库对象
* @return {String} url
*/
function getUrl (repo, clone) {
var url
// 使用协议获取源代码并添加尾随斜杠或冒号(用于SSH)
// 附加协议(附加git@或https://协议):
var origin = addProtocol(repo.origin, clone)
if (/^git\@/i.test(origin))
origin = origin + ':'
else
origin = origin + '/'
// 构建URL
// clone
if (clone) {
url = origin + repo.owner + '/' + repo.name + '.git'
// 非clone(区分:github,gitlab,bitbucket)
} else {
// github
if (repo.type === 'github')
url = origin + repo.owner + '/' + repo.name + '/archive/' + repo.checkout + '.zip'
// gitlab
else if (repo.type === 'gitlab')
url = origin + repo.owner + '/' + repo.name + '/repository/archive.zip?ref=' + repo.checkout
// bitbucket
else if (repo.type === 'bitbucket')
url = origin + repo.owner + '/' + repo.name + '/get/' + repo.checkout + '.zip'
}
return url
}
复制代码
在构造 URL 前,为 git 仓库添加附加协议(附加 git@或 https://协议):
/**
* Adds protocol to url in none specified
* 为URL添加协议
* @param {String} url
* @return {String}
*/
function addProtocol (origin, clone) {
if (!/^(f|ht)tps?:\/\//i.test(origin)) {
if (clone)
origin = 'git@' + origin
else
origin = 'https://' + origin
}
return origin
}
复制代码
五,模板渲染
1,模板渲染流程
模板下载完成后,开始执行模板渲染:
// 下载模板
download(template, tmp, { clone }, err => {
// 渲染模板
generate(name, tmp, to, err => {
//。。。
})
})
复制代码
在下载模板时,以交互式命令方式获取到用户输入的参数:
根据设置的参数,对模板进行配置,引用了 …/lib/generate
https://github.com/vuejs/vue-cli/blob/v2/lib/generate.js
使用一下相关类库:
// 高亮打印信息
const chalk = require('chalk')
// 静态网站生成器
const Metalsmith = require('metalsmith')
// Handlebars模板引擎
const Handlebars = require('handlebars')
// 异步处理工具
const async = require('async')
// 模板引擎渲染
const render = require('consolidate').handlebars.render
// node路径模块
const path = require('path')
// 多条件匹配
const multimatch = require('multimatch')
// 获取模板配置
const getOptions = require('./options')
// 询问开发者
const ask = require('./ask')
// 文件过滤
const filter = require('./filter')
// 日志
const logger = require('./logger')
复制代码
2,模板渲染的主要逻辑
generate 方法:
/**
* Generate a template given a `src` and `dest`.
* 生成一个模板,给定一个“Src”和“Dest`”
* @param {String} name
* @param {String} src
* @param {String} dest
* @param {Function} done
*/
module.exports = function generate (name, src, dest, done) {
// 获取配置-src是模板下载成功之后的临时路径
const opts = getOptions(name, src)
// 初始化Metalsmith对象-读取的内容是模板的tempalte目录
// metalsmith返回文件路径和内容的映射对象, 方便metalsmith中间件对文件进行处理
const metalsmith = Metalsmith(path.join(src, 'template'))
// 添加变量至metalsmith
const data = Object.assign(metalsmith.metadata(), {
destDirName: name,
inPlace: dest === process.cwd(),
noEscape: true
})
// 注册配置对象中的helper
opts.helpers && Object.keys(opts.helpers).map(key => {
Handlebars.registerHelper(key, opts.helpers[key])
})
const helpers = { chalk, logger }
// 配置对象是否含有before函数,如果有before函数就执行
if (opts.metalsmith && typeof opts.metalsmith.before === 'function') {
opts.metalsmith.before(metalsmith, opts, helpers)
}
// vue cli使用了三个中间件来处理模板
metalsmith
// 询问mate.js中prompts配置的问题
.use(askQuestions(opts.prompts))
// 根据配置对文件进行过滤
.use(filterFiles(opts.filters))
// 渲染模板文件
.use(renderTemplateFiles(opts.skipInterpolation))
// 配置对象是否含有after函数,如果有after函数就执行
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('.') // 从模板根开始而不是“./Src”,这是MalalSmith'缺省的“源”
.destination(dest)
.build((err, files) => {
done(err)
// //配置对象有complete函数则执行
if (typeof opts.complete === 'function') {
const helpers = { chalk, logger, files }
opts.complete(data, helpers)
} else {
// 配置对象有completeMessage,执行logMessage函数
logMessage(opts.completeMessage, data)
}
})
return data
}
复制代码
注意:读取的内容来自模板 template 目录
const metalsmith = Metalsmith(path.join(src, ‘template’))
3,handlebars
接下来,注册 handlebars 模板 Helper-if_eq 和 unless_eq
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)
})
复制代码
4,askQuestions
中间件 askQuestions:用于读取用户输入
/**
* Create a middleware for asking questions.
* 询问mate.js中prompts配置的问题
* @param {Object} prompts
* @return {Function}
*/
function askQuestions (prompts) {
return (files, metalsmith, done) => {
ask(prompts, metalsmith.metadata(), done)
}
}
复制代码
meta.{js,json} 样例:
{
"prompts": {
"name": {
"type": "string",
"required": true,
"message" : "Project name"
},
"version": {
"type": "input",
"message": "project's version",
"default": "1.0.0"
}
}
}
复制代码
在 ask 中, 对 meta 信息中的 prompt 会有条件的咨询用户:
// vue-cli/lib/ask.js#prompt
inquirer.prompt([{
type: prompt.type,
message: prompt.message,
default: prompt.default
//...
}], function(answers) {
// 保存用户的输入
})
复制代码
经过 askQuestions 中间件处理后, global metadata 是一个以 prompt 中的 key 为键, 用户的输入为值的对象:
{
name: 'my-project',
version: '1.0.0'...
}
复制代码
5,filterFiles
中间件 filterFiles:根据 meta 信息中的 filters 文件进行过滤:
/**
* Create a middleware for filtering files.
* 创建用于过滤文件的中间件
* @param {Object} filters
* @return {Function}
*/
function filterFiles (filters) {
return (files, metalsmith, done) => {
filter(files, filters, metalsmith.metadata(), done)
}
}
复制代码
filter 源码:
// vue-cli/lib/filter.js
module.exports = function (files, filters, data, done) {
// 没filters,直接调用done()返回
if (!filters) {
return done()
}
// 得到全部文件名
var fileNames = Object.keys(files)
// 遍历filters,进行匹配,删除不需要的文件
Object.keys(filters).forEach(function (glob) {
fileNames.forEach(function (file) {
if (match(file, glob, { dot: true })) {
// 获取到匹配的值
var condition = filters[glob]
// evaluate用于执行js表达式,在vue-cli/lib/eval.js
// var fn = new Function('data', 'with (data) { return ' + exp + '}')
if (!evaluate(condition, data)) {
// 删除文件-根据用户输入过滤掉不需要的文件
delete files[file]
}
}
})
})
done()
}
复制代码
6,renderTemplateFiles
renderTemplateFiles 中间件:执行渲染模板操作:
/**
* Template in place plugin.
* 渲染模板文件
* @param {Object} files 全部文件对象
* @param {Metalsmith} metalsmith metalsmith对象
* @param {Function} done
*/
function renderTemplateFiles (skipInterpolation) {
// skipInterpolation如果不是数组,就转成数组,确保为数组类型
skipInterpolation = typeof skipInterpolation === 'string'
? [skipInterpolation]
: skipInterpolation
return (files, metalsmith, done) => {
// 获取files对象所有的key
const keys = Object.keys(files)
// 获取metalsmith对象的metadata
const metalsmithMetadata = metalsmith.metadata()
// 异步处理所有key对应的files
async.each(keys, (file, next) => {
// 跳过符合skipInterpolation的配置的file
if (skipInterpolation && multimatch([file], skipInterpolation, { dot: true }).length) {
return next()
}
// 获取文件内容
const str = files[file].contents.toString()
// 跳过不符合handlebars语法的file(不渲染不含mustaches表达式的文件)
if (!/{{([^{}]+)}}/g.test(str)) {
return next()
}
// 调用handlebars完成文件渲染
render(str, metalsmithMetadata, (err, res) => {
if (err) {
err.message = `[${file}] ${err.message}`
return next(err)
}
files[file].contents = new Buffer(res)
next()
})
}, done)
}
}
复制代码
显示模板完成信息:
模板文件渲染完成后, metalsmith 会将最终结果 build 到 dest 目录
如果 build 失败, 会将 err 信息传给回调输出;
build 成功后,如果 meta 信息有 complete 函数则调用,有 completeMessage 则输出:
/**
* Display template complete message.
*
* @param {String} message 消息
* @param {Object} data 数据
*/
function logMessage (message, data) {
// 如果没有message,直接return
if (!message) return
// 渲染信息
render(message, data, (err, res) => {
if (err) {
console.error('\n Error when rendering template complete message: ' + err.message.trim())
} else {
console.log('\n' + res.split(/\r?\n/g).map(line => ' ' + line).join('\n'))
}
})
}
复制代码
7,读取配置信息
options.js 中读取配置信息(来自 meta{.json/.js}和 getGitUser):
const getGitUser = require('./git-user')
module.exports = function options (name, dir) {
// 读取模板的meta(.json/.js)信息
// dir 是模板下载成功之后的临时路径
const opts = getMetadata(dir)
// 向配置对象添加字段默认值
setDefault(opts, 'name', name)
// 检测配置对象中name字段是否合法
setValidateName(opts)
// 读取用户的git昵称和邮箱,用于设置meta信息默认属性
const author = getGitUser()
if (author) {
setDefault(opts, 'author', author)
}
return opts
}
复制代码
8,流程总结
总结一下整个模板渲染的流程:
获取模板配置,
初始化 Metalsmith,添加变量至 Metalsmith
handlebars 模板注册 helper
执行 before 函数(如果有)
询问问题,过滤文件,渲染模板文件
执行 after 函数(如果有)
构建项目
构建完成后,有 complete 函数则执行,没有则打印配置对象中的 completeMessage 信息,有错误就执行回调函数 done(err)
9,其他模块简介
在整个过程中,还用到了 lib 文件夹中的其他文件,进行简单说明:
options.js
获取模板配置文件
设置name字段并检测name是否合法
设置author
getMetadata:获取meta.js或meta.json中的配置信息
setDefault: 向配置对象添加默认字段值
setValidateName: 检测name是否合法
git-user.js
用于获取本地的git配置的用户名和邮件,并返回格式 姓名<邮箱> 的字符串。
eval.js
在data的作用域执行exp表达式并返回其执行得到的值
ask.js
将meta.js或meta.json中prompts字段解析成问题并询问
filter.js
根据metalsmith.metadata()删除不需要模板文件
local-path.js
isLocalPath:
UNIX (以“.”或者"/"开头) WINDOWS(以形如:“C:”的方式开头)
getTemplatePath:
templatePath是绝对路径返回templatePath,否则转为绝对路径并规范化
check-version.js
检查本地node版本,是否达到package.json中对node版本的要求
获取vue-cli最新版本号,和package.json中version字段比较,提示升级
warnings.js
v2SuffixTemplatesDeprecated:
提示“-2.0”模板已弃用,官方默认2.0。不需要用“-2.0”区分1.0和2.0
v2BranchIsNowDefault:
vue-init中已被注释,默认使用2.0
logger.js
复制代码
六,从非官方模板库获取 vue 脚手架模板生成脚手架项目
通过以上代码分析,了解从 vue init 到下载模板,生成脚手架的整个过程和原理
这样就可以使用 vue init 命令从指定仓库获取自定义 Vue 脚手架模板了;
例如:我们可以 fork 一份官方的 webpack 脚手架模板到自己的 github 仓库;
官方 webpack 模板地址:https://github.com/vuejs-templates/webpack
然后,通过 vue init username/repo my-project 生成 Vue 脚手架;
如:vue init BraveWangDev/webpack my-project:
七,结尾
通过分析 Vue cli 命令源码,了解 vue 相关命令的运行机制;
在自定义 Vue 脚手架模板中,vue-init、generate.js、options.js、ask.js、filter.js,这五个文件构成了 vue-cli 构建项目的主流程;
通过 vue-cli 获取的模板工程一定要具备以下两点:
好了,接下来就要开始自定义 Vue 脚手架模板了;
评论