Vue进阶之Vue CLI
Vue CLI
- Vue-cli
- 初步学习
- vue-cli源码解读
- 文件夹功能解读
- bin/vue.js
- lib/create.js create构建原理
- lib/Creator.js Creator类 ——基本都是在处理preset
Vue-cli
初步学习
vue-cli官网
通过 @vue/cli 实现的交互式的项目脚手架。
通过 @vue/cli + @vue/cli-service-global 实现的零配置原型开发。
一个运行时依赖 (@vue/cli-service),该依赖:
可升级;
基于 webpack 构建,并带有合理的默认配置;
可以通过项目内的配置文件进行配置;
可以通过插件进行扩展。
一个丰富的官方插件集合,集成了前端生态中最好的工具。
一套完全图形化的创建和管理 Vue.js 项目的用户界面。
全局安装:
pnpm i -g @vue/cli
或者
npm i -g @vue/cli
用这个命令检查版本是否正确
vue --version
上述的vue --version输出的是vue-cli版本而不是vue的版本的原因是什么呢?通过vue-cli的源代码一起来探究下这个问题吧
vue-cli源码解读
文件夹功能解读
- vue-cli
- packages
- @vue
- cli
- package.json
- cli
- @vue
- packages
bin/vue.js
bin目录一般是我们执行的一个指令,lib目录一般是我们
- vue-cli
- packages
- @vue
- cli
- bin
- vue.js
- bin
- cli
- @vue
- packages
const { chalk, semver } = require('@vue/cli-shared-utils')
const leven = require('leven')
- chalk 是 npm包安装时候的显示颜色
- semver 是 npm包版本的语义化,判断使用的包的版本符不符合预期
- leven 是 用来进行差异化比较的
- cli-shared-utils
- index.js
这些包也是在shared这个文件里引入的,只不过是通过这个入口统一导出的,在中大型项目中,经常使用多个包引入相同的依赖,就可以把这些依赖放入一个文件夹中统一使用
// 比较node的版本 wanted,提供的版本
function checkNodeVersion(wanted, id) {
// process-当前node版本的环境,requiredVersion-就是刚说的package.json中的engines.node的版本
// 比较当前node的版本是否符合wanted的区间
if (!semver.satisfies(process.version, wanted, { includePrerelease: true })) {
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')
- slash 比较目录结构的
// enter debug mode when creating test repo
// 是否进入开发者环境的判断
// slash-比较目录结构的,解决win和mac和linux上面目录的兼容问题
if (
slash(process.cwd()).indexOf('/packages/test') > 0 && (
fs.existsSync(path.resolve(process.cwd(), '../@vue')) ||
fs.existsSync(path.resolve(process.cwd(), '../../@vue'))
)
) {
process.env.VUE_CLI_DEBUG = true
}
- commander 是用来交互式的脚手架,告诉你vue-cli是如何使用的
这段代码使用的是lib下的create的指令
const program = require('commander')
const loadCommand = require('../lib/util/loadCommand')
program //vue-cli的版本-引用的是相对路径下的package.version
.version(`@vue/cli ${require('../package').version}`)
.usage('<command> [options]')
program
.command('create <app-name>') // <app-name> 表示的是变量名称 会传入到下面action的name中,
.description('create a new project powered by vue-cli-service') //以下这些都是指令 -p 简写 --preset 全称,presetName 变量名称,后面是 desc描述 这样就能通过 vue --version能够获取到以下这些指令了
.option('-p, --preset <presetName>', 'Skip prompts and use saved or remote preset')
.option('-d, --default', 'Skip prompts and use default preset')
.option('-i, --inlinePreset <json>', 'Skip prompts and use inline JSON string as preset')
.option('-m, --packageManager <command>', 'Use specified npm client when installing dependencies')
.option('-r, --registry <url>', 'Use specified npm registry when installing dependencies (only for npm)')
.option('-g, --git [message]', 'Force git initialization with initial commit message')
.option('-n, --no-git', 'Skip git initialization')
.option('-f, --force', 'Overwrite target directory if it exists')
.option('--merge', 'Merge target directory if it exists')
.option('-c, --clone', 'Use git clone when fetching remote preset')
.option('-x, --proxy <proxyUrl>', 'Use specified proxy when creating project')
.option('-b, --bare', 'Scaffold project without beginner instructions')
.option('--skipGetStarted', 'Skip displaying "Get started" instructions')
.action((name, options) => {
if (minimist(process.argv.slice(3))._.length > 1) { //通过 minimist 解析用户输入的命令行参数,检查是否提供了多个位置参数(即用户传入的非标志性参数)。如果用户提供了多个位置参数(大于1个),它会在控制台输出一个警告信息,告知用户只有第一个参数会被作为应用的名称,其余参数将被忽略 vue create realName app1 app2 =>只会使用第一个
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
}
//lib下的create的指令 下面讲述
require('../lib/create')(name, options)
})
比如
vue create test -force -registry=‘https://npm.org’
action((name,options)=>{xxx})
name => test
options => options={
force:true,
registry:‘https://npm.org’
}
minimist(process.argv.slice(3))._.length > 1
这句话的作用:
只有app1会被使用为真正的名字
以下这两个命令执行结果都相同
node vue.js
vue -h
这两个效果是一模一样的
以此类推,node vue.js create test 和 vue create test 这两个效果也是一模一样的
这个使用的是lib下的add的指令
program
.command('add <plugin> [pluginOptions]')
.description('install a plugin and invoke its generator in an already created project')
.option('--registry <url>', 'Use specified npm registry when installing dependencies (only for npm)')
.allowUnknownOption()
.action((plugin) => {
require('../lib/add')(plugin, minimist(process.argv.slice(3)))
})
还有后面的inspect,serve,init等等
program.on('--help', () => {
console.log()
console.log(` Run ${chalk.cyan(`vue <command> --help`)} for detailed usage of given command.`)
console.log()
})
这里的使用:
node vue.js -h
执行以上所有脚手架的意思:
program.parse(process.argv)
看一个脚手架,先看脚手架的入口文件(bin的vue)
- vue-cli
- packages
- @vue
- cli
- package.json
- package.json
- cli
- @vue
- packages
lib/create.js create构建原理
- vue-cli
- packages
- @vue
- cli
- lib
- create.js
- lib
- cli
- @vue
- packages
async function create (projectName, options) {
if (options.proxy) {
// 如果有proxy配置,将http的环境变量设置为设定的值
process.env.HTTP_PROXY = options.proxy
}
// 找到当前项目所在的目录
const cwd = options.cwd || process.cwd() //这里的cwd等于win命令pwd
const inCurrent = projectName === '.'
const name = inCurrent ? path.relative('../', cwd) : projectName
// 相当于是在bin目录下
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)
}
// 一般的fs是异步的操作,这里使用的是fs的同步操作,
if (fs.existsSync(targetDir) && !options.merge) {
if (options.force) { //是否强制覆盖原有的目录
await fs.remove(targetDir) //确定则删除原来的目录
} else {
// 非强制覆盖则进行合并
// 这里就是一个文本的console方式 清除终端(控制台)屏幕上的内容,并可选地在屏幕上显示一个标题
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)
}
}
}
}
// 新建creator对象
//name-项目名称或者当前目录名称,targetDir-完整路径:targetDir D:\xxx\....\vue-cli\packages\@vue\cli\bin\test 这种(如果是vue create test)这里是希望根据当前完整路径下创建test文件夹
// getPromptModules() 是否要植入这些插件
const creator = new Creator(name, targetDir, getPromptModules())
//下面讲这个方法
await creator.create(options)
}
module.exports = (...args) => {
//将调用时候传的参数原封不动的传递给create函数
return create(...args).catch(err => {
stopSpinner(false) // do not persist 结束加载
error(err)
if (!process.env.VUE_CLI_TEST) {
process.exit(1)
}
})
}
- validate-npm-package-name 判断名称是否合规的包
- clearConsole 清除终端(控制台)屏幕上的内容,并可选地在屏幕上显示一个标题
核心:
exports.clearConsole = title => {
//判断当前是否在终端环境中
if (process.stdout.isTTY) {
//清空整个终端屏幕的内容(通过输出换行符并清除屏幕)
const blank = '\n'.repeat(process.stdout.rows)
console.log(blank)
//将光标移动到屏幕的左上角
readline.cursorTo(process.stdout, 0, 0)
readline.clearScreenDown(process.stdout)
if (title) {
//如果传入了标题参数,则在屏幕顶部输出该标题
console.log(title)
}
}
}
- inquirer 的使用 一种在命令行中交互的方式
一种使用场景
另一种情况:
- getPromptModules 是否要植入这些插件
// 新建creator对象
const creator = new Creator(name, targetDir, getPromptModules())
await creator.create(options)
针对于所有文件,读取的是promptModules目录下对应的文件
- stopSpinner 和 ora
stopSpinner 就是 结束加载的意思
loading的加载动画就是ora
vue-cli/packages/@vue/cli-shared-utils/lib/spinner.js
const ora = require('ora')
const chalk = require('chalk')
const spinner = ora()
let lastMsg = null
let isPaused = false
//开始加载
exports.logWithSpinner = (symbol, msg) => {
if (!msg) {
msg = symbol
symbol = chalk.green('✔')
}
if (lastMsg) {
spinner.stopAndPersist({
symbol: lastMsg.symbol,
text: lastMsg.text
})
}
spinner.text = ' ' + msg
lastMsg = {
symbol: symbol + ' ',
text: msg
}
spinner.start()
}
//结束加载
exports.stopSpinner = (persist) => {
if (!spinner.isSpinning) {
return
}
if (lastMsg && persist !== false) {
spinner.stopAndPersist({
symbol: lastMsg.symbol,
text: lastMsg.text
})
} else {
spinner.stop()
}
lastMsg = null
}
//中止加载的方法
exports.pauseSpinner = () => {
if (spinner.isSpinning) {
spinner.stop()
isPaused = true
}
}
exports.resumeSpinner = () => {
if (isPaused) {
spinner.start()
isPaused = false
}
}
exports.failSpinner = (text) => {
spinner.fail(text)
}
// silent all logs except errors during tests and keep record
if (process.env.VUE_CLI_TEST) {
require('./_silence')('spinner', exports)
}
lib/Creator.js Creator类 ——基本都是在处理preset
- vue-cli
- packages
- @vue
- cli
- lib
- Creator.js
- lib
- cli
- @vue
- packages
- 在preset中植入插件
// clone before mutating 对preset进行复制
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
}
}
// legacy support for vuex
if (preset.vuex) {
preset.plugins['@vue/cli-plugin-vuex'] = {}
}
// 打印输出preset
console.log("preset is", preset);
process.exit(1)
- 判断使用哪个包管理工具
目的就是判断当前某个包管理工具有没有
const packageManager = (
cliOptions.packageManager ||
loadOptions().packageManager ||
(hasYarn() ? 'yarn' : null) ||
(hasPnpm3OrLater() ? 'pnpm' : 'npm') //pnpm版本是否比3大,大于3就用pnpm,否则用npm
)
loadOptions:
exports.loadOptions = () => {
if (cachedOptions) {
return cachedOptions
}
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 {}
hasYarn:
exports.hasYarn = () => {
if (process.env.VUE_CLI_TEST) { //vue-cli的测试环境就默认使用hasYarn
return true
}
if (_hasYarn != null) {
return _hasYarn
}
try {
//execSync就是执行脚本,执行成功hasYarn为true,否则就为false
execSync('yarn --version', { stdio: 'ignore' })
return (_hasYarn = true)
} catch (e) {
return (_hasYarn = false)
}
}
hasPnpm3OrLater:
判断pnpm版本
exports.hasPnpmVersionOrLater = (version) => {
if (process.env.VUE_CLI_TEST) {
return true
}
//获得pnpm版本后,与version版本进行比较 g-greater 大于,e-equal 等于 => gte:大于等于
return semver.gte(getPnpmVersion(), version)
}
exports.hasPnpm3OrLater = () => {
//判断当前版本是否比3.0.0版本大
return this.hasPnpmVersionOrLater('3.0.0')
}
function getPnpmVersion () {
if (_pnpmVersion != null) {
return _pnpmVersion
}
try {
//执行脚本pnpm --version得到pnpm版本
_pnpmVersion = execSync('pnpm --version', {
stdio: ['pipe', 'pipe', 'ignore']
}).toString()
// there's a critical bug in pnpm 2
// https://github.com/pnpm/pnpm/issues/1678#issuecomment-469981972
// so we only support pnpm >= 3.0.0
_hasPnpm = true
} catch (e) {}
return _pnpmVersion || '0.0.0'
}
- 清除控制台
await clearConsole()
exports.clearConsole = async function clearConsoleWithTitle (checkUpdate) {
const title = await exports.generateTitle(checkUpdate)
clearConsole(title)
}
- PackageManager - cli默认创建的包管理工具
const pm = new PackageManager({ context, forcePackageManager: packageManager })
通过一个类最终创建package.json的一个效果
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)) {
throw new Error(
'You are using an outdated version of NPM.\n' +
'It does not support some core functionalities of Vue CLI.\n' +
'Please upgrade your NPM version.'
)
}
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 getAuthConfig (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`
const authUsernameKey = `${registryWithoutProtocol}:username`
const authPasswordKey = `${registryWithoutProtocol}:_password`
const auth = {}
if (authTokenKey in npmConfig) {
auth.token = npmConfig[authTokenKey]
}
if (authPasswordKey in npmConfig) {
auth.username = npmConfig[authUsernameKey]
auth.password = Buffer.from(npmConfig[authPasswordKey], 'base64').toString()
}
return auth
}
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 {
// 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) {
const projectPkg = resolvePkg(this.context)
if (projectPkg && projectPkg.devDependencies && projectPkg.devDependencies.cypress) {
const wantedCypressVersion = await this.getRemoteVersion('cypress', projectPkg.devDependencies.cypress)
process.env.CYPRESS_INSTALL_BINARY =
`${cypressMirror.host}/${wantedCypressVersion}/${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 authConfig = await this.getAuthConfig(scope)
if ('password' in authConfig) {
const credentials = Buffer.from(`${authConfig.username}:${authConfig.password}`).toString('base64')
headers.Authorization = `Basic ${credentials}`
}
if ('token' in authConfig) {
headers.Authorization = `Bearer ${authConfig.token}`
}
const url = `${registry.replace(/\/$/g, '')}/${packageName}`
try {
metadata = (await request.get(url, { headers }))
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) {
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
}
}
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)
}
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)
}
}
使用 node vue.js create test 命令输出以下结果
- 获取cli版本号
// get latest CLI plugin version
const { latestMinor } = await getVersions()
获取cli文件下的package.json里的version版本
module.exports = async function getVersions () {
if (sessionCached) {
return sessionCached
}
let latest
// 获取cli文件下的package.json里的version版本
const local = require(`../../package.json`).version
if (process.env.VUE_CLI_TEST || process.env.VUE_CLI_DEBUG) {
return (sessionCached = {
current: local,
latest: local,
latestMinor: local
})
}
// should also check for prerelease versions if the current one is a prerelease
const includePrerelease = !!semver.prerelease(local)
const { latestVersion = local, lastChecked = 0 } = loadOptions()
//上次版本判断的时间
const cached = latestVersion
//当前的时间-上次判断时间
const daysPassed = (Date.now() - lastChecked) / (60 * 60 * 1000 * 24)
let error
//当前的时间比上次判断时间大于1天的情况
if (daysPassed > 1) {
// if we haven't check for a new version in a day, wait for the check
// before proceeding
// 要进行版本判断
try {
latest = await getAndCacheLatestVersion(cached, includePrerelease)
} catch (e) {
latest = cached
error = e
}
} else {
// Otherwise, do a check in the background. If the result was updated,
// it will be used for the next 24 hours.
// don't throw to interrupt the user if the background check failed
getAndCacheLatestVersion(cached, includePrerelease).catch(() => {})
latest = cached
}
// if the installed version is updated but the cache doesn't update
if (semver.gt(local, latest) && !semver.prerelease(local)) {
latest = local
}
let latestMinor = `${semver.major(latest)}.${semver.minor(latest)}.0`
if (
// if the latest version contains breaking changes
/major/.test(semver.diff(local, latest)) ||
// or if using `next` branch of cli
(semver.gte(local, latest) && semver.prerelease(local))
) {
// fallback to the local cli version number
latestMinor = local
}
return (sessionCached = {
current: local,
latest,
latestMinor,
error
})
}
- 拿着获取到的版本号创建package.json文件
// 拿着获取到的版本号创建package.json文件
// generate package.json with plugin dependencies
const pkg = {
name,
version: '0.1.0',
private: true,
devDependencies: {},
...resolvePkg(context)
}
这里的pkg和刚刚创建的test的项目中的package.json的内容是对应着的
- 下来就是npm的包的一些适配的过程,最终返回的是package.json,
这个pkg就是最终我们需要注入到生成的package.json中的内容,
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 ? `latest` : `~${latestMinor}`
} else {
version = 'latest'
}
}
pkg.devDependencies[dep] = version
- 通过一个tree的方式写文件
转成树的方式写package.json文件
await writeFileTree(context, {
'package.json': JSON.stringify(pkg, null, 2) //2代表每行开始有2个空格的缩进
})
- 判断包的版本是哪些,是哪些的话就要创建对应的文件,
// generate a .npmrc file for pnpm, to persist the `shamefully-flatten` flag
//如果包是pnpm的话
if (packageManager === 'pnpm') {
const pnpmConfig = hasPnpmVersionOrLater('4.0.0')
// pnpm v7 makes breaking change to set strict-peer-dependencies=true by default, which may cause some problems when installing
? 'shamefully-hoist=true\nstrict-peer-dependencies=false\n'
: 'shamefully-flatten=true\n'
//创建.npmrc文件作为pnpm包的配置
await writeFileTree(context, {
'.npmrc': pnpmConfig
})
}
- 是否需要执行一个git
//是否需要添加git
const shouldInitGit = this.shouldInitGit(cliOptions)
if (shouldInitGit) {
//如果需要一个git,则会创建一个git的目录
log(`🗃 Initializing git repository...`)
this.emit('creation', { event: 'git-init' })
// 这里的run就是execa,也就是child_process.spawn 用子进程去执行
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 {
//如果是pnpm的话,就执行 pnpm install
await pm.install()
}
- 执行generator
创建一个Generator的实例
// 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()
}
- 然后创建README.md文件
if (!generator.files['README.md']) {
// generate README.md
log()
log('📄 Generating README.md...')
await writeFileTree(context, {
'README.md': generateReadme(generator.pkg, packageManager)
})
}
- 是否需要执行git
进行一系列git的配置
// 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
}
}
- 项目创建成功,创建成功后就可以执行pnpm run serve
// 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
}
}
- gitCommit报错的话,提醒要填写username和email
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`
)
}
- 以上所有操作都要通过run来执行,run中通过execa包来执行的
run (command, args) {
if (!args) { [command, ...args] = command.split(/\s+/) }
return execa(command, args, { cwd: this.context })
}
上述内容的对应执行过程如下:
脚手架和node_modules的关系:
脚手架创建的只是最初的那一点代码
创建完脚手架之后执行 pnpm install 才能安装node_modules
是否需要安装node_modules取决于是否希望给用户将其所有的代码依赖全都安装好
cli:commander+inquirer commander:cmd输入命令,inquirer:交互式选择器 通过问答的方式获取的是 options 然后去消费options,添加代码模板