走進(jìn)Vue-cli源碼腮敌,搭建屬于你的CLI——Chapter 1

導(dǎo)語(yǔ):這兩天搭建UIBot前端框架阱当,突發(fā)奇想到可以搭建一套屬于自己的構(gòu)建工具。鉆研了幾天Vue-cli 源碼糜工,成功搭建出 bot-cli 構(gòu)建工具弊添。

介紹:什么是前端腳手架?

在不使用腳手架進(jìn)行創(chuàng)建項(xiàng)目的過(guò)程中捌木,往往得有這樣一堆操作:

  1. 選擇包管理工具油坝,初始化 package.json 文件
  2. 查找項(xiàng)目引用的框架依賴和工具依賴包,并逐一安裝相關(guān)的包
  3. 針對(duì)已安裝的包分別寫對(duì)應(yīng)的配置
  4. 配置本地 web server刨裆,代理靜態(tài)資源文件

這樣還沒(méi)完澈圈,在項(xiàng)目配置測(cè)試環(huán)境,還得安裝單元測(cè)試相關(guān)依賴帆啃,寫對(duì)應(yīng)的配置和運(yùn)行腳本瞬女;發(fā)布到生產(chǎn)環(huán)境時(shí),得進(jìn)行代碼合并努潘、壓縮诽偷、混淆、規(guī)范化疯坤。這對(duì)于不經(jīng)常寫配置的開(kāi)發(fā)者是十分不友好的报慕。基于這個(gè)痛點(diǎn)压怠,我們可以引入腳手架工具來(lái)解決眠冈。

  • 腳手架工具執(zhí)行邏輯:


    cli.png

一、Vue-cli 源碼分析

Vue-cli是一款非常優(yōu)秀的用于迅速構(gòu)建基于Vue的Web應(yīng)用工具菌瘫,通過(guò)簡(jiǎn)單的幾個(gè)命令就能夠生成一個(gè)模板項(xiàng)目洋闽,極大地降低了開(kāi)發(fā)人員工作量提高工作效率。此次源碼分析我采用的是vue-cli v2.9.6 的代碼突梦,原因是vue-cli 3.0之后的代碼量增大且結(jié)構(gòu)不再那么清晰,理解起來(lái)不夠容易羽利。

第一部分:vue-cli 目錄結(jié)構(gòu)

  • 部分非重要代碼沒(méi)有展示出來(lái)宫患,僅分析核心代碼目錄結(jié)構(gòu):
│    package.json         -- 項(xiàng)目配置文件
├─bin                             -- 二進(jìn)制命令目錄
│      vue                        
│      vue-build
│      vue-create
│      vue-init
│      vue-list
├─docs                        -- 相關(guān)docs文檔目錄
├─lib                           -- 核心邏輯代碼目錄
│      ask.js
│      check-version.js
│      eval.js
│      filter.js
│      generate.js
│      git-user.js
│      local-path.js
│      logger.js
│      options.js
│      warnings.js
├─node_modules            -- 中間件依賴
└─test                          -- 測(cè)試代碼目錄
  • 目錄簡(jiǎn)要介紹:
  1. bin:這里放的vue的一些命令文件,比如vue init這樣的命令都是從由這里控制的这弧;
  2. docs:一些注意事項(xiàng)啥的娃闲,不重要的目錄虚汛,可以直接忽略;
  3. lib:這里存放著一些vue-cli需要的一些自定義方法皇帮;
  4. node_modules:第三方依賴卷哩;
  5. test:?jiǎn)卧獪y(cè)試,開(kāi)發(fā)vue-cli工具時(shí)會(huì)用到属拾,我們讀源碼的時(shí)候可以直接忽略掉将谊;
  6. 一些雜七雜八的東西,比如eslint配置渐白、.gitignore尊浓、LICENSE等等諸如此類這些東西,不影響我們閱讀源碼纯衍,可以直接忽略掉栋齿;
  7. package.json/README.md

接下來(lái),我將從bin目錄開(kāi)始襟诸,逐層分解vue-cli源碼瓦堵。

第二部分:vue-cli 命令分解

在開(kāi)始閱讀源碼之前,首先要介紹一個(gè)工具(commander)歌亲,這是用來(lái)處理命令行的工具菇用,具體的使用方法可查看github的README

