Введите исходный код Vue-cli и самостоятельно создайте интерфейсный инструмент формирования шаблонов.

исходный код Командная строка JavaScript Vue.js

предисловие

Некоторое время назад я прочитал исходный код vue-cli и многому научился. Я хотел найти время, чтобы обновить статью, но в последнее время происходит много дел, и у меня нет времени разбираться в этих вещах. Воспользовавшись этими двумя днями свободного времени, я разобрался, а потом поделился с вами. Если ваши друзья набрали столько же, сколько и я после прочтения, надеюсь, вы дадите больше лайков, коллекций и поддержки.

Введение в Vue-Cli

Vue-cli — отличный инструмент для быстрого создания веб-приложений на основе Vue. В отличие от таких инструментов, как creat-react-app, разработчикам нужно сосредоточиться только на коде логики проекта, и им не нужно заботиться об упаковке веб-пакетов, запуске сервисов Node и так далее. Vue-cli – это инструмент разработки на основе шаблонов, который эквивалентен копированию структуры других проектов. Доступны все конфигурации. Вы можете внести некоторые изменения в конфигурацию в соответствии с реальной ситуацией, что является более гибким и бесплатным. Конечно, это выдвигает более высокие требования к фронтенд-инженерам, и тут есть над чем подумать. Тем не менее, Vue-cli вот-вот выпустит версию 3.0, и весь Vue-cli претерпел огромные изменения, он принимает модель таких инструментов, как creat-react-app, и разработчикам нужно только обращать внимание на код проекта. логика. Однако версия 3.0 еще не вышла, так чтоВ этом анализе исходного кода я использовал исходный код версии 2.9.3, который является кодом версии 2.0.. Следующие ребята должны обратить внимание на следующее при чтении.

Структура проекта Vue-cli

image

Структура каталогов всего проекта показана на рисунке выше.Ниже я кратко расскажу, что делает каждая папка.

  • bin(Некоторые командные файлы Vue размещены здесь, напримерvue initТакие команды управляются отсюда. )

  • docs(Некоторые меры предосторожности, неважные каталоги можно игнорировать напрямую.)

  • lib(Вот некоторые пользовательские методы, необходимые vue-cli.)

  • node_modules(Мне не нужно говорить больше здесь. Я думаю, что все это знают. Если вы не знаете, можете идти к стене! ●-● )

  • test(Модульное тестирование будет использоваться при разработке инструмента vue-cli, и мы можем игнорировать его непосредственно при чтении исходного кода.)

  • некоторые разные вещи(Например, конфигурация eslint, .gitignore, LICENSE и т. д. и т. д. не влияют на чтение исходного кода и могут быть напрямую проигнорированы.)

  • package.json/README.md(Вы можете пойти на стену, даже не подозревая об этом! ●-●)

В общем, нам нужно обращать внимание только на bin и lib при чтении исходного кода, а остальные можно игнорировать. Начните свое читательское путешествие

Экскурсия по чтению исходного кода Vue-cli

Прежде чем приступить к чтению исходного кода, сначала я хочу представить инструмент (commander), который является инструментом, используемым для обработки командной строки. Для конкретного использования см. README.md github https://github.com/tj/commander.js. Прежде чем читать следующий контент, друзья, рекомендуется сначала понятьcommander, чтобы облегчить последующее понимание. Здесь у нас естьcommanderЯ не буду вдаваться в подробности. Здесь vue-cli принимаетcommanderнаписание в стиле git. Файл vue обрабатывает команды vue, vue-init обрабатывает команды vue init и так далее. Затем мы смотрим на команды одну за другой.

vue

Импортированные пакеты:

  • commander(Для обработки командной строки.)

эффект:В файле vue очень мало кода, поэтому я вставлю его напрямую.

#!/usr/bin/env node

require('commander')
  .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')
  .parse(process.argv)

Этот файл в основном используется, когда пользователь вводит "vue", инструкции по использованию параметра отображаются на терминале. Для конкретного метода записи, пожалуйста, обратитесь к инструкциям на https://github.com/tj/commander.js.

vue build

Импортированные пакеты:

  • chalk(Используется для выделения информации, распечатываемой терминалом.)

эффект: vue buildКоманда была удалена из vue-cli, и в исходный код были внесены некоторые инструкции. Кода не так много, поэтому я вставлю его напрямую.


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

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

