vue-cli源码分析(试探篇)

4,990 阅读4分钟

引言:近期在研究怎么通过node命令生成自己想要的文件夹结构,因为大佬说vue-cli的原理和这个很像,所以抽时间研究了一下,并分享自己的研究心得。希望我的初学经验对你有用。


1.vue init命令是怎么实现的

vue-cli可以通过命令vue init webpack my-project运行

拿到一个项目配置,首先看的是package.json

"bin": {
    "vue": "bin/vue",
    "vue-init": "bin/vue-init",
    "vue-list": "bin/vue-list",
    "vue-build": "bin/vue-build"
}

bin项用来指定各个内部命令对应的可执行文件的位置。所以,vue可以运行文件bin/vue,让我们看看bin/vue里面是什么?

// file: ./bin/vue
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)

于是,找到了commander,8000多star,github地址请点这里

正常来说,command的用法有三个参数,如下

program
   .command('aaa')
   .description('do something')
   //aaa命令调用是回调action函数
   .action(function(env) {
     console.log('deploying "%s"', env);
   });

而vue-cli里面并没有显示调用action(fn)时,则将会使用子命令模式。Commander 将尝试在入口脚本的目录中搜索可执行文件,(像./bin/vue)与名称 program-command,像 vue-init,vue-build。

.command('init')命令则会找到同文件夹的vue-init,所以会调用文件"bin/vue-init"并执行。

然后vue init就可以解释了,那么vue init webpack my-project 怎么执行?往下看

2.vue-init文件解析

头部模块
#!/usr/bin/env node

//从仓库下载并提取git存储库(GitHub,GitLab,Bitbucket)。
var download = require('download-git-repo')
//主要用于创建子命令和切割命令行参数并执行
var program = require('commander')
//检查文件是否存在
var exists = require('fs').existsSync
//路径模块提供用于处理文件和目录路径的实用程序。 比如路径分割,文件路径格式化,json格式化等
var path = require('path')
//漂亮的loding
var ora = require('ora')
//获取用户主目录的路径
var home = require('user-home')
//绝对路径转换为相对路径
var tildify = require('tildify')
//美化
var chalk = require('chalk')
//常用的交互式命令行用户界面的集合。表现是控制台输出提问
var inquirer = require('inquirer')
var rm = require('rimraf').sync
var logger = require('../lib/logger')
//输出信息
var generate = require('../lib/generate')
var checkVersion = require('../lib/check-version')
var warnings = require('../lib/warnings')
var localPath = require('../lib/local-path')

var isLocalPath = localPath.isLocalPath
var getTemplatePath = localPath.getTemplatePath
主体部分
//help部分太简单跳过
/***
    用法举例:
    1.path.relative('/data/orandea/test/aaa', '/data/orandea/impl/bbb');
    Returns: '../../impl/bbb'

    2.path.resolve('wwwroot', 'static_files/png/', '../gif/image.gif');
    如果当前路径是/home/myself/node,
    Returns: '/home/myself/node/wwwroot/static_files/gif/image.gif'

    3.path.join('/foo', 'bar', 'baz/asdf', 'quux', '..');
    Returns: '/foo/bar/baz/asdf'
*/

/**
 * 配置
 */
//假设是vue init webpack my-project,第一个参数是webpack
var template = program.args[0]
var hasSlash = template.indexOf('/') > -1
var rawName = program.args[1]
var inPlace = !rawName || rawName === '.'

//path.relative()方法根据当前工作目录返回相对路径。 
//如果从每个解析到相同的路径(在每个路径上调用path.resolve()之后),返回零长度的字符串。
var name = inPlace ? path.relative('../', process.cwd()) : rawName

//合并路径
var to = path.resolve(rawName || '.')
var clone = program.clone || false
//path.join()方法使用平台特定的分隔符作为分隔符将所有给定的路径段连接在一起,    
//然后对结果路径进行规范化。
//home输出举例 => /Users/admin, tmp => /Users/admin/.vue-templates/webpack
var tmp = path.join(home, '.vue-templates', template.replace(/\//g, '-'))
//如果是线下,则template直接取这个路径,否则需要去线上仓库下载
if (program.offline) {
  console.log(`> Use cached template at ${chalk.yellow(tildify(tmp))}`)
  template = tmp
}


/**
 * Padding.
 */

console.log()
process.on('exit', function () {
  console.log()
})

if (exists(to)) {
    //询问选择
  inquirer.prompt([{
    type: 'confirm',
    message: inPlace
      ? 'Generate project in current directory?'
      : 'Target directory exists. Continue?',
    name: 'ok'
  }], function (answers) {
    if (answers.ok) {
      run()
    }
  })
} else {
  run()
}

讲run函数之前,要先了解下generate命令的调用文件--lib/generate文件,里面用到了metalsmith,github地址请点这里

/**
 * Check, download and generate the project.
 */

function run () {
  // 检查要下载的模版是否是本地路径
  if (isLocalPath(template)) {
    var templatePath = getTemplatePath(template)
    //信息输出
    if (exists(templatePath)) {
      generate(name, templatePath, to, function (err) {
        if (err) logger.fatal(err)
        console.log()
        logger.success('Generated "%s".', name)
      })
    } else {
      logger.fatal('Local template "%s" not found.', template)
    }
  } else {
      //检查版本并输出信息
    checkVersion(function () {
      if (!hasSlash) {
        // use official templates
        var 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)
      }
    })
  }
}

/**
 * 从模版仓库下载模版
 *
 * @param {String} template
 */

function downloadAndGenerate (template) {
  //启动控制台loading
  var spinner = ora('downloading template')
  spinner.start()
  // 如果存在本地模版则移除
  if (exists(tmp)) rm(tmp)
  //下载
  download(template, tmp, { clone: clone }, function (err) {
    spinner.stop()
    //日志
    if (err) logger.fatal('Failed to download repo ' + template + ': ' + err.message.trim())
    //控制台打印出模版信息,比如 Generated "my-project".
    generate(name, tmp, to, function (err) {
      if (err) logger.fatal(err)
      console.log()
      logger.success('Generated "%s".', name)
    })
  })
}

3.总结

vue init webpack my-project 的执行大致如下:

1.通过program.argv拿到参数[webpack,my-project]

2.根据参数通过拼接、path处理等操作,拿到下载模版路径,然后根据本地和在线的区别,做不同的操作去下载即可。

以上有说得不对的地方,还请大家多多指教,共同进步。

参考资料:

【1】html-js.site/2017/05/26/…