rico-c/RICO-BLOG

Vue-Cli源码解析

rico-c opened this issue · 0 comments

源码地址点这里:Vue-Cli源码地址

初始化项目

npm install -g @vue/cli

在全局安装Vue-Cli后,可以使用vue create hello-world初始化项目,我们就从这里开始看源码。

package.json

下面为项目的package.json文件

{
  "name": "@vue/cli",
  "description": "Command line interface for rapid Vue.js development",
  "bin": {
    //注册vue命令
    "vue": "bin/vue.js"
  },
  "author": "Evan You",
  "license": "MIT",
  "publishConfig": {
    "access": "public"
  },
  "dependencies": {
    "@vue/cli-shared-utils": "^3.2.2",
    "@vue/cli-ui": "^3.2.3",
    "@vue/cli-ui-addon-webpack": "^3.2.3",
    "@vue/cli-ui-addon-widgets": "^3.2.3",
      //给终端界面添加颜色
    "chalk": "^2.4.1",
    "cmd-shim": "^2.0.2",
      //实现输入命令的响应
    "commander": "^2.16.0",
    "debug": "^4.1.0",
    "deepmerge": "^3.0.0",
      //从Git仓库下载并解析
    "download-git-repo": "^1.0.2",
    "ejs": "^2.6.1",
    "envinfo": "^6.0.1",
    "execa": "^1.0.0",
    "fs-extra": "^7.0.1",
    "globby": "^8.0.1",
    "import-global": "^0.1.0",
      //实现命令行的问答
    "inquirer": "^6.0.0",
    "isbinaryfile": "^3.0.2",
    "javascript-stringify": "^1.6.0",
    "js-yaml": "^3.12.0",
    "lodash.clonedeep": "^4.5.0",
      //命令行参数解析
    "minimist": "^1.2.0",
    "recast": "^0.15.2",
    "request": "^2.87.0",
    "request-promise-native": "^1.0.5",
    "resolve": "^1.8.1",
      //版本号解析工具
    "semver": "^5.5.0",
    "shortid": "^2.2.11",
      //将windows风格的路径转为Unix风格的路径
    "slash": "^2.0.0",
    "validate-npm-package-name": "^3.0.0",
    "yaml-front-matter": "^3.4.1"
  },
  "engines": {
    "node": ">=8.9"
  }
}

首先,vue命令被注册在package.json的bin字段下,关于bin字段的使用可以参考npm官方文档介绍部分:

A lot of packages have one or more executable files that they’d like to install into the PATH. npm makes this pretty easy (in fact, it uses this feature to install the “npm” executable.)

To use this, supply a bin field in your package.json which is a map of command name to local file name. On install, npm will symlink that file into prefix/bin for global installs, or ./node_modules/.bin/ for local installs.

bin字段注册的可执行文件vue.js为项目的入口,在经过全局安装后,vue命令被注册到全局的bin目录下,这时我们就可以直接全局使用vue.js可执行文件。

vue.js

在vue.js顶部写入的#!/usr/bin/env node,可以保证默认使用node来运行该可执行文件,这样运行时就可以直接通过输入在bin中注册号的的执行名vue来运行vue.js。

执行vue命令后,首先检查node.js版本号:

//版本号对比工具
const semver = require('semver')
const requiredVersion = require('../package.json').engines.node

function checkNodeVersion (wanted, id) {
    //process.version返回当前使用的Node.js版本号
  if (!semver.satisfies(process.version, wanted)) {
    console.log(chalk.red(
      'You are using Node ' + process.version + ', but this version of ' + id +
      ' requires Node ' + wanted + '.\nPlease upgrade your Node version.'
    ))
      //终止当前进程并返回1状态码
    process.exit(1)
  }
}

checkNodeVersion(requiredVersion, 'vue-cli')

if (semver.satisfies(process.version, '9.x')) {
    //以红色字体渲染文本
  console.log(chalk.red(
    `You are using Node ${process.version}.\n` +
    `Node.js 9.x has already reached end-of-life and will not be supported in future major releases.\n` +
    `It's strongly recommended to use an active LTS version instead.`
  ))
}

检查版本

const program = require('commander')
const loadCommand = require('../lib/util/loadCommand')