Импортированные пакеты:

  • request(Инструмент для отправки http-запросов.)
  • chalk(Используется для выделения информации, напечатанной console.log.)
  • logger(Пользовательский инструмент — для печати журнала.)

эффект:При входе "vue list"Время(Когда мы тестируем, мы можем напрямую ввести «bin/vue-list» на терминале в текущем каталоге исходного файла.), vue-cli запросит интерфейс, получит информацию об официальном шаблоне, а затем выполнит некоторую обработку и отобразит имя шаблона и соответствующее описание на терминале.

Эффект следующий:

  Available official templates:

  ★  browserify - A full-featured Browserify + vueify setup with hot-reload, linting & unit testing.
  ★  browserify-simple - A simple Browserify + vueify setup for quick prototyping.
  ★  pwa - PWA template for vue-cli based on the webpack template
  ★  simple - The simplest possible Vue setup in a single HTML file
  ★  webpack - A full-featured Webpack + vue-loader setup with hot reload, linting, testing & css extraction.
  ★  webpack-simple - A simple Webpack + vue-loader setup for quick prototyping.

vue init

"vue init" — это команда, используемая для сборки проекта, и это также основной файл vue-cli. Вышеприведенные три — очень простые команды, которые являются закуской для нас, чтобы прочитать исходный код. Настоящий праздник здесь.

процесс работы

Прежде чем говорить о коде, сначала мы должны поговорить о процессе всего начального проекта vue-cli, а затем мы шаг за шагом рассмотрим этот процесс.

image

весьvue initОбщий процесс показан на рисунке выше, который должен быть относительно простым для понимания. Здесь я кратко описываю общий процесс.

  1. vue-cli сначала определит, находится ли ваш шаблон в удаленном репозитории github или в вашем локальном файле.Если это локальная папка, он немедленно перейдет к шагу 3, в противном случае перейдите к шагу 2.

  2. Второй шаг определит, является ли это официальным шаблоном.Официальный шаблон загрузит шаблон из официального репозитория github в локальный репозиторий по умолчанию, то есть в папку .vue-templates в корневом каталоге.

  3. Третий шаг — прочитать файл meta.js или meta.json в каталоге шаблона, по содержимому будет задан вопрос разработчику, а некоторые модификации будут определены по ответу разработчика.

  4. В соответствии с содержимым шаблона и ответом разработчика структура проекта визуализируется и генерируется в указанный каталог.

Исходный контент

здесьvue-initВ файле много кода, поэтому я разделю его на несколько частей. Прежде всего, я перечислю структуру всего файла для облегчения последующего чтения.

  /**
   * 引入一大堆包
   */
    const program = require('commander')
    ...
  
   
   /**
    * 配置commander的使用方法
    */     
    
    program
      .usage('<template-name> [project-name]')
      .option('-c, --clone', 'use git clone')
      .option('--offline', 'use cached template')
      
  /**
    * 定义commander的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()
    })
    
    
    function help () {
      program.parse(process.argv)
      if (program.args.length < 1) return program.help() //如果没有输入参数,终端显示帮助
    }
    help()
    
    /**
     * 定义一大堆变量
     */
     
     let template = program.args[0]
     ...
     
     /**
      * 判断是否输入项目名  是 - 直接执行run函数  否- 询问开发者是否在当前目录下生成项目,开发者回答“是” 也执行run函数 否则不执行run函数
      */
     
     /**
     * 定义主函数 run
     */
     function run (){
         ...
     }
     
     /**
      * 定义下载模板并生产项目的函数 downloadAndGenerate
      */
      function downloadAndGenerate(){
          ...
      }

Общее содержание всего файла показано выше, и мы рассмотрим его по частям позже.

Куча импортированных пакетов