vue-cli采用了commander的Git風(fēng)格的寫法应结,vue文件處理vue命令刨疼,vue-init處理vue init命令以此類推。

vue
  • 引入的包:commander——用于處理命令行
  • 這個(gè)文件代碼很少鹅龄,直接貼出來(lái):
#!/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')
program.parse(process.argv)

這個(gè)文件主要是在用戶輸入“vue”時(shí)揩慕,終端上顯示參數(shù)的使用說(shuō)明。

vue build
  • 引入的包:chalk_用于高亮終端打印出來(lái)的信息
  • vue build命令在vue-cli之中已經(jīng)刪除了扮休,源碼上做了一定的說(shuō)明迎卤。代碼不多,我就直接貼出來(lái):
#!/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'
))

這個(gè)文件主要是在用戶輸入“vue”時(shí)玷坠,終端上顯示參數(shù)的使用說(shuō)明蜗搔。

vue list
  • 引入的包:
  1. request——發(fā)送http請(qǐng)求的工具;
  2. chalk——用于高亮console.log打印出來(lái)的信息八堡;
  3. logger——自定義工具樟凄,用于日志打印兄渺;
#!/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)
    }
})
  • 作用:
    當(dāng)輸入"vue list"時(shí)(我們測(cè)試時(shí)缝龄,可以直接在當(dāng)前源碼文件目錄下的終端上輸入“bin/vue-list”),vue-cli會(huì)請(qǐng)求接口,獲取官方模板的信息叔壤,然后做了一定處理瞎饲,在終端上顯示出來(lái)模板名稱和對(duì)應(yīng)的說(shuō)明。

  • 效果如下:

  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”是用來(lái)構(gòu)建項(xiàng)目的命令炼绘,也是vue-cli的核心文件嗅战,上面的三個(gè)都是非常簡(jiǎn)單的命令,算是我們閱讀源碼的開(kāi)胃菜俺亮,真正的大餐在這里驮捍。工作流程在講代碼之前,首先我們要講一下整個(gè)vue-cli初始項(xiàng)目的流程铅辞,然后我們沿著流程一步一步走下去厌漂。


vue-cli-logic.jpg
  • 整個(gè)vue init大致流程如我上圖所示,應(yīng)該還是比較好理解的斟珊。這里我大致闡述一下大致的流程苇倡。
  1. vue-cli會(huì)先判斷你的模板在遠(yuǎn)程github倉(cāng)庫(kù)上還是在你的本地某個(gè)文件里面,若是本地文件夾則會(huì)立即跳到第3步囤踩,反之則走第2步。
  2. 第2步會(huì)判斷是否為官方模板堵漱,官方模板則會(huì)從官方github倉(cāng)庫(kù)中下載模板到本地的默認(rèn)倉(cāng)庫(kù)下示惊,即根目錄下.vue-templates文件夾下丈探。
  3. 第3步則讀取模板目錄下meta.js或者meta.json文件碗降,根據(jù)里面的內(nèi)容會(huì)詢問(wèn)開(kāi)發(fā)者隘竭,根據(jù)開(kāi)發(fā)者的回答,確定一些修改讼渊。
  4. 根據(jù)模板內(nèi)容以及開(kāi)發(fā)者的回答动看,渲染出項(xiàng)目結(jié)構(gòu)并生成到指定目錄。
  • 源碼內(nèi)容:這里vue-init文件的代碼比較多爪幻,我這里就拆分幾塊來(lái)看弧圆。首先我先把整個(gè)文件的結(jié)構(gòu)列出來(lái)赋兵,方便后續(xù)的閱讀。
/**
   * 引入一大堆包
   */
    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() //如果沒(méi)有輸入?yún)?shù)搔预,終端顯示幫助
    }
    help()
    
    /**
     * 定義一大堆變量
     */
     
     let template = program.args[0]
     ...
     
     /**
      * 判斷是否輸入項(xiàng)目名  是 - 直接執(zhí)行run函數(shù)  否- 詢問(wèn)開(kāi)發(fā)者是否在當(dāng)前目錄下生成項(xiàng)目,開(kāi)發(fā)者回答“是” 也執(zhí)行run函數(shù) 否則不執(zhí)行run函數(shù)
      */
     
     /**
     * 定義主函數(shù) run
     */
     function run (){
             ...
     }
     
     /**
      * 定義下載模板并生產(chǎn)項(xiàng)目的函數(shù) downloadAndGenerate
      */
      function downloadAndGenerate(){
              ...
      }

