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中向我们提供了许多命令,有create、add、serve、build、ui等,其中就包括了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实例调用实例下的injectFeature,injectPrompt,onPromptComplete方法初始化featurePrompt,injectedPrompts,promptCompleteCbs
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
]
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注入的,接着执行afterInvokeCbs和afterAnyInvokeCbs中的回调,这里面的回调根据调试知道的是这样的
async () => {
try {
await require('../lint')({ silent: true }, api)
} catch (e) {}
}
后面的就比较简单了,生成readme.md文件,git提交,输出日志等
vue create总结
看完vue create后的感觉就是有点复杂,能力有限,但还好是理清了主要流程