program
  .version(require('../package').version)
  .usage('<command> [options]')

初始化项目

Vue-Cli使用commander作为命令工具,详细关于commander的API可以查阅commander的github主页。

接着往下看,进入初始化项目的配置,下面是源码:

program
  .command('create <app-name>')
  .description('create a new project powered by vue-cli-service')
  .option('-p, --preset <presetName>', 'Skip prompts and use saved or remote preset')
  .option('-d, --default', 'Skip prompts and use default preset')
  .option('-i, --inlinePreset <json>', 'Skip prompts and use inline JSON string as preset')
  .option('-m, --packageManager <command>', 'Use specified npm client when installing dependencies')
  .option('-r, --registry <url>', 'Use specified npm registry when installing dependencies (only for npm)')
  .option('-g, --git [message]', 'Force git initialization with initial commit message')
  .option('-n, --no-git', 'Skip git initialization')
  .option('-f, --force', 'Overwrite target directory if it exists')
  .option('-c, --clone', 'Use git clone when fetching remote preset')
  .option('-x, --proxy', 'Use specified proxy when creating project')
  .option('-b, --bare', 'Scaffold project without beginner instructions')
  .action((name, cmd) => {
    const options = cleanArgs(cmd)
    // --git makes commander to default git to true
    if (process.argv.includes('-g') || process.argv.includes('--git')) {
      options.forceGit = true
    }
    require('../lib/create')(name, options)
  })

.command('create <app-name>')绑定了create命令,并保存了输入的项目名,.option('-d,--default','Skip prompts and use default preset')用于定义选项值,设置描述及选项快捷键,并将选项值保存到process.argv中。
在链式配置好选项值后,.action(()=>{})用于配置该命令的处理函数,将输入的项目名和配置的选项传入create.js中执行。

const fs = require('fs-extra')
const path = require('path')
const chalk = require('chalk')
const inquirer = require('inquirer')
const Creator = require('./Creator')
const { clearConsole } = require('./util/clearConsole')
const { getPromptModules } = require('./util/createTools')
const { error, stopSpinner, exit } = require('@vue/cli-shared-utils')
const validateProjectName = require('validate-npm-package-name')

async function create (projectName, options) {
  if (options.proxy) {
   //设置代理
    process.env.HTTP_PROXY = options.proxy
  }

  const cwd = options.cwd || process.cwd()
  const inCurrent = projectName === '.'
  const name = inCurrent ? path.relative('../', cwd) : projectName
  const targetDir = path.resolve(cwd, projectName || '.')

  const result = validateProjectName(name)
  if (!result.validForNewPackages) {
    console.error(chalk.red(`Invalid project name: "${name}"`))
    result.errors && result.errors.forEach(err => {
      console.error(chalk.red.dim('Error: ' + err))
    })
    result.warnings && result.warnings.forEach(warn => {
      console.error(chalk.red.dim('Warning: ' + warn))
    })
    exit(1)
  }

  if (fs.existsSync(targetDir)) {
    if (options.force) {
      await fs.remove(targetDir)
    } else {
      await clearConsole()
      if (inCurrent) {
        const { ok } = await inquirer.prompt([
          {
            name: 'ok',
            type: 'confirm',
            message: `Generate project in current directory?`
          }
        ])
        if (!ok) {
          return
        }
      } else {
        const { action } = await inquirer.prompt([
          {
            name: 'action',
            type: 'list',
            message: `Target directory ${chalk.cyan(targetDir)} already exists. Pick an action:`,
            choices: [
              { name: 'Overwrite', value: 'overwrite' },
              { name: 'Merge', value: 'merge' },
              { name: 'Cancel', value: false }
            ]
          }
        ])
        if (!action) {
          return
        } else if (action === 'overwrite') {
          console.log(`\nRemoving ${chalk.cyan(targetDir)}...`)
          await fs.remove(targetDir)
        }
      }
    }
  }

  const creator = new Creator(name, targetDir, getPromptModules())
  await creator.create(options)
}

module.exports = (...args) => {
  return create(...args).catch(err => {
    stopSpinner(false) // do not persist
    error(err)
    if (!process.env.VUE_CLI_TEST) {
      process.exit(1)
    }
  })
}