const download = require('download-git-repo')  //用于下载远程仓库至本地 支持GitHub、GitLab、Bitbucket
const program = require('commander') //命令行处理工具
const exists = require('fs').existsSync  //node自带的fs模块下的existsSync方法,用于检测路径是否存在。(会阻塞)
const path = require('path') //node自带的path模块,用于拼接路径
const ora = require('ora') //用于命令行上的加载效果
const home = require('user-home')  //用于获取用户的根目录
const tildify = require('tildify') //将绝对路径转换成带波浪符的路径
const chalk = require('chalk')// 用于高亮终端打印出的信息
const inquirer = require('inquirer') //用于命令行与开发者交互
const rm = require('rimraf').sync // 相当于UNIX的“rm -rf”命令
const logger = require('../lib/logger') //自定义工具-用于日志打印
const generate = require('../lib/generate')  //自定义工具-用于基于模板构建项目
const checkVersion = require('../lib/check-version') //自定义工具-用于检测vue-cli版本的工具
const warnings = require('../lib/warnings') //自定义工具-用于模板的警告
const localPath = require('../lib/local-path') //自定义工具-用于路径的处理

const isLocalPath = localPath.isLocalPath  //判断是否是本地路径
const getTemplatePath = localPath.getTemplatePath  //获取本地模板的绝对路径

определена куча переменных

let template = program.args[0]  //模板名称
const hasSlash = template.indexOf('/') > -1   //是否有斜杠,后面将会用来判定是否为官方模板   
const rawName = program.args[1]  //项目构建目录名
const inPlace = !rawName || rawName === '.'  // 没写或者“.”,表示当前目录下构建项目
const name = inPlace ? path.relative('../', process.cwd()) : rawName  //如果在当前目录下构建项目,当前目录名为项目构建目录名,否则是当前目录下的子目录【rawName】为项目构建目录名
const to = path.resolve(rawName || '.') //项目构建目录的绝对路径
const clone = program.clone || false  //是否采用clone模式,提供给“download-git-repo”的参数

const tmp = path.join(home, '.vue-templates', template.replace(/[\/:]/g, '-'))  //远程模板下载到本地的路径

основная логика

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

Для приведенного выше кодаvue-cliбудет судить в помещении иexists(to),true спрашивает разработчика и выполняется, когда разработчик отвечает «да».запустить функцию, иначе выполнить напрямуюзапустить функцию. Здесь можно задать два вопроса разработчикам:

  • Generate project in current directory?//Сборить ли проект в текущем каталоге

  • Target directory exists. Continue?//Каталог сборки уже существует, нужно ли продолжать

Эти два вопроса зависят отпеременная на местеПриходите выбирать, позвольте мне взглянуть нижепеременная на местеКак ты получил это.

const rawName = program.args[1]  //rawName为命令行的第二个参数(项目构建目录的相对目录)
const inPlace = !rawName || rawName === '.'  //rawName存在或者为“.”的时候,视为在当前目录下构建

Из вышеприведенного описания видно, чтопеременная на местеИспользуется для определения необходимости сборки в текущем каталоге, поэтомупеременная на местеКогда это правда, это подскажетGenerate project in current directory?, наоборотпеременная на местеКогда ложно, в это времяexists(to)Должна быть правдой, это подскажетTarget directory exists. Continue?.

Функция запуска

логика:

image

Исходный код:

function run () {
  // check if template is local
  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
        // 从这句话以及download-git-repo的用法,我们得知了vue的官方的模板库的地址:https://github.com/vuejs-templates
        const officialTemplate = 'vuejs-templates/' + template
        if (template.indexOf('#') !== -1) {  //模板名是否带"#"
          downloadAndGenerate(officialTemplate) //下载模板
        } else {
          if (template.indexOf('-2.0') !== -1) { //是都带"-2.0"
             //发出警告
            warnings.v2SuffixTemplatesDeprecated(template, inPlace ? '' : name)
            return
          }

          // warnings.v2BranchIsNowDefault(template, inPlace ? '' : name)
          downloadAndGenerate(officialTemplate)//下载模板
        }
      } else {
        downloadAndGenerate(template)//下载模板
      }
    })
  }
}

функция downloadAndGenerate

function downloadAndGenerate (template) {
  const spinner = ora('downloading template')  
  spinner.start()//显示加载状态
  // Remove if local template exists
  if (exists(tmp)) rm(tmp)  //当前模板库是否存在该模板,存在就删除
   //下载模板  template-模板名    tmp- 模板路径   clone-是否采用git clone模板   err-错误短信
    
  download(template, tmp, { clone }, err => {
    spinner.stop() //隐藏加载状态
    //如果有错误,打印错误日志
    if (err) logger.fatal('Failed to download repo ' + template + ': ' + err.message.trim())
    //渲染模板
    generate(name, tmp, to, err => {
      if (err) logger.fatal(err)
      console.log()
      logger.success('Generated "%s".', name)
    })
  })
}