整個(gè)文件大致的東西入上面所示叶组,后面我們將一塊一塊內(nèi)容來(lái)看:

引入一堆包:
const download = require('download-git-repo')       //用于下載遠(yuǎn)程倉(cāng)庫(kù)至本地 支持GitHub拯田、GitLab、Bitbucket
const program = require('commander')        //命令行處理工具
const exists = require('fs').existsSync         //node自帶的fs模塊下的existsSync方法甩十,用于檢測(cè)路徑是否存在船庇。(會(huì)阻塞)
const path = require('path')            //node自帶的path模塊,用于拼接路徑
const ora = require('ora')              //用于命令行上的加載效果
const home = require('user-home')           //用于獲取用戶的根目錄
const tildify = require('tildify')           //將絕對(duì)路徑轉(zhuǎn)換成帶波浪符的路徑
const chalk = require('chalk')          // 用于高亮終端打印出的信息
const inquirer = require('inquirer')            //用于命令行與開(kāi)發(fā)者交互
const rm = require('rimraf').sync           // 相當(dāng)于UNIX的“rm -rf”命令
const logger = require('../lib/logger')             //自定義工具-用于日志打印
const generate = require('../lib/generate')             //自定義工具-用于基于模板構(gòu)建項(xiàng)目
const checkVersion = require('../lib/check-version')            //自定義工具-用于檢測(cè)vue-cli版本的工具
const warnings = require('../lib/warnings')             //自定義工具-用于模板的警告
const localPath = require('../lib/local-path')          //自定義工具-用于路徑的處理

const isLocalPath = localPath.isLocalPath           //判斷是否是本地路徑
const getTemplatePath = localPath.getTemplatePath                //獲取本地模板的絕對(duì)路徑
定義一堆變量:
let template = program.args[0]  //模板名稱
const hasSlash = template.indexOf('/') > -1   //是否有斜杠侣监,后面將會(huì)用來(lái)判定是否為官方模板   
const rawName = program.args[1]  //項(xiàng)目構(gòu)建目錄名
const inPlace = !rawName || rawName === '.'  // 沒(méi)寫或者“.”鸭轮,表示當(dāng)前目錄下構(gòu)建項(xiàng)目
const name = inPlace ? path.relative('../', process.cwd()) : rawName  //如果在當(dāng)前目錄下構(gòu)建項(xiàng)目,當(dāng)前目錄名為項(xiàng)目構(gòu)建目錄名,否則是當(dāng)前目錄下的子目錄【rawName】為項(xiàng)目構(gòu)建目錄名
const to = path.resolve(rawName || '.') //項(xiàng)目構(gòu)建目錄的絕對(duì)路徑
const clone = program.clone || false  //是否采用clone模式橄霉,提供給“download-git-repo”的參數(shù)

const tmp = path.join(home, '.vue-templates', template.replace(/[\/:]/g, '-'))  //遠(yuǎn)程模板下載到本地的路徑

主邏輯:
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()
}

對(duì)著上面代碼窃爷,vue-cli會(huì)判斷inPlace和exists(to),結(jié)果為true則詢問(wèn)開(kāi)發(fā)者,當(dāng)開(kāi)發(fā)者回答“yes”的時(shí)候執(zhí)行run函數(shù)姓蜂,否則直接執(zhí)行run函數(shù)按厘。這里詢問(wèn)開(kāi)發(fā)者的問(wèn)題有如下兩個(gè):

  • Generate project in current directory? //是否在當(dāng)前目錄下構(gòu)建項(xiàng)目?
  • Target directory exists. Continue? //構(gòu)建目錄已存在,是否繼續(xù)钱慢?

這兩個(gè)問(wèn)題依靠變量inPlace來(lái)選擇逮京,下面我看一下變量inPlace是怎么得來(lái)的。

const rawName = program.args[1]  //rawName為命令行的第二個(gè)參數(shù)(項(xiàng)目構(gòu)建目錄的相對(duì)目錄)
const inPlace = !rawName || rawName === '.'  //rawName存在或者為“.”的時(shí)候束莫,視為在當(dāng)前目錄下構(gòu)建
Run函數(shù)

