【Vue】vue-cli源码分析

vue-cli源码版本为3.1.3

命令行工具入口文件在packages/@vue/cli/bin/vue.js
vue-cli的核心命令是create,整体相对是比较复杂的,我们理解其中的主要逻辑就好。


处理命令行参数

// packages/@vue/cli/bin/vue.js
const chalk = require('chalk')
const semver = require('semver')
const requiredVersion = require('../package.json').engines.node

function checkNodeVersion (wanted, id) {
  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.'
    ))
    process.exit(1)
  }
}

checkNodeVersion(requiredVersion, 'vue-cli')

进程进来比较进程的node版本和vue-cli所要求的最低node版本,若不符合则提示更新node并退出进程

接着通过commander生成命令行,通过子命令create来处理vue create的命令,最后将项目名称和命令参数传给lib/create中的函数。


实例化核心方法Creator

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

packages/@vue/cli/lib/create中,首先通过vlidate-npm-package-name处理包名是否合规。接着在目标文件存在的情况下来提供用户处理目标文件的交互。这一段代码都比较好理解,最后生成creator类并调用了creator.create方法。Creator类是整个vue-cli的核心。

在实例化Creator中传入了getPromptModules()

exports.getPromptModules = () => {
  return [
    'babel',
    'typescript',
    'pwa',
    'router',
    'vuex',
    'cssPreprocessors',
    'linter',
    'unit',
    'e2e'
  ].map(file => require(`../promptModules/${file}`))
}
const chalk = require('chalk')

module.exports = cli => {
  cli.injectFeature({
    name: 'Router',
    value: 'router',
    description: 'Structure the app with dynamic pages',
    link: 'https://router.vuejs.org/'
  })

  cli.injectPrompt({
    name: 'routerHistoryMode',
    when: answers => answers.features.includes('router'),
    type: 'confirm',
    message: `Use history mode for router? ${chalk.yellow(`(Requires proper server setup for index fallback in production)`)}`,
    description: `By using the HTML5 History API, the URLs don't need the '#' character anymore.`,
    link: 'https://router.vuejs.org/guide/essentials/history-mode.html'
  })

  cli.onPromptComplete((answers, options) => {
    if (answers.features.includes('router')) {
      options.router = true
      options.routerHistoryMode = answers.routerHistoryMode
    }
  })
}

我们通过router来看,这其实主要做了注入特性、注入提示、和用户选择回调的功能。


Creator构造函数

constructor (name, context, promptModules) {
    super()

    this.name = name
    this.context = process.env.VUE_CLI_CONTEXT = context
    const { presetPrompt, featurePrompt } = this.resolveIntroPrompts()
    this.presetPrompt = presetPrompt
    this.featurePrompt = featurePrompt
    this.outroPrompts = this.resolveOutroPrompts()
    this.injectedPrompts = []
    this.promptCompleteCbs = []
    this.createCompleteCbs = []

    this.run = this.run.bind(this)

    const promptAPI = new PromptModuleAPI(this)
    promptModules.forEach(m => m(promptAPI))
  }

这里主要生成终端交互的选项参数,resolveIntroPrompts()通过合并项目默认的预设配置和用户生成的.vuerc配置生成选项功能。


执行create方法

先获取preset预设配置

const packageManager = (
      cliOptions.packageManager ||
      loadOptions().packageManager ||
      (hasYarn() ? 'yarn' : 'npm')
    )

    await clearConsole()
    logWithSpinner(`✨`, `Creating project in ${chalk.yellow(context)}.`)
    this.emit('creation', { event: 'creating' })

    // get latest CLI version
    const { latest } = await getVersions()
    // generate package.json with plugin dependencies
    const pkg = {
      name,
      version: '0.1.0',
      private: true,
      devDependencies: {}
    }
    const deps = Object.keys(preset.plugins)
    deps.forEach(dep => {
      if (preset.plugins[dep]._isPreset) {
        return
      }
      pkg.devDependencies[dep] = (
        preset.plugins[dep].version ||
        ((/^@vue/.test(dep) && latest[dep]) ? `^${latest[dep]}` : `latest`)
      )
    })
    // write package.json
    await writeFileTree(context, {
      'package.json': JSON.stringify(pkg, null, 2)
    })

这里接着就是获取最新的CLI版本生成package.json文件

// intilaize git repository before installing deps
// so that vue-cli-service can setup git hooks.
    const shouldInitGit = await this.shouldInitGit(cliOptions)
    if (shouldInitGit) {
      logWithSpinner(``, `Initializing git repository...`)
      this.emit('creation', { event: 'git-init' })
      await run('git init')
    }

    // install plugins
    stopSpinner()
    log(`⚙  Installing CLI plugins. This might take a while...`)
    log()
    this.emit('creation', { event: 'plugins-install' })
    if (isTestOrDebug) {
      // in development, avoid installation process
      await require('./util/setupDevProject')(context)
    } else {
      await installDeps(context, packageManager, cliOptions.registry)
    }

再接着就是是否初始化git并执行installDeps()安装依赖。


Generator生成插件

安装完依赖后,就会实例化Generator并调用generator方法来加载每个插件的generator

	const generator = new Generator(context, {
      pkg,
      plugins,
      completeCbs: createCompleteCbs
    })
    await generator.generate({
      extractConfigFiles: preset.useConfigFiles
    })

最后操作

// install additional deps (injected by generators)
log(`  Installing additional dependencies...`)
this.emit('creation', { event: 'deps-install' })
log()
if (!isTestOrDebug) {
  await installDeps(context, packageManager, cliOptions.registry)
}

// run complete cbs if any (injected by generators)
logWithSpinner('⚓', `Running completion hooks...`)
this.emit('creation', { event: 'completion-hooks' })
for (const cb of createCompleteCbs) {
  await cb()
}

// generate README.md
stopSpinner()
log()
logWithSpinner('', 'Generating README.md...')
await writeFileTree(context, {
  'README.md': generateReadme(generator.pkg, packageManager)
})

// commit initial state
let gitCommitFailed = false
if (shouldInitGit) {
  await run('git add -A')
  if (isTestOrDebug) {
    await run('git', ['config', 'user.name', 'test'])
    await run('git', ['config', 'user.email', '[email protected]'])
  }
  const msg = typeof cliOptions.git === 'string' ? cliOptions.git : 'init'
  try {
    await run('git', ['commit', '-m', msg])
  } catch (e) {
    gitCommitFailed = true
  }
}

// log instructions
stopSpinner()
log()
log(`  Successfully created project ${chalk.yellow(name)}.`)
log(
  `  Get started with the following commands:\n\n` +
  (this.context === process.cwd() ? `` : chalk.cyan(` ${chalk.gray('$')} cd ${name}\n`)) +
  chalk.cyan(` ${chalk.gray('$')} ${packageManager === 'yarn' ? 'yarn serve' : 'npm run serve'}`)
)
log()
this.emit('creation', { event: 'done' })

if (gitCommitFailed) {
  warn(
    `Skipped git commit due to missing username and email in git config.\n` +
    `You will need to perform the initial commit yourself.\n`
  )
}

generator.printExitLogs()

这里主要安装额外依赖、git初始化提交

总结

create是vue-cli中的核心部分,generatorcreate中最核心的部分。

你可能感兴趣的:(vue)