一,前言
由于最近需要做一款企业内部使用的 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,Bitbucketconst download = require('download-git-repo')// 创建子命令,切割命令行参数并执行const program = require('commander')// 检查文件是否存在const exists = require('fs').existsSync// 路径模块const path = require('path')// loadingconst 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. */// 模板类型:获取第一个参数,如:webpacklet template = program.args[0]// 是否有“/”符号const hasSlash = template.indexOf('/') > -1// 自定义项目名称,如:my-projectconst rawName = program.args[1]
// rawName存在或者为“.”的时候,视为在当前目录下构建const inPlace = !rawName || rawName === '.'// path.relative():根据当前工作目录返回相对路径 const name = inPlace ? path.relative('../', process.cwd()) : rawName// 合并路径const to = path.resolve(rawName || '.')// 检查参数是否cloneconst clone = program.clone || false// path.join():使用平台特定分隔符,将所有给定的路径连接在一起,然后对结果路径进行规范化// 如 : /Users/admin/.vue-templates/webpackconst 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 || '.')// 检查参数是否cloneconst clone = program.clone || false// path.join():使用平台特定分隔符,将所有给定的路径连接在一起,然后对结果路径进行规范化// 如 : /Users/admin/.vue-templates/webpackconst 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-fixturebitbucket:flipxfx/download-git-repo-fixture#my-branchgitlab:mygitlab.com:flipxfx/download-git-repo-fixture#my-branch
复制代码
direct:
^(?:(direct):([^#]+)(?:#(.+))?)$
direct:https://gitlab.com/flipxfx/download-git-repo-fixture/repository/archive.zipdirect:https://gitlab.com/flipxfx/download-git-repo-fixture.gitdirect: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#promptinquirer.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.jsmodule.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.0logger.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 脚手架模板了;
评论