lib

сгенерировать.js (★)

Самый важный файл js под файлом lib — это самая важная часть нашего строительного проекта, и он рендерится в нужный нам проект по шаблону. Это то, на чем мы должны сосредоточиться.

const chalk = require('chalk')
const Metalsmith = require('metalsmith')
const Handlebars = require('handlebars')
const async = require('async')
const render = require('consolidate').handlebars.render
const path = require('path')
const multimatch = require('multimatch')
const getOptions = require('./options')
const ask = require('./ask')
const filter = require('./filter')
const logger = require('./logger')

// register handlebars helper  注册handlebars的helper
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)
})

/**
 * Generate a template given a `src` and `dest`.
 *
 * @param {String} name
 * @param {String} src
 * @param {String} dest
 * @param {Function} done
 */

module.exports = function generate (name, src, dest, done) {
  const opts = getOptions(name, src)  //获取配置
  const metalsmith = Metalsmith(path.join(src, 'template'))  //初始化Metalsmith对象
  const data = Object.assign(metalsmith.metadata(), {
    destDirName: name,
    inPlace: dest === process.cwd(),
    noEscape: true
  })//添加一些变量至metalsmith中,并获取metalsmith中全部变量
  
  //注册配置对象中的helper
  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)
  }

  metalsmith.use(askQuestions(opts.prompts))  //询问问题
    .use(filterFiles(opts.filters))  //过滤文件
    .use(renderTemplateFiles(opts.skipInterpolation)) //渲染模板文件


  //配置对象是否有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('.') // start from template root instead of `./src` which is Metalsmith's default for `source`
    .destination(dest)
    .build((err, files) => {
      done(err)
      if (typeof opts.complete === 'function') {
      //配置对象有complete函数则执行
        const helpers = { chalk, logger, files }
        opts.complete(data, helpers)
      } else {
      //配置对象有completeMessage,执行logMessage函数
        logMessage(opts.completeMessage, data)
      }
    })

  return data
}

/**
 * Create a middleware for asking questions.
 *
 * @param {Object} prompts
 * @return {Function}
 */

function askQuestions (prompts) {
  return (files, metalsmith, done) => {
    ask(prompts, metalsmith.metadata(), done)
  }
}

/**
 * Create a middleware for filtering files.
 *
 * @param {Object} filters
 * @return {Function}
 */

function filterFiles (filters) {
  return (files, metalsmith, done) => {
    filter(files, filters, metalsmith.metadata(), done)
  }
}

/**
 * Template in place plugin.
 *
 * @param {Object} files
 * @param {Metalsmith} metalsmith
 * @param {Function} done
 */

function renderTemplateFiles (skipInterpolation) {
  skipInterpolation = typeof skipInterpolation === 'string'
    ? [skipInterpolation]
    : skipInterpolation    //保证skipInterpolation是一个数组
  return (files, metalsmith, done) => {
    const keys = Object.keys(files) //获取files的所有key
    const metalsmithMetadata = metalsmith.metadata() //获取metalsmith的所有变量
    async.each(keys, (file, next) => { //异步处理所有files
      // skipping files with skipInterpolation option  
      // 跳过符合skipInterpolation的要求的file
      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
      //跳过不符合handlebars语法的file
      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)
  }
}

/**
 * Display template complete message.
 *
 * @param {String} message
 * @param {Object} data
 */

function logMessage (message, data) {
  if (!message) return  //没有message直接退出函数
  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'))
      //渲染成功打印最终渲染的结果
    }
  })
}

Импортированные пакеты:

  • chalk(Используется для выделения информации, распечатываемой терминалом.)
  • metalsmith(Генератор статических сайтов.)
  • handlebars(Известный шаблонизатор.)
  • async(Очень мощный инструмент для асинхронной обработки.)
  • consolidate(Поддерживает рендеринг для различных механизмов шаблонов.)
  • path(узел поставляется с модулем пути для обработки пути.)
  • multimatch(Может поддерживать сопоставление нескольких условий.)
  • options(Пользовательский инструмент — используется для получения конфигурации шаблона.)
  • ask(Пользовательский инструмент - используется, чтобы спросить разработчика.)
  • filter(Пользовательский инструмент — для фильтрации файлов.)
  • logger(Пользовательский инструмент — для печати журнала.)

