当前位置: 首页 > article >正文

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

在这里插入图片描述

bin/vue.js

bin目录一般是我们执行的一个指令,lib目录一般是我们

  • vue-cli
    • packages
      • @vue
        • cli
          • bin
            • vue.js
const { chalk, semver } = require('@vue/cli-shared-utils')
const leven = require('leven')
  1. chalk 是 npm包安装时候的显示颜色
  2. semver 是 npm包版本的语义化,判断使用的包的版本符不符合预期
  3. 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')
  1. 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
}
  1. 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
            在这里插入图片描述

lib/create.js create构建原理

  • vue-cli
    • packages
      • @vue
        • cli
          • lib
            • create.js
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)
    }
  })
}
  1. validate-npm-package-name 判断名称是否合规的包
  2. 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)
    }
  }
}
  1. inquirer 的使用 一种在命令行中交互的方式
    一种使用场景
    在这里插入图片描述
    在这里插入图片描述
    另一种情况:

在这里插入图片描述

  1. getPromptModules 是否要植入这些插件
// 新建creator对象
  const creator = new Creator(name, targetDir, getPromptModules())
  await creator.create(options)

针对于所有文件,读取的是promptModules目录下对应的文件
在这里插入图片描述

  1. 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
  1. 在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)

在这里插入图片描述

在这里插入图片描述

  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'
}
  1. 清除控制台
await clearConsole()
exports.clearConsole = async function clearConsoleWithTitle (checkUpdate) {
  const title = await exports.generateTitle(checkUpdate)
  clearConsole(title)
}
  1. 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 命令输出以下结果

在这里插入图片描述

  1. 获取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
  })
}
  1. 拿着获取到的版本号创建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的内容是对应着的
在这里插入图片描述

  1. 下来就是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
  1. 通过一个tree的方式写文件
    转成树的方式写package.json文件
await writeFileTree(context, {
      'package.json': JSON.stringify(pkg, null, 2)  //2代表每行开始有2个空格的缩进
    })
  1. 判断包的版本是哪些,是哪些的话就要创建对应的文件,
// 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
      })
    }
  1. 是否需要执行一个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')
    }
  1. 安装插件
// 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()
    }
  1. 执行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
    })
  1. 创建对应的依赖,然后执行安装
// 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()
    }
  1. 然后创建README.md文件
 if (!generator.files['README.md']) {
      // generate README.md
      log()
      log('📄  Generating README.md...')
      await writeFileTree(context, {
        'README.md': generateReadme(generator.pkg, packageManager)
      })
    }
  1. 是否需要执行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
      }
    }
  1. 项目创建成功,创建成功后就可以执行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
      }
    }
  1. 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`
      )
    }
  1. 以上所有操作都要通过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,添加代码模板


http://www.kler.cn/a/412443.html

相关文章:

  • PIMPL模式和D指针
  • Java后端如何进行文件上传和下载 —— 本地版
  • 【DL笔记】神经网络轻量化(CV方向)的一些论文记录
  • 【漏洞复现】CVE-2020-13925
  • 路由器中继与桥接
  • 0BB定位胶具有优异的焊带粘接性和可靠性,助力客户降本增效
  • 云原生后端开发:构建现代化可扩展的服务
  • 20241127 给typecho文章编辑附件 添加视频 图片预览
  • HTTPS的单向认证和双向认证是什么?有什么区别?
  • Flink 中 JDBC Connector 使用详解
  • WPF ItemsControl控件
  • 【深度学习|目标跟踪】StrongSort 详解(以及StrongSort++)
  • 浏览器缓存与协商缓存
  • 深入理解HTML基本结构:构建现代网页的基石
  • CSDN设置成黑色背景(谷歌 Edge)
  • 手机实时提取SIM卡打电话的信令声音-智能拨号器的双SIM卡切换方案
  • JS-对象-05-DOM
  • vue页面成绩案例(for渲染表格/删除/添加/统计总分/平均分/不及格显红色/输入内容去首尾空格trim/输入内容转数字number)
  • <<WTF-Solidity>>学习笔记(part 9-12)
  • 减速电机的减速比是什么意思?
  • 软件测试丨Pytest 第三方插件与 Hook 函数
  • ffmpeg 预设的值 加速
  • git源码安装
  • 集合卡尔曼滤波(EnKF)的三维滤波(模拟平面定位)例程,带逐行注释
  • Docker容器运行CentOS镜像,执行yum命令提示“Failed to set locale, defaulting to C.UTF-8”
  • mysql window安装(学习使用)