create vue都出来了,还不知道vue create做了什么吗?

336 阅读4分钟

vue-cli目前已经处于维护模式,现在官方已经推荐我们使用create-vue创建基于Vite的新项目

vue create xxx

使用vue-cli创建项目时,我们使用vue create命令,那为什么我们可以直接使用这个命令呢

"bin": {
    "vue": "bin/vue.js"
  },

查看源码可知,在@/vue/cli/package.json中通过bin命令指定可执行脚本,所以我们能直接在命令行中调用。好,根据这个命令我们也可以得知执行的是vue.js文件,接下来便跟随着来到vue.js中一窥究竟。

vue.js

vue.js中向我们提供了许多命令,有createaddservebuildui等,其中就包括了vue create命令,这里我们只看create命令

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')
  // 省略
  .action((name, options) => {
    if (minimist(process.argv.slice(3))._.length > 1) {
      console.log(chalk.yellow('\n Info: You provided more than one argument. The first one will be used as the app\'s name, the rest are ignored.'))
    }
    // --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)
  })

这里就是使用到了commander这个命令行库,提供命令行输入和参数解析,直接对应到官网上就是这些。最后在action方法中执行create函数

  -p, --preset <presetName>       忽略提示符并使用已保存的或远程的预设选项
  -d, --default                   忽略提示符并使用默认预设选项
  -i, --inlinePreset <json>       忽略提示符并使用内联的 JSON 字符串预设选项
  -m, --packageManager <command>  在安装依赖时使用指定的 npm 客户端
  -r, --registry <url>            在安装依赖时使用指定的 npm registry
  -g, --git [message]             强制 / 跳过 git 初始化,并可选的指定初始化提交信息
  -n, --no-git                    跳过 git 初始化
  -f, --force                     覆写目标目录可能存在的配置
  -c, --clone                     使用 git clone 获取远程预设选项
  -x, --proxy                     使用指定的代理创建项目
  -b, --bare                      创建项目时省略默认组件中的新手指导信息
  -h, --help                      输出使用帮助信息

create

async function create (projectName, options) {

  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) {
   // 省略
  }

  if (fs.existsSync(targetDir) && !options.merge) { // 是否已经存在相同目录名称
    if (options.force) {
      await fs.remove(targetDir)
    } else {
      // 省略
    }
  }

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

这一段的流程会先检验项目名称是否符合规范,不符合规范退出执行。然后再检验是否有相同目录重复,如果已经存在了并且传递了参数force直接移除目录,没有force参数,调用inquirer.prompt进行询问再移除。最后生成Creator实例,执行实例create方法。

Creator

class Creator extends EventEmitter {
  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// 功能提示 Babel TS Vuex...
    this.outroPrompts = this.resolveOutroPrompts()// 存放配置文件,保存预设,使用包管理工具
    this.injectedPrompts = []
    this.promptCompleteCbs = []
    this.afterInvokeCbs = []
    this.afterAnyInvokeCbs = []

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

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

我们看PromptModuleAPI做了什么,PromptModuleAPI接收Creator类,并且定义了四个方法

class PromptModuleAPI {
  constructor (creator) {
    this.creator = creator
  }

  injectFeature (feature) {
    this.creator.featurePrompt.choices.push(feature)
  }

  injectPrompt (prompt) {
    this.creator.injectedPrompts.push(prompt)
  }

  injectOptionForPrompt (name, option) {
    this.creator.injectedPrompts.find(f => {
      return f.name === name
    }).choices.push(option)
  }

  onPromptComplete (cb) {
    this.creator.promptCompleteCbs.push(cb)
  }
}

promptModules遍历传入PromptModuleAPI实例调用实例下的injectFeatureinjectPromptonPromptComplete方法初始化featurePromptinjectedPromptspromptCompleteCbs

image.png

Creator下的create

preset

async function create(cliOptions = {}, preset = null) {
  if (!preset) {
    if (cliOptions.preset) {
      // vue create foo --preset bar
      preset = await this.resolvePreset(cliOptions.preset, cliOptions.clone)
    } else if (cliOptions.default) {
      // vue create foo --default
      preset = defaults.presets['Default (Vue 3)']
    } else if (cliOptions.inlinePreset) {
      // vue create foo --inlinePreset {...}
      try {
        preset = JSON.parse(cliOptions.inlinePreset)
      } catch (e) {
        error(`CLI inline preset is not valid JSON: ${cliOptions.inlinePreset}`)
        exit(1)
      }
    } else {
      preset = await this.promptAndResolvePreset()
    }
  }
  preset = cloneDeep(preset)
  preset.plugins['@vue/cli-service'] = Object.assign({
      projectName: name
    }, preset)
}

首先看是否有传递preset选项,传递了调用resolvePreset解析,如果传递的是default,则为Default (Vue 3),如果传递的是inlinePreset,就用JSON.parse解析,都不是的话就调用promptAndResolvePreset,之后对获取到的预设深克隆,在注入核心插件@vue/cli-service

install依赖

// 获取包管理工具
const packageManager = (
      cliOptions.packageManager ||
      loadOptions().packageManager ||
      (hasYarn() ? 'yarn' : null) ||
      (hasPnpm3OrLater() ? 'pnpm' : 'npm')
    )
// 获取最新CLI版本号
const { latestMinor } = await getVersions()
const pkg = {
      name,
      version: '0.1.0',
      private: true,
      devDependencies: {},
      ...resolvePkg(context)
    }
    const deps = Object.keys(preset.plugins)
    deps.forEach(dep => {
      // 省略
      pkg.devDependencies[dep] = version
    })

