vuecli2源码解析,你学会了吗?

一、vue-cli使用和源码下载

npm安装vue-cli:

npm install -g vue-cli   // 现在版本已经到了5了,如果需要下载2版本的需要指定版本了

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

vue init webpack <project-name>

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

源码下载:

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

vue-cli源码项目结构:

整个项目的目录结构如上图所示,下面我大概介绍每个文件夹的东西大致都是干嘛的。

  • bin //这里放的vue的一些命令文件,比如vue init这样的命令都是从由这里控制的
  • docs //一些注意事项啥的,不重要的目录,可以直接忽略
  • lib //这里存放着一些vue-cli需要的一些自定义方法
  • node_modules //这里就不用我多说了
  • test // 单元测试 开会vue-cli工具时会用到,我们读源码的时候可以直接忽略掉
  • 一些杂七杂八的东西 //比如eslint配置、.gitignore、LICENSE等等诸如此类这些东西。不影响阅读源码,直接忽略掉。
  • package.json/README.md //这个也不用我多说了,大家都知道的

二、vue-cli命令分析

本地相同时运行vuecli2和vuecli3命令,可以参考如下进行配置:https://blog.csdn.net/weixin_41346436/article/details/121966844

查看package.json文件:

图片[1]-vuecli2源码解析,你学会了吗?-烟雨网bin指定命令对应的可执行文件位置
在vue-cli 2.X版本中,vue-build,vue-create不支持

1. vue命令:

#!/usr/bin/env node

/**
 * 指明用node执行该脚本文件,系统去env环境找node的安装路径
 * #! -----在Linux或者Unix中,是一个符号名称,用于指明这个脚本文件的解释程序,即用什么来执行这个脚本文件。
 * /usr/bin/env:告诉系统可以在env设置里面查找该解释器的路径。如果出现No such file or directory的错误,则添加node环境变量配置就可以了
 * 注意:windows不支持Shebang,它是通过文件的扩展名来确定使用什么解释器来执行脚本
 */
// commander是Node.js命令行的解决方案(即Node.js下实现自定义命令)
const program = require('commander')

program
  .version(require('../package').version)    // version:定义版本----会添加到Options
  .usage('<command> [options]')   // usage:帮助信息输出,不写.name('xx')则Usage的名字当前文件的名字,否则为.name('xxxx')的xxxx
  .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)')

// .parse的第一个参数是要解析的字符串数组,也可以省略参数而使用process.argv。
program.parse(process.argv)   // parse:解析参数用于匹配对应选项或命令

注意:帮助信息Options是 Commander 基于你的程序自动生成的,默认的帮助选项是-h,--help。

通过命令进行调试:./bin/vue

执行vue命令,查看输出:

2. nodejs commander命令行

commander命令想详细了解可以参考一下这边文章:
想阅读vue-cli源码,commander你都不会吗? – 掘金 (juejin.cn)

这里用到的是 commander 这个库。它的文档地址是:https://www.npmjs.com/package/commander

const program = require("commander");

// 分为2种操作, 2种操作互相冲突

// Options 操作
program
  .version("0.0.1")
  .option("-t, --types [type]", "test options")
  // option这句话必须加
  .parse(process.argv);

// Commands 操作
program
  // 命令与参数: <> 必填; [] 选填
  .command("exec <cmd> [env]")
  // 别名
  .alias("ex")
  // 帮助信息
  .description("execute the given remote cmd")
  // 没用,option和command是冲突的
  .option("-e, --exec_mode <mode>", "Which exec mode to use")
  // 执行的操作
  .action((cmd, env, options) => {
    // 参数可以拿到
    console.log(`env is ${env}`);
    console.log('exec "%s" using %s mode', cmd, options.exec_mode);
  })
  // 自定义help信息
  .on("--help", function() {
    console.log("自定义help信息");
  });

// 参数长度不够, 打印帮助信息
if (!process.argv.slice(2).length) {
  program.outputHelp();
}

if (program.types) {
  console.log(program.types);
}

// 解析命令行参数
program.parse(process.argv);

文档上基本都写明白了,但是有几个需要注意的点:

  1. 它主要提供 optionscommands 两种操作,option 就是形如“-t,–types”这样的传参,commands 就是形如“exec”这样的传参。 不要混用两者 。
  2. 读取 commands 中传入的参数,写在 .action 中;读取 options 传入的参数,是通过访问 program 上的变量。除此之外,options 操作需要执行 .parse(process.argv) 解析命令行参数
  3. -V 和 -h 默认也是提供的,但是也可以通过自定义覆盖
  4. 一般都把 options 写在前面, 顺便标识版本号 ;把 commands 写在后面;最后会判断一下参数长度,不够会自动输出打印信息

