写点什么

自定义 Vue 脚手架模板之:Vue-Cli 源码分析

用户头像
Brave
关注
发布于: 2021 年 10 月 07 日

一,前言


由于最近需要做一款企业内部使用的 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:

npm install -g 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 => {    })})
复制代码


  • 下载模板使用了 download-git-repo:

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.comgitlab.combitbucket.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

  • generate.js 文件:

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,流程总结

总结一下整个模板渲染的流程:

  1. 获取模板配置,

  2. 初始化 Metalsmith,添加变量至 Metalsmith

  3. handlebars 模板注册 helper

  4. 执行 before 函数(如果有)

  5. 询问问题,过滤文件,渲染模板文件

  6. 执行 after 函数(如果有)

  7. 构建项目

  8. 构建完成后,有 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 获取的模板工程一定要具备以下两点:

  • 工程根目录一定要有 meta.js 文件;

  • 工程下一定要有 template 文件夹,其中的内容才是真正的脚手架模板;


好了,接下来就要开始自定义 Vue 脚手架模板了;

用户头像

Brave

关注

还未添加个人签名 2018.12.13 加入

还未添加个人简介

评论

发布
暂无评论
自定义Vue脚手架模板之:Vue-Cli源码分析