邏輯:

vue-cli-run.jpg

源碼:

function run () {
  // check if template is local
  if (isLocalPath(template)) {    //是否是本地模板
    const templatePath = getTemplatePath(template)  //獲取絕對(duì)路徑
    if (exists(templatePath)) {  //判斷模板所在路徑是否存在
       //渲染模板
      generate(name, templatePath, to, err => {
        if (err) logger.fatal(err)
        console.log()
        logger.success('Generated "%s".', name)
      })
    } else {
       //打印錯(cuò)誤日志懒棉,提示本地模板不存在
      logger.fatal('Local template "%s" not found.', template)
    }
  } else {
    checkVersion(() => {  //檢查版本號(hào)
      if (!hasSlash) {  //官方模板還是第三方模板
        // use official templates
        // 從這句話以及download-git-repo的用法,我們得知了vue的官方的模板庫(kù)的地址: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"
             //發(fā)出警告
            warnings.v2SuffixTemplatesDeprecated(template, inPlace ? '' : name)
            return
          }

          // warnings.v2BranchIsNowDefault(template, inPlace ? '' : name)
          downloadAndGenerate(officialTemplate)//下載模板
        }
      } else {
        downloadAndGenerate(template)//下載模板
      }
    })
  }
}
downloadAndGenerate函數(shù)
function downloadAndGenerate (template) {
  const spinner = ora('downloading template')  
  spinner.start()//顯示加載狀態(tài)
  // Remove if local template exists
  if (exists(tmp)) rm(tmp)  //當(dāng)前模板庫(kù)是否存在該模板览绿,存在就刪除
   //下載模板  template-模板名    tmp- 模板路徑   clone-是否采用git clone模板   err-錯(cuò)誤短信
    
  download(template, tmp, { clone }, err => {
    spinner.stop() //隱藏加載狀態(tài)
    //如果有錯(cuò)誤策严,打印錯(cuò)誤日志
    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目錄源碼分析

generate.js (★)

lib文件下最重要的js文件,他是我們構(gòu)建項(xiàng)目中最重要的一環(huán)挟裂,根據(jù)模板渲染成我們需要的項(xiàng)目享钞。這塊內(nèi)容是需要我們重點(diǎn)關(guān)注的。

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  注冊(cè)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對(duì)象
  const data = Object.assign(metalsmith.metadata(), {
    destDirName: name,
    inPlace: dest === process.cwd(),
    noEscape: true
  })//添加一些變量至metalsmith中诀蓉,并獲取metalsmith中全部變量
  
  //注冊(cè)配置對(duì)象中的helper
  opts.helpers && Object.keys(opts.helpers).map(key => {
    Handlebars.registerHelper(key, opts.helpers[key])
  })

  const helpers = { chalk, logger }

 //配置對(duì)象是否有before函數(shù)栗竖,是則執(zhí)行
  if (opts.metalsmith && typeof opts.metalsmith.before === 'function') {
    opts.metalsmith.before(metalsmith, opts, helpers)
  }

  metalsmith.use(askQuestions(opts.prompts))  //詢問(wèn)問(wèn)題
    .use(filterFiles(opts.filters))  //過(guò)濾文件
    .use(renderTemplateFiles(opts.skipInterpolation)) //渲染模板文件


  //配置對(duì)象是否有after函數(shù),是則執(zhí)行
  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') {
      //配置對(duì)象有complete函數(shù)則執(zhí)行
        const helpers = { chalk, logger, files }
        opts.complete(data, helpers)
      } else {
      //配置對(duì)象有completeMessage渠啤,執(zhí)行l(wèi)ogMessage函數(shù)
        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是一個(gè)數(shù)組
  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  
      // 跳過(guò)符合skipInterpolation的要求的file
      if (skipInterpolation && multimatch([file], skipInterpolation, { dot: true }).length) {
        return next()
      }
      //獲取文件的文本內(nèi)容
      const str = files[file].contents.toString()
      // do not attempt to render files that do not have mustaches
      //跳過(guò)不符合handlebars語(yǔ)法的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  //沒(méi)有message直接退出函數(shù)
  render(message, data, (err, res) => {
    if (err) {
      console.error('\n   Error when rendering template complete message: ' + err.message.trim())  //渲染錯(cuò)誤打印錯(cuò)誤信息
    } else {
      console.log('\n' + res.split(/\r?\n/g).map(line => '   ' + line).join('\n'))
      //渲染成功打印最終渲染的結(jié)果
    }
  })
}

引入的包:

  • chalk (用于高亮終端打印出來(lái)的信息狐肢。)
  • metalsmith (靜態(tài)網(wǎng)站生成器。)
  • handlebars (知名的模板引擎沥曹。)
  • async (非常強(qiáng)大的異步處理工具份名。)
  • consolidate (支持各種模板引擎的渲染碟联。)
  • path (node自帶path模塊,用于路徑的處理僵腺。)
  • multimatch ( 可以支持多個(gè)條件的匹配渺绒。)
  • options (自定義工具-用于獲取模板配置。)
  • ask (自定義工具-用于詢問(wèn)開(kāi)發(fā)者沫屡。)
  • filter (自定義工具-用于文件過(guò)濾责循。)
  • logger (自定義工具-用于日志打印。)

主邏輯:

獲取模板配置 -->初始化Metalsmith -->添加一些變量至Metalsmith -->handlebars模板注冊(cè)helper -->配置對(duì)象中是否有before函數(shù)琉兜,有則執(zhí)行 -->詢問(wèn)問(wèn)題 -->過(guò)濾文件 -->渲染模板文件 -->配置對(duì)象中是否有after函數(shù)凯正,有則執(zhí)行 -->最后構(gòu)建項(xiàng)目?jī)?nèi)容 -->構(gòu)建完成,成功若配置對(duì)象中有complete函數(shù)則執(zhí)行豌蟋,否則打印配置對(duì)象中的completeMessage信息廊散,如果有錯(cuò)誤,執(zhí)行回調(diào)函數(shù)done(err)

