一,前言

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

npm安装vue-cli:

npm install -g vue-cli

使用vue init命令创建工程(使用webpack模板)

vue init webpack <project-name>

通过以上两个命令,可以得到一个基于webpack模板生成的项目脚手架


源码下载:
clone vue-cli branch v2:

git clone -b v2 https://github.com/vuejs/vue-cli

vue-cli源码项目结构:
vue-cli源码目录结构


三,vue-cli命令分析

注意安装成功后的输出信息:
vue-cli安装输出
查看/usr/local/lib/node_modules/vue-cli/bin:
vue-cli命令集

查看package.json文件:
vue-cli-package

bin指定命令对应的可执行文件位置
在vue-cli 2.X版本中,vue-build,vue-create不支持


1,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命令,查看输出:
vue命令输出


2,vue-init命令:

vue init webpack vueCodeBase:
初始化脚手架

注意:交互式命令获取参数并不是在vue-cli中实现的,而是在模板项目mate.js
这篇主要说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)
    })
  })
}

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

3,vue-list命令:

vue-list比较简单,主要是获取并显示官方git仓库中模板信息列表
vue-list

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)
  }
})

####4,vue-build命令

vue-build命令在vue-cli 2.x不支持:
vue-build

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),提供相同的功能!

Poi:

前端工程化解决方案

5,vue-create命令:

vue createVue CLI 3命令
提示卸载vue-cli,安装@vue/cli,升级到Vue CLI 3
vue-create

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,规范仓库字符串(根据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		// 分支
    }
  }
}

download-git-repo的download规则

文档:https://www.npmjs.com/package/download-git-repo
js正则测试工具:http://tools.jb51.net/regex/javascript

下载范例:

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

3,构建下载模板的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
}

四,渲染模板

模板下载好了,开始渲染

// 下载模板
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')

主要逻辑:

/**
 * 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
}

注意:

读取的内容是模板的tempalte目录

const metalsmith = Metalsmith(path.join(src, ‘template’))


注册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)
})

中间件 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 为 key, 用户的输入为 value 的对象:

{
	name: 'my-project',
	version: '1.0.0'...
}

中间件 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()
}

使用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'))
    }
  })
}

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
}

总结一下模板渲染流程:

1,获取模板配置,
2,初始化Metalsmith,添加变量至Metalsmith
3,handlebars模板注册helper
4,执行before函数(如果有)
5,询问问题,过滤文件,渲染模板文件
6,执行after函数(如果有)
7,构建项目,
8,构建完成后,有complete函数则执行,
  没有则打印配置对象中的completeMessage信息,
  有错误就执行回调函数done(err)

其中还用到了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.02.0
	v2BranchIsNowDefault: 
		vue-init中已被注释,默认使用2.0
logger.js


五,从非官方模板库获取vue脚手架模板生成脚手架项目

通过以上代码,了解从vue init到下载模板,生成脚手架的整个过程和原理
这样就可以使用vue init命令从指定仓库获取自定义Vue脚手架模板了

例如:我们可以fork一份官方的webpack脚手架模板到自己的github仓库
官方webpack模板地址:https://github.com/vuejs-templates/webpack
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获取的模板工程一定要具备以下两点:
1,工程根目录一定要有meta.js文件
2,工程下一定要有template文件夹,这里边才是真正的脚手架模板

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

Logo

前往低代码交流专区

更多推荐