Основная логика:

Получить конфигурацию шаблона -->Инициализировать Metalsmith -->Добавьте несколько переменных в Metalsmith -->помощник по регистрации шаблона руля -->Есть ли функция before в объекте конфигурации, если да, то выполнить ее -->задавать вопросы -->фильтровать файлы -->файл шаблона визуализации -->Есть ли функция after в объекте конфигурации, если да, то выполнить ее -->Наконец создайте контент проекта -->Построение завершено, если в конфигурационном объекте есть завершенная функция, она будет выполнена, в противном случае будет напечатана информация completeMessage в конфигурационном объекте, в случае ошибки будет выполнена callback-функция done(err) .

Другие функции:

  • askQuestions:задавать вопросы
  • filterFiles:фильтровать файлы
  • renderTemplateFiles:файл шаблона визуализации
  • logMessage:Используется для печати информации об успешной сборке.

Формат плагина металсина:

function <function name> {
  return (files,metalsmith,done)=>{
    //逻辑代码
    ...
  }
}

options.js

const path = require('path')
const metadata = require('read-metadata')
const exists = require('fs').existsSync
const getGitUser = require('./git-user')
const validateName = require('validate-npm-package-name')

/**
 * Read prompts metadata.
 *
 * @param {String} dir
 * @return {Object}
 */

module.exports = function options (name, dir) {
  const opts = getMetadata(dir)

  setDefault(opts, 'name', name)
  setValidateName(opts)

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

  return opts
}

/**
 * Gets the metadata from either a meta.json or meta.js file.
 *
 * @param  {String} dir
 * @return {Object}
 */

function getMetadata (dir) {
  const json = path.join(dir, 'meta.json')
  const js = path.join(dir, 'meta.js')
  let opts = {}

  if (exists(json)) {
    opts = metadata.sync(json)
  } else if (exists(js)) {
    const req = require(path.resolve(js))
    if (req !== Object(req)) {
      throw new Error('meta.js needs to expose an object')
    }
    opts = req
  }

  return opts
}

/**
 * Set the default value for a prompt question
 *
 * @param {Object} opts
 * @param {String} key
 * @param {String} val
 */

function setDefault (opts, key, val) {
  if (opts.schema) {
    opts.prompts = opts.schema
    delete opts.schema
  }
  const prompts = opts.prompts || (opts.prompts = {})
  if (!prompts[key] || typeof prompts[key] !== 'object') {
    prompts[key] = {
      'type': 'string',
      'default': val
    }
  } else {
    prompts[key]['default'] = val
  }
}

function setValidateName (opts) {
  const name = opts.prompts.name
  const customValidate = name.validate
  name.validate = name => {
    const its = validateName(name)
    if (!its.validForNewPackages) {
      const errors = (its.errors || []).concat(its.warnings || [])
      return 'Sorry, ' + errors.join(' and ') + '.'
    }
    if (typeof customValidate === 'function') return customValidate(name)
    return true
  }
}

Импортированные пакеты:

  • path(узел поставляется с модулем пути для обработки пути.)
  • read-metadata(Используется для чтения файлов метаданных json или yaml и возврата объекта.)
  • fs.existsSync(узел поставляется с методом existsSync модуля fs, чтобы определить, существует ли путь.)
  • git-user(Получите локальную конфигурацию git.)
  • validate-npm-package-name(Является ли имя, используемое для пакетов npm, законным.)

эффект:

  • Основной метод:Шаг 1: Получите информацию о файле конфигурации шаблона; Шаг 2: Установите поле имени и проверьте, является ли имя допустимым; Шаг 3: Только поле автора.
  • getMetadata:Получите информацию о конфигурации в meta.js или meta.json
  • setDefault:Используется для добавления следующих полей по умолчанию в объект конфигурации
  • setValidateName:Используется для проверки допустимости поля имени в объекте конфигурации.

git-user.js

const exec = require('child_process').execSync

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

Импортированные пакеты:

  • child_process.execSync(Метод execSync в собственном модуле узла child_process используется для открытия новой оболочки, выполнения соответствующей команды и возврата соответствующего вывода.)

эффект:Имя пользователя и адрес электронной почты, используемые для получения локальной конфигурации git и возврата форматаИмя Нить.