3. vue build

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

4. vue create

#!/usr/bin/env node

// chalk 用于高亮终端打印出来的信息
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()

5. vue list

拉取vuejs-templates的模板信息并输出。

#!/usr/bin/env node
// 记录
const logger = require('../lib/logger')  //自定义工具-用于日志打印。
const request = require('request')     //发送http请求的工具。
const chalk = require('chalk')    //用于高亮console.log打印出来的信息。

/**
 * Padding.
 */

console.log()
process.on('exit', () => {
  console.log('11111')
})

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

// 当输入"vue list"时(我们测试时,可以直接在当前源码文件目录下的终端上输入“bin/vue-list”),vue-cli会请求接口,获取官方模板的信息,然后做了一定处理,在终端上显示出来模板名称和对应的说明。

6. vue init(重点)

  1. 引进一些模块和本地常用工具类方法
#!/usr/bin/env node
// 加载依赖配置
// 下载远程仓库
const download = require("download-git-repo");
// 命令行处理工具
// commander可以让node命令更加简单,提供了命令行输入、参数解析等强大功能
// 可以将文字输出到终端当中的模块
const program = require("commander");
// 文件操作,existsSync - 同步的方法检测文件路径的,从而判断路径是否存在,存在返回true,否则返回false
const exists = require("fs").existsSync;
// node自带的path模块,包含了一些工具函数,用于处理文件与目录的路径,通常用来拼接路径
const path = require("path");
// 在node执行脚本时候,在控制台显示loading效果、显示各种状态的图标等
const ora = require("ora");
// 获取用户根目录
const home = require("user-home");
// 绝对路径用波浪号替换   比如/Users/Documents/dev → ~/dev
const tildify = require("tildify");
// 高亮
const chalk = require("chalk");
// 用户与脚本的命令行交互
// 是一个命令行回答的模块,可以自己设定终端的问题,然后对这些回答给出相应的处理
const inquirer = require("inquirer");
// 是一个可以使用 UNIX 命令的模块,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;
  1. 下面代码声明了vue init的用法
/**
 * Usage.
 */

// 面试: 如何使用本地离线下载好的或者自己预设的模板 --offline

// -------------------------------------------- 1 --------------------------------------------
/**
 * 下面的代码声明了vue init的用法,如果在终端当中 输入 vue init --help或者跟在vue init 后面的参数长
 * 度小于1,会输出下面的描述
 */

/**
 * Usage.
 * usage: 显示help的时候,自定义命令行第一行的用法描述
 * option:添加命令行
 */
program
  // 模板名称-项目名称 <必填> [选填] 例:vue init webpack myvue
  .usage("<template-name> [project-name]") 
  .option("-c, --clone", "use git clone")
  .option("--offline", "use cached template");  
//如果我们已经下载过了模板就不需要再下载模板了  例:vue init webplck myvue --offline

/**
 * Help.
 * on:自定义监听事件
 */
// 可以使用vue init --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); //  解析命令行参数,参数放在属性args上
  if (program.args.length < 1) return program.help(); // 显示帮助信息
}
help();

在控制台输入vue init或者vue init –help,输出信息如下所示:

说明:process是一个全局对象,它提供当前 Node.js 进程的有关信息,以及控制当前 Node.js 进程。process.argv属性返回一个数组,具有的元素如下所述:

// process.argv 返回一个数组

process.argv[0]:返回启动Node.js进程的可执行文件所在的绝对路径

process.argv[1]:返回当前执行的JavaScript文件绝对路径

剩余的元素为其他命令行参数

示例如下图所示:

  1. 下面代码主要是获取变量值:
/**
 * Settings.
 */
/***
 * 获取program.args
 * 
 * process.argv返回一个数组
 * process.argv[0]:返回启动Node.js进程的可执行文件所在的绝对路径
 * process.argv[1]:返回当前执行的JavaScript文件绝对路径
 * 剩余的元素为其他命令行参数
 * 
 * 打印的process.argv数据
 * [
    'C:\Program Files\nodejs\node.exe',
    'C:\Program Files\nodejs\node_modules\vue-cli\bin\vue-init'
   ]
 */

