vue cli

129 阅读3分钟

cli 脚手架

vue-router 事实上是一个栈,不同的vue实例做一个集中化处理

若干个实例中有状态机,支持若干个实例中的状态流转

脚手架在最底层,用来初始化模版

image.png

vue cli 作用

  1. 基础,用于生成项目
  2. metadata 配置 问询用户交互
  3. 渲染 生成

初始化

vue init 参数一 模版

可以通过vue list 查看有哪些模版: webpack webpack-mini pwa

新版本为 vue create

主逻辑

  1. 本地已经有默认模版,直接拉取local-template > 默认/自定义
  2. vue-cli 拉取本地没有的remote-template > 官方模版/第三方模版

image.png

源码解析

bin 主要命令主入口

  1. vue-build 范式化模版
  2. create v-3版本
  3. list 提供列举模版 4.vue-init

从大的结构开始看,先上后下 function run () 入口

rawName: my-project inPlace: 如果是true,指向当前路径构架

exists: 是否存在当前路径

// 是否为当前目录下构建 or 存在当前路径
if (inPlace || exists(to)) {
  inquirer.prompt([{
    type: 'confirm',
    message: inPlace
      ? 'Generate project in current directory?'
      : 'Target directory exists. Continue?',

init定义主函数: run (): isLocalPath(template): 是否模版是本地的 getTemplatePath(template): 获取文件路径 **本地文件都在ls -a ~/**中的.vue-templates

image.png

开始生成框架: generate()

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

logger 打印状态

检查版本号:是否有slash

先下载,后生成: downloadAndGenerate()

官方直接下载;非官方拼接一段

checkVersion(() => {
  // 官方 or 第三方
  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)
  }

init 的主流程如下: image.png

其他一些工具: 下载远程仓库 const download = require('download-git-repo')

命令行处理工具 const program = require('commander')

路径检测 const exists = require('fs').existsSync

用户根目录 const home = require('user-home')

波浪符路径转换 const tildify = require('tildify')

高亮 const chalk = require('chalk')

命令行与开发者交流的工具 const inquirer = require('inquirer')

日志打印 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

配置commander使用方法

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

工具类函数使用: github.com/tj/commande…

settings:

模板名称 let template = program.args[0]

是否包含斜杠,主要判断是否是官方 const hasSlash = template.indexOf('/') > -1

项目构建目录名 const rawName = program.args[1]

前往绝对路径 const to = path.resolve(rawName || '.')

模版加载地址

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
}

lib库中的generator

读取配置项入口 const opts = getOptions(name, src)

静态网页生成,利用配置生成网页 const Metalsmith = require('metalsmith')

metalsmith初始化 const metalsmith = Metalsmith(path.join(src, 'template'))

配置项合并

const data = Object.assign(metalsmith.metadata(), {
  destDirName: name,
  inPlace: dest === process.cwd(),
  noEscape: true
})

// 配置对象
opts.helpers && Object.keys(opts.helpers).map(key => {
  Handlebars.registerHelper(key, opts.helpers[key])
})

const helpers = { chalk, logger }

// 调用before钩子
if (opts.metalsmith && typeof opts.metalsmith.before === 'function') {
  opts.metalsmith.before(metalsmith, opts, helpers)
  console.log('metalsmith before')
}

// 询问
metalsmith.use(askQuestions(opts.prompts))
  // 配置过滤
  .use(filterFiles(opts.filters))
  .use(renderTemplateFiles(opts.skipInterpolation))

// 直接执行(ms为函数,直接执行)
// 有after配置的 执行after钩子
if (typeof opts.metalsmith === 'function') {
  opts.metalsmith(metalsmith, opts, helpers)
  console.log('metalsmith exec')
} else if (opts.metalsmith && typeof opts.metalsmith.after === 'function') {
  opts.metalsmith.after(metalsmith, opts, helpers)
  console.log('metalsmith after')
}

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)
    // 结尾后执行complete回调
    if (typeof opts.complete === 'function') {
      const helpers = { chalk, logger, files }
      opts.complete(data, helpers)
      console.log('metalsmith complete')
    } else {
      // 有completeMessage 完成调用logMessage
      logMessage(opts.completeMessage, data)
    }
  })

generator 函数:构建项目

  1. opts 获取vue默认参数

image.png

  1. generator 流程

image.png

模版引擎:handleBars const Handlebars = require('handlebars')

生成单页的HTML文件,通过路由生成若干个html文件 -> 项目中的多页形态 image.png

run dev 实时生成多个HTML 文件,不同的页面绑定不同HTML 文件,不同的文件加载的不同的依赖(jquery、vue、跨平台)

模板引擎解析渲染器 const render = require('consolidate').handlebars.render

多个条件匹配

const multimatch = require('multimatch')
const getOptions = require('./options')
const ask = require('./ask')
const filter = require('./filter')
const logger = require('./logger')

用户交互入口

function askQuestions (prompts) {
  return (files, metalsmith, done) => {
    // 自定义函数
    ask(prompts, metalsmith.metadata(), done)
  }
}

ask 函数

// meta.json meta.js prompts
// data - Metalsmith.metadata()
// done - next流程
module.exports = function ask (prompts, data, done) {
  async.eachSeries(Object.keys(prompts), (key, next) => {
    prompt(data, key, prompts[key], next)
  }, done)
}
function prompt (data, key, prompt, done) {
  // skip prompts whose when condition is not met
  if (prompt.when && !evaluate(prompt.when, data)) {
    return done()
  }
  
  // 赋予默认值
  let promptDefault = prompt.default
  if (typeof prompt.default === 'function') {
    promptDefault = function () {
      return prompt.default.bind(this)(data)
    }
  }

  inquirer.prompt([{
    type: promptMapping[prompt.type] || prompt.type,
    name: key,
    message: prompt.message || prompt.label || key,
    default: promptDefault,
    choices: prompt.choices || [],
    validate: prompt.validate || (() => true)
  }]).then(answers => {
    // 根据答案类型拼接数据
    if (Array.isArray(answers[key])) {
      data[key] = {}
      answers[key].forEach(multiChoiceAnswer => {
        data[key][multiChoiceAnswer] = true
      })
    } else if (typeof answers[key] === 'string') {
      data[key] = answers[key].replace(/"/g, '\\"')
    } else {
      data[key] = answers[key]
    }
    done()
  }).catch(done)
}

过滤配置

function filterFiles (filters) {
  return (files, metalsmith, done) => {
    // 过滤自定义函数
    filter(files, filters, metalsmith.metadata(), done)
  }
}
// 经过ask获取用户需求之后 => metadata => 过滤掉不需要的模板文件

// files 模板内所有文件
// filters - 配置中的filters字段
// metadata
// done => next

module.exports = (files, filters, data, done) => {
  if (!filters) {
    return done()
  }
  const fileNames = Object.keys(files)
  Object.keys(filters).forEach(glob => {
    fileNames.forEach(file => {
      // 匹配文件名
      if (match(file, glob, { dot: true })) {
        const condition = filters[glob]
        // condition表达式不成立就删除文件
        if (!evaluate(condition, data)) {
          delete files[file]
        }
      }
    })
  })
  done()
}

渲染模版文件

function renderTemplateFiles (skipInterpolation) {
  // 确保传入数组
  skipInterpolation = typeof skipInterpolation === 'string'
    ? [skipInterpolation]
    : skipInterpolation
  return (files, metalsmith, done) => {
    const keys = Object.keys(files)
    const metalsmithMetadata = metalsmith.metadata()
    async.each(keys, (file, next) => {
      // 进入异步处理每个文件
      // skipInterpolation - 要跳过的文件
      // skipping files with skipInterpolation option
      if (skipInterpolation && multimatch([file], skipInterpolation, { dot: true }).length) {
        return next()
      }
      // 内容字符串
      const str = files[file].contents.toString()
      // do not attempt to render files that do not have mustaches
      if (!/{{([^{}]+)}}/g.test(str)) {
        return next()
      }

      // 渲染文件
      render(str, metalsmithMetadata, (err, res) => {
        if (err) {
          err.message = `[${file}] ${err.message}`
          return next(err)
        }
        files[file].contents = new Buffer(res)
        next()
      })
    }, done)
  }
}

options.js 获取原配置文件 => name设置校验 => 和本地git作者信息做合并

const path = require('path')
// 读取元数据参数
const metadata = require('read-metadata')
const exists = require('fs').existsSync
// 获取本地git配置
const getGitUser = require('./git-user')
// npm包校验器
const validateName = require('validate-npm-package-name')

通过git 读到用户名字

const author = getGitUser()
  if (author) {
    setDefault(opts, 'author', author)
  }

git-user.js

const exec = require('child_process').execSync
// 对于用户git信息的收集
module.exports = () => {
  let name
  let email

  try {
    name = exec('git config --get user.name')
    email = exec('git config --get user.email')
  } catch (e) {}

  name = name && JSON.stringify(name.toString().trim()).slice(1, -1)
  email = email && (' <' + email.toString().trim() + '>')
  return (name || '') + (email || '')
}

check-version

检查node版本号(直接block) => 检查vue cli 的版本(提示)

module.exports = done => {
  // Ensure minimum supported node version is used
  if (!semver.satisfies(process.version, packageConfig.engines.node)) {
    return console.log(chalk.red(
      '  You must upgrade node to >=' + packageConfig.engines.node + '.x to use vue-cli'
    ))
  }

  request({
    url: 'https://registry.npmjs.org/vue-cli',
    timeout: 1000
  }, (err, res, body) => {
    if (!err && res.statusCode === 200) {
      const latestVersion = JSON.parse(body)['dist-tags'].latest
      const localVersion = packageConfig.version
      if (semver.lt(localVersion, latestVersion)) {
        console.log(chalk.yellow('  A newer version of vue-cli is available.'))
        console.log()
        console.log('  latest:    ' + chalk.green(latestVersion))
        console.log('  installed: ' + chalk.red(localVersion))
        console.log()
      }
    }
    done()
  })
}

如何将字符串转函数 eval() - 但是语法不支持的话查不到错误: 因此将eval 用new Function实现

module.exports = function evaluate (exp, data) {
  /* eslint-disable no-new-func */
  const fn = new Function('data', 'with (data) { return ' + exp + '}')
  try {
    return fn(data)
  } catch (e) {
    console.error(chalk.red('Error when evaluating filter condition: ' + exp))
  }
}

local-path.js 检查当前路径

logger.js 统一打印管理器

warnings.js 提示