eval.js

const chalk = require('chalk')

/**
 * Evaluate an expression in meta.json in the context of
 * prompt answers data.
 */

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

Импортированные пакеты:

  • chalk(Используется для выделения информации, распечатываемой терминалом.)

эффект:Выполнить выражение exp в области данных и вернуть значение, полученное при его выполнении.

ask.js

const async = require('async')
const inquirer = require('inquirer')
const evaluate = require('./eval')

// Support types from prompt-for which was used before
const promptMapping = {
  string: 'input',
  boolean: 'confirm'
}

/**
 * Ask questions, return results.
 *
 * @param {Object} prompts
 * @param {Object} data
 * @param {Function} done
 */
 
/**
 * prompts meta.js或者meta.json中的prompts字段
 * data metalsmith.metadata()
 * done 交于下一个metalsmith插件处理
 */
module.exports = function ask (prompts, data, done) {
 //遍历处理prompts下的每一个字段
  async.eachSeries(Object.keys(prompts), (key, next) => {
    prompt(data, key, prompts[key], next)
  }, done)
}

/**
 * Inquirer prompt wrapper.
 *
 * @param {Object} data
 * @param {String} key
 * @param {Object} prompt
 * @param {Function} 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)
    }
  }
  //设置问题,具体使用方法可去https://github.com/SBoudrias/Inquirer.js上面查看
  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)
}

Импортированные пакеты:

  • async(Инструмент асинхронной обработки.)
  • inquirer(Взаимодействие между командной строкой и пользователем.)
  • eval(Возвращает значение выражения под действием.)

эффект:Разберите поле подсказок в meta.js или meta.json на соответствующий вопрос.

filter.js

const match = require('minimatch')
const evaluate = require('./eval')
/**
 * files 模板内的所有文件
 * filters meta.js或者meta.json的filters字段
 * data metalsmith.metadata()
 * done  交于下一个metalsmith插件处理
 */
module.exports = (files, filters, data, done) => {
  if (!filters) {
    //meta.js或者meta.json没有filters字段直接跳过交于下一个metalsmith插件处理
    return done()
  }
  //获取所有文件的名字
  const fileNames = Object.keys(files)
  //遍历meta.js或者meta.json没有filters下的所有字段
  Object.keys(filters).forEach(glob => {
    //遍历所有文件名
    fileNames.forEach(file => {
      //如果有文件名跟filters下的某一个字段匹配上
      if (match(file, glob, { dot: true })) {        
        const condition = filters[glob]
        if (!evaluate(condition, data)) {
          //如果metalsmith.metadata()下condition表达式不成立,删除该文件
          delete files[file]
        }
      }
    })
  })
  done()
}

Импортированные пакеты:

  • minimatch(Инструмент сопоставления символов.)
  • eval(Возвращает значение выражения под действием.)

эффект:Удалите некоторые ненужные файлы шаблонов в соответствии с metalsmith.metadata(), а metalsmith.metadata() в основном изменен в ask.js, то есть потребности пользователя получены в ask.js.

logger.js

const chalk = require('chalk')
const format = require('util').format

/**
 * Prefix.
 */

const prefix = '   vue-cli'
const sep = chalk.gray('·')

/**
 * Log a `message` to the console.
 *
 * @param {String} message
 */

exports.log = function (...args) {
  const msg = format.apply(format, args)
  console.log(chalk.white(prefix), sep, msg)
}

/**
 * Log an error `message` to the console and exit.
 *
 * @param {String} message
 */

exports.fatal = function (...args) {
  if (args[0] instanceof Error) args[0] = args[0].message.trim()
  const msg = format.apply(format, args)
  console.error(chalk.red(prefix), sep, msg)
  process.exit(1)
}

/**
 * Log a success `message` to the console and exit.
 *
 * @param {String} message
 */

exports.success = function (...args) {
  const msg = format.apply(format, args)
  console.log(chalk.white(prefix), sep, msg)
}

Импортированные пакеты:

  • chalk(Используется для выделения информации, распечатываемой терминалом.)
  • format(Метод формата в модуле util, который поставляется с узлом.)

эффект:logger.js в основном предоставляет три метода: журнал (обычный журнал), фатальный (журнал ошибок) и успех (журнал успеха). Каждый метод довольно прост, и я не хочу объяснять слишком много.