// 模板名称 template
let template = program.args[0];
// 例如:vue init webpack myvue  -----  则template = webpack
// 是否包含斜杠 => 是一个路径(如果含有'/'代表是非官方的模板) vue init username/repo my-project
const hasSlash = template.indexOf("/") > -1;
// 项目名称  刚刚输入的为 myvue
const rawName = program.args[1];
// 如果不存在项目名称或项目名称输入是'.' ,则name取的是 当前文件夹的名称
const inPlace = !rawName || rawName === "."; //rawName存在或者为“.”的时候,视为在当前目录下构建
// 当前目录名为项目构建的目录名称 or 当前目录子目录
// process.cwd():获取Node.js进程的当前工作目录,如 C:\Users\joys\myvue
// path.relative('../', process.cwd())  如:myvue
const name = inPlace ? path.relative("../", process.cwd()) : rawName;
// 输出路径,如/Users/lily/Documents/project/testcommander
const to = path.resolve(rawName || ".");
console.log("program===", program);
// 是否用到git clone  监听options是否使用了 -c 命令
const clone = program.clone || false;


// 本地目录的拼接
// tmp为本地模板路径,如果是离线状态,那么模板路径取本地的
// home = require('user-home'),得到用户的主目录路径,如:/Users/lily
// tmp,如:/Users/lily/.vue-templates/-Users-lily-.nvm-versions-node-v18.7.0-bin-node
const tmp = path.join(home, ".vue-templates", template.replace(/[/:]/g, "-"));
console.log("program.offline===", program.offline);
// 是否使用了vue init --offline xxx 命令,使用了就为true,否则为false
if (program.offline) {
  // tildify(tmp)将绝对路径转换为波形路径,如:~/.vue-templates/-Users-tangxiujiang-.nvm-versions-node-v18.7.0-bin-node
  // 即~相当于/Users/lily
  console.log(`> Use cached template at ${chalk.yellow(tildify(tmp))}`);
  template = tmp;   // 如果读取的是本地模板,则把本地模板赋值给template
}

// ls cat vim/nano/…… tail 《鸟哥的linux私房菜》

/**
 * Padding.
 */
console.log();
// 监听exit事件
process.on("exit", () => {
  console.log();
});

// inPlace:如果不存在项目名称或项目名称输入是'.'
// 或者输出路径存在,则在终端显示设定的问题,并根据回答进行处理
// inquirer是一个命令行回答的模块,可以自己设定终端的问题,然后对这些回答给出相应的处理
// 是否在当前目录下构建 || 指定路径
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();
}

inquirer是一个命令行回答的模块,可以自己设定终端的问题,然后对这些回答给出相应的处理,如下图所示,输入vue init ff,则终端显示等待用户输入答案,并根据答案进行相应的处理:

  1. 根据模版名称,下载、生成模版
/**
 * Check, download and generate the project.
 */
// 主函数
function run() {
  // check if template is local
  // 是否本地路径
  if (isLocalPath(template)) {
    // 模板路径 ls ~/.vue-template/ ……   
    const templatePath = getTemplatePath(template);
    // 如果本地模版路径存在 则开始生成模版
    if (exists(templatePath)) {
      // 通过模板生成最终文件项目 - mark
      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(() => {
      // 官方 or 第三方
      // 路径是否包含‘/’ , 路径不包含'/',则进入该分支,使用官方模版
      if (!hasSlash) {
        // use official templates
// 从这句话以及download-git-repo的用法,我们得知了vue的官方的模板库的地址:https://github.com/vuejs-templates
        const officialTemplate = "vuejs-templates/" + template;
        // 路径有'#'则直接下载     // 例:vuejs-template/pwa#rc-2.0
        if (template.indexOf("#") !== -1) {
          downloadAndGenerate(officialTemplate);
        } else {
          // 路径不包含'-2.0',则输出模版废弃的相关提示
          if (template.indexOf("-2.0") !== -1) {
            warnings.v2SuffixTemplatesDeprecated(template, inPlace ? "" : name);
            return;
          }
          
          // 下载并生成模版
          downloadAndGenerate(officialTemplate);
        }
      } else {
        downloadAndGenerate(template);
      }
    });
  }
}

/**
 * Download a generate from a template repo.
 *
 * @param {String} template
 */

function downloadAndGenerate(template) {
  // 显示loading icon + 'downloading template'
  const spinner = ora("downloading template");
  spinner.start();
  // Remove if local template exists
  // 删除本地存在的模版
  if (exists(tmp)) rm(tmp);
  // 下载模版
  // template目标地址,tmp为下载地址,clone代表是否需要clone
  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);
    });
  });
}

****下载模版方法:download

downloaddownload-git-repo模块,该方法的使用可参考文档:download-git-repo – npm,作用是从代码仓库中下载代码,如下是API的介绍:

