前言
上文简单介绍了vue -V的执行过程,本文在此基础上,继续分析vue create的执行过程,了解vue create <app-name>经历了哪些过程 ?
正文
文件路径:/packages/@vue/cli/bin/vue.js
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相关代码
...
.action((name, cmd) => {
const options = cleanArgs(cmd)
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
解析到create <app-name>
命令,会自动执行action
方法中的回调函数。首先是调用cleanArgs
方法,它的作用是将传递的Command对象cnd
,提取实际的选项到一个新对象中作为options,主要是为了可读性,5.0版本已经将这个方法删除。直接传递Command对象作为options
变量,了解即可。然后就是校验命令输入的非options参数是否超过1个,若是则提醒用户,将使用第一个非option参数作为项目名称。其次是判断是否有-g
选项并重新赋值forceGit
避免歧义。最重要就是执行/lib/create
函数。
create.js
文件路径:/packages/@vue/cli/lib/create.js
module.exports = (...args) => {
return create(...args).catch(err => {
stopSpinner(false) // do not persist
error(err)
if (!process.env.VUE_CLI_TEST) {
process.exit(1)
}
})
}
执行文件定义create
async函数,其中...args
剩余参数可以作为参数传递的小技巧。如果抛出异常则执行catch函数。
create
async function create (projectName, options) {
// 是否有--proxy,若有则定义process.env.HTTP_PROXY变量
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)
}
// 判断生成目标文件夹是否存在且命令行没有--merge选项
if (fs.existsSync(targetDir) && !options.merge) {
// 有--force选项,直接移除已存在的目标文件夹
if (options.force) {
await fs.remove(targetDir)
} else {
await clearConsole()
if (inCurrent) {
// 1.当前目录创建项目,询问是否继续,否则退出,是则继续
const { ok } = await inquirer.prompt([
{
name: 'ok',
type: 'confirm',
message: `Generate project in current directory?`
}
])
if (!ok) {
return
}
} else {
// 1.非当前目录创建项目,给出3个选择:Overwrite、Merge、Cancel
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 }
]
}
])
// cancel直接退出
if (!action) {
return
} else if (action === 'overwrite') {
// overwrite 删除原文件夹,选择merge就是继续执行下面代码
console.log(`\nRemoving ${chalk.cyan(targetDir)}...`)
await fs.remove(targetDir)
}
}
}
}
const creator = new Creator(name, targetDir, getPromptModules())
await creator.create(options)
}
在当前目录创建项目,会执行vue create .
,所以通过projectName === '.'
是否为当前目录。validateProjectName
函数则是校验项目名称是否符合npm包名标准,如不符合则警告并退出。接着就是校验要生成目标文件夹是否已存在,若已存在,则选择是否继续执行(merge),或者直接退出执行,以及删除已存在文件夹继续执行生成项目。最终就是生成Creator
实例creator
,通过creator
的create
方法来创建新的项目。
Creator
文件路径:/packages/@vue/cli/lib/Creator.js
将创建项目的相关逻辑封装到了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
this.outroPrompts = this.resolveOutroPrompts()
this.injectedPrompts = []
// 回调函数数组
this.promptCompleteCbs = []
this.afterInvokeCbs = []
this.afterAnyInvokeCbs = []
this.run = this.run.bind(this)
const promptAPI = new PromptModuleAPI(this)
// 获取cli/lib/promptModules下所有函数,并传入promptAPI作为参数
promptModules.forEach(m => m(promptAPI))
}
}
实例化Creator
类会接收3个参数,分别是项目名称name
,生成目标文件夹路径targetDir
,以及promptModules
。这里传入的promptModules
是由getPromptModules
函数生成。它会返回函数数组,分别动态引用了cli/lib/promptModules
各个文件暴露出的函数,每个函数都会接收PromptModuleAPI
实例做为cli
参数。这里不过多介绍PromptModuleAPI类,你可以简单理解为对creator实例的二次封装。通过PromptModuleAPI实例来完成对命令行交互提示相关逻辑的操作。
文件路径:/packages/@vue/cli/lib/utils/createTools.js
exports.getPromptModules = () => {
return [
'vueVersion',
'babel',
'typescript',
'pwa',
'router',
'vuex',
'cssPreprocessors',
'linter',
'unit',
'e2e'
].map(file => require(`../promptModules/${file}`))
}
其中presetPrompt
与featurePrompt
由Creator类的resolveIntroPrompts
方法生成。
resolveIntroPrompts () {
// 获取preset对象
const presets = this.getPresets()
// preset对象转换成array
const presetChoices = Object.entries(presets).map(([name, preset]) => {
let displayName = name
if (name === 'default') {
displayName = 'Default'
} else if (name === '__default_vue_3__') {
displayName = 'Default (Vue 3 Preview)'
}
return {
name: `${displayName} (${formatFeatures(preset)})`,
value: name
}
})
// 生成presetPrompt
const presetPrompt = {
name: 'preset',
type: 'list',
message: `Please pick a preset:`,
choices: [
...presetChoices,
{
name: 'Manually select features',
value: '__manual__'
}
]
}
// 生成 featurePrompt
const featurePrompt = {
name: 'features',
when: isManualMode,
type: 'checkbox',
message: 'Check the features needed for your project:',
choices: [],
pageSize: 10
}
return {
presetPrompt,
featurePrompt
}
}
在resolveIntroPrompts
函数中会首先执行getPresets
方法来获取preset。从这里开始就用到option.js
相关的操作loadOptions
。在 vue create
过程中保存的 preset 会被放在你的 home 目录下的一个配置文件中 (~/.vuerc
)。所以resolveIntroPrompts
函数会读取.vuerc
的preset。然后将默认内置的defaults.presets
合并成完整的preset
列表对象。
exports.defaults = {
lastChecked: undefined,
latestVersion: undefined,
packageManager: undefined,
useTaobaoRegistry: undefined,
presets: {
'default': Object.assign({ vueVersion: '2' }, exports.defaultPreset),
'__default_vue_3__': Object.assign({ vueVersion: '3' }, exports.defaultPreset)
}
}
getPresets () {
const savedOptions = loadOptions()
return Object.assign({}, savedOptions.presets, defaults.presets)
}
文件路径:/packages/@vue/cli/lib/options.js
let cachedOptions
exports.loadOptions = () => {
// 存在则返回缓存的Options
if (cachedOptions) {
return cachedOptions
}
// rcPath一般为用户目录下的.vuerc文件的绝对路径
if (fs.existsSync(rcPath)) {
try {
cachedOptions = JSON.parse(fs.readFileSync(rcPath, 'utf-8'))
} catch (e) {
error(
`Error loading saved preferences: ` +
`~/.vuerc may be corrupted or have syntax errors. ` +
`Please fix/delete it and re-run vue-cli in manual mode.\n` +
`(${e.message})`
)
exit(1)
}
validate(cachedOptions, schema, () => {
error(
`~/.vuerc may be outdated. ` +
`Please delete it and re-run vue-cli in manual mode.`
)
})
return cachedOptions
} else {
return {}
}
}
- preset示例
{
"useConfigFiles": true,
"cssPreprocessor": "sass",
"plugins": {
"@vue/cli-plugin-babel": {},
"@vue/cli-plugin-eslint": {
"config": "airbnb",
"lintOn": ["save", "commit"]
},
"@vue/cli-plugin-router": {},
"@vue/cli-plugin-vuex": {}
}
}
outroPrompts
则是由resolveOutroPrompts()
生成,封装了命令行执行的结尾相关的Prompt,如是否拆分单个配置化文件,是否保存preset到.vuerc
等。最后还会判断是否有设置默认包管理器,没有的话(换句话说,第一次执行vue create <app-name>
)则增加选择默认包管理器的交互提示。
resolveOutroPrompts () {
const outroPrompts = [
{
name: 'useConfigFiles',
when: isManualMode,
type: 'list',
message: 'Where do you prefer placing config for Babel, ESLint, etc.?',
choices: [
{
name: 'In dedicated config files',
value: 'files'
},
{
name: 'In package.json',
value: 'pkg'
}
]
},
{
name: 'save',
when: isManualMode,
type: 'confirm',
message: 'Save this as a preset for future projects?',
default: false
},
{
name: 'saveName',
when: answers => answers.save,
type: 'input',
message: 'Save preset as:'
}
]
// ask for packageManager once
const savedOptions = loadOptions()
if (!savedOptions.packageManager && (hasYarn() || hasPnpm3OrLater())) {
const packageManagerChoices = []
if (hasYarn()) {
packageManagerChoices.push({
name: 'Use Yarn',
value: 'yarn',
short: 'Yarn'
})
}
if (hasPnpm3OrLater()) {
packageManagerChoices.push({
name: 'Use PNPM',
value: 'pnpm',
short: 'PNPM'
})
}
packageManagerChoices.push({
name: 'Use NPM',
value: 'npm',
short: 'NPM'
})
outroPrompts.push({
name: 'packageManager',
type: 'list',
message: 'Pick the package manager to use when installing dependencies:',
choices: packageManagerChoices
})
}
return outroPrompts
}
create 方法
真正生成项目的逻辑则是调用了creator
实例的create
方法。由上文可知,cliOptions
是命令行的参数选项构成的对象,preset
不传默认是null。所以会生成preset
对象,代码中分为4种情况,这里分析最后一种,通过promptAndResolvePreset
方法生成preset
对象。
async create (cliOptions = {}, preset = null) {
const isTestOrDebug = process.env.VUE_CLI_TEST || process.env.VUE_CLI_DEBUG
const { run, name, context, afterInvokeCbs, afterAnyInvokeCbs } = this
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
} 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()
}
}
// clone before mutating
// 克隆对象
preset = cloneDeep(preset)
// inject core service
// 注入核心服务
preset.plugins['@vue/cli-service'] = Object.assign({
projectName: name
}, preset)
if (cliOptions.bare) {
preset.plugins['@vue/cli-service'].bare = true
}
// legacy support for router
// 添加路由支持
if (preset.router) {
preset.plugins['@vue/cli-plugin-router'] = {}
if (preset.routerHistoryMode) {
preset.plugins['@vue/cli-plugin-router'].historyMode = true
}
}
// Introducing this hack because typescript plugin must be invoked after router.
// Currently we rely on the `plugins` object enumeration order,
// which depends on the order of the field initialization.
// FIXME: Remove this ugly hack after the plugin ordering API settled down
// 引入这个hack是因为typescript插件必须在router之后调用。
// 目前我们依赖于' plugins '对象枚举顺序,
// 这取决于字段初始化的顺序。
if (preset.plugins['@vue/cli-plugin-router'] && preset.plugins['@vue/cli-plugin-typescript']) {
const tmp = preset.plugins['@vue/cli-plugin-typescript']
delete preset.plugins['@vue/cli-plugin-typescript']
preset.plugins['@vue/cli-plugin-typescript'] = tmp
}
// legacy support for vuex
// 添加vuex支持
if (preset.vuex) {
preset.plugins['@vue/cli-plugin-vuex'] = {}
}
// 确定包管理工具
const packageManager = (
cliOptions.packageManager ||
loadOptions().packageManager ||
(hasYarn() ? 'yarn' : null) ||
(hasPnpm3OrLater() ? 'pnpm' : 'npm')
)
await clearConsole()
const pm = new PackageManager({ context, forcePackageManager: packageManager })
log(`✨ Creating project in ${chalk.yellow(context)}.`)
this.emit('creation', { event: 'creating' })
// get latest CLI plugin version
// 获取插件的最新版本
const { latestMinor } = await getVersions()
// generate package.json with plugin dependencies
// 通过plugin依赖生成 package.json对象
const pkg = {
name,
version: '0.1.0',
private: true,
devDependencies: {},
...resolvePkg(context)
}
const deps = Object.keys(preset.plugins)
deps.forEach(dep => {
if (preset.plugins[dep]._isPreset) {
return
}
let { version } = preset.plugins[dep]
if (!version) {
if (isOfficialPlugin(dep) || dep === '@vue/cli-service' || dep === '@vue/babel-preset-env') {
version = isTestOrDebug ? `file:${path.resolve(__dirname, '../../../', dep)}` : `~${latestMinor}`
} else {
version = 'latest'
}
}
pkg.devDependencies[dep] = version
})
// write package.json
// 生成package.json文件
await writeFileTree(context, {
'package.json': JSON.stringify(pkg, null, 2)
})
// generate a .npmrc file for pnpm, to persist the `shamefully-flatten` flag
if (packageManager === 'pnpm') {
const pnpmConfig = hasPnpmVersionOrLater('4.0.0')
? 'shamefully-hoist=true\n'
: 'shamefully-flatten=true\n'
await writeFileTree(context, {
'.npmrc': pnpmConfig
})
}
if (packageManager === 'yarn' && semver.satisfies(process.version, '8.x')) {
// Vue CLI 4.x should support Node 8.x,
// but some dependenices already bumped `engines` field to Node 10
// and Yarn treats `engines` field too strictly
await writeFileTree(context, {
'.yarnrc': '# Hotfix for Node 8.x\n--install.ignore-engines true\n'
})
}
// intilaize git repository before installing deps
// so that vue-cli-service can setup git hooks.
const shouldInitGit = this.shouldInitGit(cliOptions)
if (shouldInitGit) {
log(`🗃 Initializing git repository...`)
this.emit('creation', { event: 'git-init' })
await run('git init')
}
// install plugins
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 {
// 安装node_modules依赖
await pm.install()
}
// run generator
log(`🚀 Invoking generators...`)
this.emit('creation', { event: 'invoking-generators' })
const plugins = await this.resolvePlugins(preset.plugins, pkg)
const generator = new Generator(context, {
pkg,
plugins,
afterInvokeCbs,
afterAnyInvokeCbs
})
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 || process.env.VUE_CLI_TEST_DO_INSTALL_PLUGIN) {
await pm.install()
}
// run complete cbs if any (injected by generators)
log(`⚓ Running completion hooks...`)
this.emit('creation', { event: 'completion-hooks' })
for (const cb of afterInvokeCbs) {
await cb()
}
for (const cb of afterAnyInvokeCbs) {
await cb()
}
if (!generator.files['README.md']) {
// generate README.md
log()
log('📄 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'])
await run('git', ['config', 'commit.gpgSign', 'false'])
}
const msg = typeof cliOptions.git === 'string' ? cliOptions.git : 'init'
try {
await run('git', ['commit', '-m', msg, '--no-verify'])
} catch (e) {
gitCommitFailed = true
}
}
// log instructions
log()
log(`🎉 Successfully created project ${chalk.yellow(name)}.`)
if (!cliOptions.skipGetStarted) {
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' : packageManager === 'pnpm' ? 'pnpm run 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, or failed to sign commit.\n` +
`You will need to perform the initial commit yourself.\n`
)
}
generator.printExitLogs()
}
- promptAndResolvePreset方法
本质上就是使用inquirer
库来执行creator
实例中所有相关的Prompt
,通过用户选择手动模式或预设模式来生成相应的preset
对象。
async promptAndResolvePreset (answers = null) {
// prompt
if (!answers) {
await clearConsole(true)
// 核心代码
answers = await inquirer.prompt(this.resolveFinalPrompts())
}
debug('vue-cli:answers')(answers)
if (answers.packageManager) {
saveOptions({
packageManager: answers.packageManager
})
}
let preset
// 内置保存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))
}
// validate
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
}
其中resolveFinalPrompts
函数就是获取所有相关的Prompts。
resolveFinalPrompts () {
// patch generator-injected prompts to only show in manual mode
// 手动模式下才执行injectedPrompts数组提示
this.injectedPrompts.forEach(prompt => {
const originalWhen = prompt.when || (() => true)
prompt.when = answers => {
return isManualMode(answers) && originalWhen(answers)
}
})
const prompts = [
this.presetPrompt,
this.featurePrompt,
...this.injectedPrompts,
...this.outroPrompts
]
debug('vue-cli:prompts')(prompts)
return prompts
}
在获取完preset
之后,就可以根据preset
对象来决定package.json
需要的依赖。来生成package.json
文件.并判断是否拆分单个配置文件一次生成。生成目标文件是由writeFileTree
方法执行。
// 写入文件到目标文件夹
module.exports = async function writeFileTree (dir, files, previousFiles) {
if (process.env.VUE_CLI_SKIP_WRITE) {
return
}
if (previousFiles) {
await deleteRemovedFiles(dir, files, previousFiles)
}
Object.keys(files).forEach((name) => {
const filePath = path.join(dir, name)
fs.ensureDirSync(path.dirname(filePath))
fs.writeFileSync(filePath, files[name])
})
}
接着通过PackageManager
实例pm调用install
方法下载node_modules
相关依赖。
PackageManager
class PackageManager {
constructor ({ context, forcePackageManager } = {}) {
this.context = context || process.cwd()
this._registries = {}
if (forcePackageManager) {
this.bin = forcePackageManager
} else if (context) {
if (hasProjectYarn(context)) {
this.bin = 'yarn'
} else if (hasProjectPnpm(context)) {
this.bin = 'pnpm'
} else if (hasProjectNpm(context)) {
this.bin = 'npm'
}
}
// if no package managers specified, and no lockfile exists
if (!this.bin) {
this.bin = loadOptions().packageManager || (hasYarn() ? 'yarn' : hasPnpm3OrLater() ? 'pnpm' : 'npm')
}
if (this.bin === 'npm') {
// npm doesn't support package aliases until v6.9
const MIN_SUPPORTED_NPM_VERSION = '6.9.0'
const npmVersion = stripAnsi(execa.sync('npm', ['--version']).stdout)
if (semver.lt(npmVersion, MIN_SUPPORTED_NPM_VERSION)) {
warn(
'You are using an outdated version of NPM.\n' +
'there may be unexpected errors during installation.\n' +
'Please upgrade your NPM version.'
)
this.needsNpmInstallFix = true
}
if (semver.gte(npmVersion, '7.0.0')) {
this.needsPeerDepsFix = true
}
}
if (!SUPPORTED_PACKAGE_MANAGERS.includes(this.bin)) {
log()
warn(
`The package manager ${chalk.red(this.bin)} is ${chalk.red('not officially supported')}.\n` +
`It will be treated like ${chalk.cyan('npm')}, but compatibility issues may occur.\n` +
`See if you can use ${chalk.cyan('--registry')} instead.`
)
PACKAGE_MANAGER_CONFIG[this.bin] = PACKAGE_MANAGER_CONFIG.npm
}
// Plugin may be located in another location if `resolveFrom` presents.
const projectPkg = resolvePkg(this.context)
const resolveFrom = projectPkg && projectPkg.vuePlugins && projectPkg.vuePlugins.resolveFrom
// Logically, `resolveFrom` and `context` are distinct fields.
// But in Vue CLI we only care about plugins.
// So it is fine to let all other operations take place in the `resolveFrom` directory.
if (resolveFrom) {
this.context = path.resolve(context, resolveFrom)
}
}
// Any command that implemented registry-related feature should support
// `-r` / `--registry` option
async getRegistry (scope) {
const cacheKey = scope || ''
if (this._registries[cacheKey]) {
return this._registries[cacheKey]
}
const args = minimist(process.argv, {
alias: {
r: 'registry'
}
})
let registry
if (args.registry) {
registry = args.registry
} else if (!process.env.VUE_CLI_TEST && await shouldUseTaobao(this.bin)) {
registry = registries.taobao
} else {
try {
if (scope) {
registry = (await execa(this.bin, ['config', 'get', scope + ':registry'])).stdout
}
if (!registry || registry === 'undefined') {
registry = (await execa(this.bin, ['config', 'get', 'registry'])).stdout
}
} catch (e) {
// Yarn 2 uses `npmRegistryServer` instead of `registry`
registry = (await execa(this.bin, ['config', 'get', 'npmRegistryServer'])).stdout
}
}
this._registries[cacheKey] = stripAnsi(registry).trim()
return this._registries[cacheKey]
}
async getAuthToken (scope) {
// get npmrc (https://docs.npmjs.com/configuring-npm/npmrc.html#files)
const possibleRcPaths = [
path.resolve(this.context, '.npmrc'),
path.resolve(require('os').homedir(), '.npmrc')
]
if (process.env.PREFIX) {
possibleRcPaths.push(path.resolve(process.env.PREFIX, '/etc/npmrc'))
}
// there's also a '/path/to/npm/npmrc', skipped for simplicity of implementation
let npmConfig = {}
for (const loc of possibleRcPaths) {
if (fs.existsSync(loc)) {
try {
// the closer config file (the one with lower index) takes higher precedence
npmConfig = Object.assign({}, ini.parse(fs.readFileSync(loc, 'utf-8')), npmConfig)
} catch (e) {
// in case of file permission issues, etc.
}
}
}
const registry = await this.getRegistry(scope)
const registryWithoutProtocol = registry
.replace(/https?:/, '') // remove leading protocol
.replace(/([^/])$/, '$1/') // ensure ending with slash
const authTokenKey = `${registryWithoutProtocol}:_authToken`
return npmConfig[authTokenKey]
}
async setRegistryEnvs () {
const registry = await this.getRegistry()
process.env.npm_config_registry = registry
process.env.YARN_NPM_REGISTRY_SERVER = registry
this.setBinaryMirrors()
}
// set mirror urls for users in china
async setBinaryMirrors () {
const registry = await this.getRegistry()
if (registry !== registries.taobao) {
return
}
try {
// node-sass, chromedriver, etc.
const binaryMirrorConfigMetadata = await this.getMetadata('binary-mirror-config', { full: true })
const latest = binaryMirrorConfigMetadata['dist-tags'] && binaryMirrorConfigMetadata['dist-tags'].latest
const mirrors = binaryMirrorConfigMetadata.versions[latest].mirrors.china
for (const key in mirrors.ENVS) {
process.env[key] = mirrors.ENVS[key]
}
// Cypress
const cypressMirror = mirrors.cypress
const defaultPlatforms = {
darwin: 'osx64',
linux: 'linux64',
win32: 'win64'
}
const platforms = cypressMirror.newPlatforms || defaultPlatforms
const targetPlatform = platforms[require('os').platform()]
// Do not override user-defined env variable
// Because we may construct a wrong download url and an escape hatch is necessary
if (targetPlatform && !process.env.CYPRESS_INSTALL_BINARY) {
// We only support cypress 3 for the current major version
const latestCypressVersion = await this.getRemoteVersion('cypress', '^3')
process.env.CYPRESS_INSTALL_BINARY =
`${cypressMirror.host}/${latestCypressVersion}/${targetPlatform}/cypress.zip`
}
} catch (e) {
// get binary mirror config failed
}
}
// https://github.com/npm/registry/blob/master/docs/responses/package-metadata.md
async getMetadata (packageName, { full = false } = {}) {
const scope = extractPackageScope(packageName)
const registry = await this.getRegistry(scope)
const metadataKey = `${this.bin}-${registry}-${packageName}`
let metadata = metadataCache.get(metadataKey)
if (metadata) {
return metadata
}
const headers = {}
if (!full) {
headers.Accept = 'application/vnd.npm.install-v1+json;q=1.0, application/json;q=0.9, */*;q=0.8'
}
const authToken = await this.getAuthToken(scope)
if (authToken) {
headers.Authorization = `Bearer ${authToken}`
}
const url = `${registry.replace(/\/$/g, '')}/${packageName}`
try {
metadata = (await request.get(url, { headers })).body
if (metadata.error) {
throw new Error(metadata.error)
}
metadataCache.set(metadataKey, metadata)
return metadata
} catch (e) {
error(`Failed to get response from ${url}`)
throw e
}
}
async getRemoteVersion (packageName, versionRange = 'latest') {
const metadata = await this.getMetadata(packageName)
if (Object.keys(metadata['dist-tags']).includes(versionRange)) {
return metadata['dist-tags'][versionRange]
}
const versions = Array.isArray(metadata.versions) ? metadata.versions : Object.keys(metadata.versions)
return semver.maxSatisfying(versions, versionRange)
}
getInstalledVersion (packageName) {
// for first level deps, read package.json directly is way faster than `npm list`
try {
const packageJson = loadModule(`${packageName}/package.json`, this.context, true)
return packageJson.version
} catch (e) {}
}
async runCommand (command, args) {
await this.setRegistryEnvs()
return await executeCommand(
this.bin,
[
...PACKAGE_MANAGER_CONFIG[this.bin][command],
...(args || [])
],
this.context
)
}
async install () {
if (process.env.VUE_CLI_TEST) {
try {
process.env.PUPPETEER_SKIP_CHROMIUM_DOWNLOAD = true
await this.runCommand('install', ['--offline', '--silent', '--no-progress'])
delete process.env.PUPPETEER_SKIP_CHROMIUM_DOWNLOAD
} catch (e) {
delete process.env.PUPPETEER_SKIP_CHROMIUM_DOWNLOAD
await this.runCommand('install', ['--silent', '--no-progress'])
}
}
if (this.needsNpmInstallFix) {
// if npm 5, split into several `npm add` calls
// see https://github.com/vuejs/vue-cli/issues/5800#issuecomment-675199729
const pkg = resolvePkg(this.context)
if (pkg.dependencies) {
const deps = Object.entries(pkg.dependencies).map(([dep, range]) => `${dep}@${range}`)
await this.runCommand('install', deps)
}
if (pkg.devDependencies) {
const devDeps = Object.entries(pkg.devDependencies).map(([dep, range]) => `${dep}@${range}`)
await this.runCommand('install', [...devDeps, '--save-dev'])
}
if (pkg.optionalDependencies) {
const devDeps = Object.entries(pkg.devDependencies).map(([dep, range]) => `${dep}@${range}`)
await this.runCommand('install', [...devDeps, '--save-optional'])
}
return
}
return await this.runCommand('install', this.needsPeerDepsFix ? ['--legacy-peer-deps'] : [])
}
async add (packageName, {
tilde = false,
dev = true
} = {}) {
const args = dev ? ['-D'] : []
if (tilde) {
if (this.bin === 'yarn') {
args.push('--tilde')
} else {
process.env.npm_config_save_prefix = '~'
}
}
if (this.needsPeerDepsFix) {
args.push('--legacy-peer-deps')
}
return await this.runCommand('add', [packageName, ...args])
}
async remove (packageName) {
return await this.runCommand('remove', [packageName])
}
async upgrade (packageName) {
// manage multiple packages separated by spaces
const packageNamesArray = []
for (const packname of packageName.split(' ')) {
const realname = stripVersion(packname)
if (
isTestOrDebug &&
(packname === '@vue/cli-service' || isOfficialPlugin(resolvePluginId(realname)))
) {
// link packages in current repo for test
const src = path.resolve(__dirname, `../../../../${realname}`)
const dest = path.join(this.context, 'node_modules', realname)
await fs.remove(dest)
await fs.symlink(src, dest, 'dir')
} else {
packageNamesArray.push(packname)
}
}
if (packageNamesArray.length) return await this.runCommand('add', packageNamesArray)
}
}
这里主要分析PackageManager类的install方法。核心逻辑即使resolvePkg
获取package.json对象。并依次获取dependencies
,devDependencies
,devDependencies
属性,用runCommand
运行命令行下载对应依赖。runCommand
方法依赖的是execa
npm包。
Generator
生成package.json,安装相应依赖之后,就要生成模板文件。这一块逻辑都是有Generator类负责。会生成一个Generator
实例,并调用generate
方法来创建模板文件。这里不过多介绍,后续讲vue-cli插件时再分析。
const generator = new Generator(context, {
pkg,
plugins,
afterInvokeCbs,
afterAnyInvokeCbs
})
await generator.generate({
extractConfigFiles: preset.useConfigFiles
})
最后就是执行所有回调数组。执行git初始化相关操作等,并打印输出相关运行命令。可以参考自定义vue-cli插件。
for (const cb of afterInvokeCbs) {
await cb()
}
for (const cb of afterAnyInvokeCbs) {
await cb()
}
// 创建结束时可以输出自定义的语句。与
generator.printExitLogs()
总结
本文我们了解了vue create <app-name>的整个过程。简单概况就是首先会判断命令行的--preset选项,是否选择预设的preset(存储在home目下的.vuerc
)。或者选择手动模式,则是通过inquirer
与用户的交互来获取完整的preset
对象,再通过拿到的preset
对象来是生成package.json
。然后依据pageage.json
的依赖性下载对应的node_modules
。在最后就是获取模板文件并生成到目标文件夹。并输出结束语句退出。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。