    // write package.json
    await writeFileTree(context, {
      'package.json': JSON.stringify(pkg, null, 2)
    })

这段代码主要是获取最新CLI,将preset.plugins中的插件赋值给pkg.devDependencies并且给定好版本,最终写入到package.json

const shouldInitGit = this.shouldInitGit(cliOptions)
    if (shouldInitGit) {
      log(`🗃  Initializing git repository...`)
      this.emit('creation', { event: 'git-init' })
      await run('git init')
    }
    
log(`⚙\u{fe0f}  Installing CLI plugins. This might take a while...`)
    log()
    this.emit('creation', { event: 'plugins-install' })

    if (isTestOrDebug && !process.env.VUE_CLI_TEST_DO_INSTALL_PLUGIN) {
      // in development, avoid installation process
      await require('./util/setupDevProject')(context)
    } else {
      await pm.install()
    }

后面判断是否需要git初始化,需要就执行git init命令,在之后就是调用install安装插件

promptAndResolvePreset

async  promptAndResolvePreset(answers = null){
  if (!answers) {
    await clearConsole(true)
    answers = await inquirer.prompt(this.resolveFinalPrompts())
  }
  if (answers.packageManager) {
      saveOptions({
        packageManager: answers.packageManager
      })
    }
    let preset
    if (answers.preset && answers.preset !== '__manual__') {
      preset = await this.resolvePreset(answers.preset)
    } else {
      // manual
      preset = {
        useConfigFiles: answers.useConfigFiles === 'files',
        plugins: {}
      }
      answers.features = answers.features || []
      // run cb registered by prompt modules to finalize the preset
      this.promptCompleteCbs.forEach(cb => cb(answers, preset))
    }
    validatePreset(preset)

    // save preset
    if (answers.save && answers.saveName && savePreset(answers.saveName, preset)) {
      log()
      log(`🎉  Preset ${chalk.yellow(answers.saveName)} saved in ${chalk.yellow(rcPath)}`)
    }

    debug('vue-cli:preset')(preset)
    return preset

}

promptAndResolvePreset中用inquirer.prompt进行询问,resolveFinalPrompts就是将所有的prompt进行了整合。之后将packageManager保存。如果返回后的answers.preset是本地保存的preset或者default,调用resolvePreset解析,不是遍历promptCompleteCbs执行函数,将插件添加到options.plugins 中,后面就是验证预设和保存预设。

const prompts = [
      this.presetPrompt,
      this.featurePrompt,
      ...this.injectedPrompts,
      ...this.outroPrompts
    ]

image.png

install

async install () {
    const args = []

    if (this.needsPeerDepsFix) {
      args.push('--legacy-peer-deps')
    }

    if (process.env.VUE_CLI_TEST) {
      args.push('--silent', '--no-progress')
    }

    return await this.runCommand('install', args)
  }

install函数主要也就是执行runCommand函数,再去看runCommand

async runCommand (command, args) {
    const prevNodeEnv = process.env.NODE_ENV
    // In the use case of Vue CLI, when installing dependencies,
    // the `NODE_ENV` environment variable does no good;
    // it only confuses users by skipping dev deps (when set to `production`).
    delete process.env.NODE_ENV

    await this.setRegistryEnvs()
    await executeCommand(
      this.bin,
      [
        ...PACKAGE_MANAGER_CONFIG[this.bin][command],
        ...(args || [])
      ],
      this.context
    )

    if (prevNodeEnv) {
      process.env.NODE_ENV = prevNodeEnv
    }
  }

runCommand调用了setRegistryEnvs,作用是指定安装依赖时的镜像源,在然后就是调用executeCommand执行安装依赖命令

Generator

呃......这里的Generator看的不太明白,就只能先说下主要做了什么

const plugins = await this.resolvePlugins(preset.plugins, pkg)
const generator = new Generator(context, {
      pkg,
      plugins,
      afterInvokeCbs,
      afterAnyInvokeCbs
    })
    await generator.generate({
      extractConfigFiles: preset.useConfigFiles
    })
    
    if (!isTestOrDebug || process.env.VUE_CLI_TEST_DO_INSTALL_PLUGIN) {
      await pm.install()
    }
    for (const cb of afterInvokeCbs) {
      await cb()
    }
    for (const cb of afterAnyInvokeCbs) {
      await cb()
    }
    if (!generator.files['README.md']) {
      // 省略
      await writeFileTree(context, {
        'README.md': generateReadme(generator.pkg, packageManager)
      })
    }
    let gitCommitFailed = false
    if (shouldInitGit) {
      await run('git add -A')
      // 省略
      const msg = typeof cliOptions.git === 'string' ? cliOptions.git : 'init'
      try {
        await run('git', ['commit', '-m', msg, '--no-verify'])
      } catch (e) {
        gitCommitFailed = true
      }
      generator.printExitLogs()
    }


这一段先调用resolvePlugins加载每个插件的generator,之后实例化一个Generator,由Generator去调用每个插件的generator,然后根据useConfigFiles的值去提取配置文件。往下就是安装额外依赖,这部分依赖是由generator注入的,接着执行afterInvokeCbsafterAnyInvokeCbs中的回调,这里面的回调根据调试知道的是这样的

async () => {
  try {
    await require('../lint')({ silent: true }, api)
  } catch (e) {}
}

后面的就比较简单了,生成readme.md文件,git提交,输出日志等

vue create总结

看完vue create后的感觉就是有点复杂,能力有限,但还好是理清了主要流程

vue-cli-导出.png