download(repository, destination, options, callback)

// repository ---- 代码仓库地址
1. 可采用简写方式:

// 如:'github:liuli/uni-app'或者'liubbc/uni-app'
1.1 GitHub - github:owner/name or simply owner/name
// 如:'gitlab:liuli/uni-app'
1.2 GitLab - gitlab:owner/name
// 如:'bitbucket:liuli/uni-app'
1.3 Bitbucket - bitbucket:owner/name

// 注意:仓库下载的默认分支是master分支,但是可以修改repository的下载分支名,如下所示:
// 即在仓库名称后加上'#分支名称',如liuli/uni-app#dev,表示下载的是dev分支代码
// 另外,可以指定自定义来源,如 gitlab:custom.com:owner/name.,自定义来源默认为 https 或 git@ , 你也可以自己定义协议

2. Direct - direct:url方式
这种方式会跳过上面简写的方式,直接传递 url。有以下注意事项:

2.1 如果使用 direct,并且没有 clone配置项, 必须传入完整的zip文件地址, 包括分支(如果需要的话);
2.2 如果使用 direct 并带有 clone配置项, 必须传入完整的 git repo url ,可以通过 direct:url#dev指定分支

// destination----下载的仓库放置的路径
// options--------选项参数
// callback-------回调函数

生成模版的方法:generate

生成模版的方法generate在文件lib/generate.js文件

// 可以修改终端输出字符样式
const chalk = require('chalk')
// 一个非常简单、可插拔的静态站点生成器。用于遍历文件夹,判断是否需要进行模板渲染
const Metalsmith = require('metalsmith')
// 是一个模版编译器,通过template和json,输出一个html
const Handlebars = require('handlebars')
// 异步处理模块,类似于让方法变成一个线程
const async = require('async')
// 模版引擎整合库
const render = require('consolidate').handlebars.render
 
const path = require('path')
// 字符串数组匹配的库
const multimatch = require('multimatch')
// options.js自定义的配置项文件
const getOptions = require('./options')
// 本地定义的工具类
// ask设置提问的问题,并且对输入的答案进行处理
const ask = require('./ask')
// 过滤不符合条件的数据
const filter = require('./filter')
// 打印日志:区分失败,成功,普通日志
const logger = require('./logger')
 
 
// ------------------------------ 1 -----------------------------
// 注册两两个渲染器
// register 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)
})
// ------------------------------ 1 -----------------------------
 
 
 
 
/**
 * 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) {
  // 读取src目录下的配置文件meta.json或meta.js
  // 同时设置name ,author(当前git用户)到配置opts中
  const opts = getOptions(name, src)
  // 在该目录下生成静态文件
  const metalsmith = Metalsmith(path.join(src, 'template'))
  // data赋值
  const data = Object.assign(metalsmith.metadata(), {
    destDirName: name,
    inPlace: dest === process.cwd(),
    noEscape: true
  })
  // 遍历opts中的helpers对象,注册渲染模版数据
  opts.helpers && Object.keys(opts.helpers).map(key => {
    Handlebars.registerHelper(key, opts.helpers[key])
  })
 
  const helpers = { chalk, logger }
  // 数据合并
  if (opts.metalsmith && typeof opts.metalsmith.before === 'function') {
    opts.metalsmith.before(metalsmith, opts, helpers)
  }
  // askQuestions在终端里面询问一些问题
  metalsmith.use(askQuestions(opts.prompts))
    .use(filterFiles(opts.filters))
    .use(renderTemplateFiles(opts.skipInterpolation))
 
  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)
  }
  // clean:设置在写入之前是否删除原先目标目录 默认为true
  //  source:设置原路径
  //  destination:设置输出的目录
  // build:执行构建
  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') {
        // 当生成完毕之后执行 meta.js当中的 opts.complete方法
        const helpers = { chalk, logger, files }
        opts.complete(data, helpers)
      } else {
        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
  return (files, metalsmith, done) => {
    const keys = Object.keys(files)
    const metalsmithMetadata = metalsmith.metadata()
    async.each(keys, (file, next) => {
      // 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)
  }
}
 
/**
 * Display template complete message.
 *
 * @param {String} message
 * @param {Object} data
 */
 
function logMessage (message, data) {
  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'))
    }
  })
}

参考文章:

Vue2 环境下安装Vue3 ,同一台电脑同时安装vue2 和vue3

浅析vue-cli 2实现原理

Vue-cli学习

© 版权声明
THE END
喜欢就支持一下吧
点赞5 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容