其他函數(shù):

  • askQuestions: 詢問(wèn)問(wèn)題
  • filterFiles: 過(guò)濾文件
  • renderTemplateFiles: 渲染模板文件
  • logMessage: 用于構(gòu)建成功時(shí)梧疲,打印信息

Metalsmith插件格式:

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 (node自帶path模塊允睹,用于路徑的處理。)
  • read-metadata (用于讀取json或者yaml元數(shù)據(jù)文件并返回一個(gè)對(duì)象往声。)
  • fs.existsSync (node自帶fs模塊的existsSync方法擂找,用于檢測(cè)路徑是否存在。)
  • git-user (獲取本地的git配置浩销。)
  • validate-npm-package-name (用于npm包的名字是否是合法的贯涎。)

作用:

  • 主方法: 第一步:先獲取模板的配置文件信息;第二步:設(shè)置name字段并檢測(cè)name是否合法慢洋;第三步:只是author字段塘雳。
  • getMetadata: 獲取meta.js或則meta.json中的配置信息
  • setDefault: 用于向配置對(duì)象中添加一下默認(rèn)字段
  • setValidateName: 用于檢測(cè)配置對(duì)象中name字段是否合法
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 (node自帶模塊child_process中的execSync方法用于新開(kāi)一個(gè)shell并執(zhí)行相應(yīng)的command,并返回相應(yīng)的輸出普筹。)
  • 作用: 用于獲取本地的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 (用于高亮終端打印出來(lái)的信息。)
    作用: 在data的作用域執(zhí)行exp表達(dá)式并返回其執(zhí)行得到的值
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 交于下一個(gè)metalsmith插件處理
 */module.exports = function ask (prompts, data, done) {
 //遍歷處理prompts下的每一個(gè)字段
  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()
  }

  //獲取默認(rèn)值
  let promptDefault = prompt.default
  if (typeof prompt.default === 'function') {
    promptDefault = function () {
      return prompt.default.bind(this)(data)
    }
  }
  //設(shè)置問(wèn)題太防,具體使用方法可去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])) { 
      //當(dāng)答案是一個(gè)數(shù)組時(shí)
      data[key] = {}
      answers[key].forEach(multiChoiceAnswer => {
        data[key][multiChoiceAnswer] = true
      })
    } else if (typeof answers[key] === 'string') {
     //當(dāng)答案是一個(gè)字符串時(shí)
      data[key] = answers[key].replace(/"/g, '\\"')
    } else {
     //其他情況
      data[key] = answers[key]
    }
    done()
  }).catch(done)
}