local-path.js

const path = require('path')

module.exports = {
  isLocalPath (templatePath) {
    return /^[./]|(^[a-zA-Z]:)/.test(templatePath)
  },

  getTemplatePath (templatePath) {
    return path.isAbsolute(templatePath)
      ? templatePath
      : path.normalize(path.join(process.cwd(), templatePath))
  }
}

Импортированные пакеты:

  • path(Инструмент обработки пути, который поставляется с узлом.)

эффект:

  • isLocalPath:UNIX (начиная с «.» или «/») WINDOWS (начиная с «C:»).
  • getTemplatePath:Независимо от того, является ли templatePath абсолютным путем, он возвращает templatePath, в противном случае он преобразуется в абсолютный путь и нормализуется.

check-version.js

const request = require('request')
const semver = require('semver')
const chalk = require('chalk')
const packageConfig = require('../package.json')

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

Импортированные пакеты:

  • request(инструмент http-запроса.)
  • semver(Инструмент обработки номера версии.)
  • chalk(Используется для выделения информации, распечатываемой терминалом.)

эффект:

  • Шаг 1. Проверьте номер версии локального узла, чтобы убедиться, что онpackage.jsonТребования к версии узла в файле, если она ниже версии узлаpackage.jsonЕсли требуется версия, указанная в файле, разработчик должен напрямую обновить версию своего узла. Вместо этого начните второй шаг.
  • Шаг 2: По запросуhttps://registry.npmjs.org/vue-cliполучитьvue-cliНомер последней версии , за которым следуетpackage.jsonсерединаversionПоля сравниваются.Если номер локальной версии меньше, чем номер последней версии, будет предложено обновить последнюю версию. Здесь следует отметить, что проверка номера версии здесь не влияет на последующий процесс, даже если локальныйvue-cliВерсия не актуальна и на сборку не влияет, просто подсказка.

warnings.js

const chalk = require('chalk')

module.exports = {
  v2SuffixTemplatesDeprecated (template, name) {
    const initCommand = 'vue init ' + template.replace('-2.0', '') + ' ' + name

    console.log(chalk.red('  This template is deprecated, as the original template now uses Vue 2.0 by default.'))
    console.log()
    console.log(chalk.yellow('  Please use this command instead: ') + chalk.green(initCommand))
    console.log()
  },
  v2BranchIsNowDefault (template, name) {
    const vue1InitCommand = 'vue init ' + template + '#1.0' + ' ' + name

    console.log(chalk.green('  This will install Vue 2.x version of the template.'))
    console.log()
    console.log(chalk.yellow('  For Vue 1.x use: ') + chalk.green(vue1InitCommand))
    console.log()
  }
}

Импортированные пакеты:

  • chalk(Используется для выделения информации, распечатываемой терминалом.)

эффект:

  • v2SuffixTemplatesDeprecated: Шаблон с "-2.0" устарел, а в официальном шаблоне по умолчанию используется 2.0. Нет необходимости использовать «-2.0», чтобы отличить vue1.0 от vue2.0.
  • v2BranchIsNowDefault:Этот метод находится вВ файле vue-initбыл закомментирован и больше не используется. Он использовался при переходе с vue1.0 на vue2.0, а сейчас стоит 2.0 по умолчанию, поэтому естественно не используется.

Суммировать

Из-за большого количества кода я не буду подробно останавливаться на многих кодах, для некоторых относительно простых или не очень важных файлов js я просто объясню их функцию. А вот ключевой js файл, я еще добавил на него много аннотаций. Среди них я лично считаю, что более важными документами являютсяvue-init,generate.js,options.js,ask.js,filter.js, эти пять файлов составляютvue-cliОсновной процесс сборки проекта, поэтому нам нужно уделить ему больше времени. Кроме того, в процессе чтения исходного кода мы должны понимать, как выглядит весь процесс построения, и у нас в сердце должен быть спектр. Прочитав сам весь vue-cli, я также запустил инструмент создания лесов в соответствии с процессом vue-cli, просто для справки и обучения. Адрес следующий:

https://github.com/ruichengping/asuna-cli

Наконец, я желаю вам всего наилучшего на дороге впереди! Если вам нравятся мои статьи, не забудьте подписаться на меня! В будущем будет опубликовано больше качественных статей, так что следите за обновлениями!