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
中的函数。
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
来看,这其实主要做了注入特性、注入提示、和用户选择回调
的功能。
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
配置生成选项功能。
先获取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
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', 'test@test.com'])
}
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中的核心部分,generator
是create
中最核心的部分。