引入的包:

  • async (異步處理工具妻顶。)
  • inquirer (命令行與用戶之間的交互。)
  • eval (返回某作用下表達(dá)式的值蜒车。)
    作用: 將meta.js或者meta.json中的prompts字段解析成對(duì)應(yīng)的問(wèn)題詢問(wèn)讳嘱。
filter.js
const match = require('minimatch')
const evaluate = require('./eval')
/**
 * files 模板內(nèi)的所有文件
 * filters meta.js或者meta.json的filters字段
 * data metalsmith.metadata()
 * done  交于下一個(gè)metalsmith插件處理
 */
 module.exports = (files, filters, data, done) => {
  if (!filters) {
    //meta.js或者meta.json沒(méi)有filters字段直接跳過(guò)交于下一個(gè)metalsmith插件處理
    return done()
  }
  //獲取所有文件的名字
  const fileNames = Object.keys(files)
  //遍歷meta.js或者meta.json沒(méi)有filters下的所有字段
  Object.keys(filters).forEach(glob => {
    //遍歷所有文件名
    fileNames.forEach(file => {
      //如果有文件名跟filters下的某一個(gè)字段匹配上
      if (match(file, glob, { dot: true })) {        
        const condition = filters[glob]
        if (!evaluate(condition, data)) {
          //如果metalsmith.metadata()下condition表達(dá)式不成立,刪除該文件
          delete files[file]
        }
      }
    })
  })
  done()
}

引入的包:

  • minimatch (字符匹配工具酿愧。)
  • eval (返回某作用下表達(dá)式的值沥潭。)
    作用: 根據(jù)metalsmith.metadata()刪除一些不需要的模板文件,而metalsmith.metadata()主要在ask.js中改變的嬉挡,也就是說(shuō)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 (用于高亮終端打印出來(lái)的信息汇恤。)
  • format (node自帶的util模塊中的format方法。)
    作用: logger.js主要提供三個(gè)方法log(常規(guī)日志)拔恰、fatal(錯(cuò)誤日志)因谎、success(成功日志)。每個(gè)方法都挺簡(jiǎn)單的仁连,我就不錯(cuò)過(guò)多的解釋了蓝角。
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 (node自帶的路徑處理工具。)

作用:

  • isLocalPath: UNIX (以“.”或者"/"開(kāi)頭) WINDOWS(以形如:“C:”的方式開(kāi)頭)饭冬。
  • getTemplatePath: templatePath是否為絕對(duì)路徑,是則返回templatePath 否則轉(zhuǎn)換成絕對(duì)路徑并規(guī)范化揪阶。
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請(qǐng)求工具昌抠。)
  • semver (版本號(hào)處理工具。)
  • chalk (用于高亮終端打印出來(lái)的信息鲁僚。)

作用:

  • 第一步:檢查本地的node版本號(hào)炊苫,是否達(dá)到package.json文件中對(duì)node版本的要求,若低于nodepackage.json文件中要求的版本冰沙,則直接要求開(kāi)發(fā)者更新自己的node版本侨艾。反之,可開(kāi)始第二步拓挥。
  • 第二步: 通過(guò)請(qǐng)求https://registry.npmjs.org/vue-cli來(lái)獲取vue-cli的最新版本號(hào)唠梨,跟package.json中的version字段進(jìn)行比較,若本地的版本號(hào)小于最新的版本號(hào)侥啤,則提示有最新版本可以更新当叭。這里需要注意的是,這里檢查版本號(hào)并不影響后續(xù)的流程盖灸,即便本地的vue-cli版本不是最新的蚁鳖,也不影響構(gòu)建,僅僅提示一下赁炎。
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 (用于高亮終端打印出來(lái)的信息醉箕。)

作用:

  • v2SuffixTemplatesDeprecated:提示帶“-2.0”的模板已經(jīng)棄用了,官方模板默認(rèn)用2.0了徙垫。不需要用“-2.0”來(lái)區(qū)分vue1.0和vue2.0了讥裤。
  • v2BranchIsNowDefault: 這個(gè)方法在vue-init文件中已經(jīng)被注釋掉,不再使用了松邪。在vue1.0向vue2.0過(guò)渡的時(shí)候用到過(guò)坞琴,現(xiàn)在都是默認(rèn)2.0了,自然也就不用了逗抑。

源碼總結(jié)

由于代碼比較多剧辐,很多代碼我就沒(méi)有一一細(xì)講了寒亥,一些比較簡(jiǎn)單或者不是很重要的js文件,我就單單說(shuō)明了它的作用了荧关。但是重點(diǎn)的js文件溉奕,我還是加了很多注解在上面。其中我個(gè)人認(rèn)為比較重點(diǎn)的文件就是vue-init忍啤、generate.js加勤、options.js、ask.js同波、filter.js,這五個(gè)文件構(gòu)成了vue-cli構(gòu)建項(xiàng)目的主流程鳄梅,因此需要我們花更多的時(shí)間在上面。另外未檩,我們?cè)谧x源碼的過(guò)程中戴尸,一定要理清楚整個(gè)構(gòu)建流程是什么樣子的,心里得有一個(gè)譜冤狡。

二孙蒙、如何簡(jiǎn)易搭建屬于你的CLI

本篇篇幅過(guò)長(zhǎng),呱聊的bot-cli具體實(shí)現(xiàn)內(nèi)容請(qǐng)關(guān)注下一節(jié)《走進(jìn)Vue-cli源碼悲雳,搭建屬于你的CLI——Chapter 2》

部分圖片挎峦、文字摘取自:

  • 簡(jiǎn)書:《憤怒的企鵝——走進(jìn)Vue-cli源碼,自己動(dòng)手搭建前端腳手架工具》
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末合瓢,一起剝皮案震驚了整個(gè)濱河市坦胶,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌歪玲,老刑警劉巖迁央,帶你破解...
    沈念sama閱讀 206,311評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異滥崩,居然都是意外死亡岖圈,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門钙皮,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)蜂科,“玉大人,你說(shuō)我怎么就攤上這事短条〉枷唬” “怎么了?”我有些...
    開(kāi)封第一講書人閱讀 152,671評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵茸时,是天一觀的道長(zhǎng)贡定。 經(jīng)常有香客問(wèn)我,道長(zhǎng)可都,這世上最難降的妖魔是什么缓待? 我笑而不...
    開(kāi)封第一講書人閱讀 55,252評(píng)論 1 279
  • 正文 為了忘掉前任蚓耽,我火速辦了婚禮,結(jié)果婚禮上旋炒,老公的妹妹穿的比我還像新娘步悠。我一直安慰自己,他們只是感情好瘫镇,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布鼎兽。 她就那樣靜靜地躺著,像睡著了一般铣除。 火紅的嫁衣襯著肌膚如雪谚咬。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書人閱讀 49,031評(píng)論 1 285
  • 那天尚粘,我揣著相機(jī)與錄音序宦,去河邊找鬼。 笑死背苦,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的潘明。 我是一名探鬼主播行剂,決...
    沈念sama閱讀 38,340評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼钳降!你這毒婦竟也來(lái)了厚宰?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書人閱讀 36,973評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤遂填,失蹤者是張志新(化名)和其女友劉穎铲觉,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體吓坚,經(jīng)...
    沈念sama閱讀 43,466評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡撵幽,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了礁击。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片盐杂。...
    茶點(diǎn)故事閱讀 38,039評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖哆窿,靈堂內(nèi)的尸體忽然破棺而出链烈,到底是詐尸還是另有隱情,我是刑警寧澤挚躯,帶...
    沈念sama閱讀 33,701評(píng)論 4 323
  • 正文 年R本政府宣布强衡,位于F島的核電站,受9級(jí)特大地震影響码荔,放射性物質(zhì)發(fā)生泄漏漩勤。R本人自食惡果不足惜感挥,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望锯七。 院中可真熱鬧链快,春花似錦、人聲如沸眉尸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 30,259評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)噪猾。三九已至霉祸,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間袱蜡,已是汗流浹背丝蹭。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留坪蚁,地道東北人奔穿。 一個(gè)月前我還...
    沈念sama閱讀 45,497評(píng)論 2 354
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像敏晤,于是被迫代替她去往敵國(guó)和親贱田。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容