vue-cli3.0源碼分析@vue/cli-----create

本文主要學習vue-cli3.0的源碼的記錄。源碼地址: https://github.com/vuejs/vue-cli
主要對packages裏面的@vue進行學習。以下圖

clipboard.png
在圖中咱們能夠看到vue-cli中,不只僅有初始化工程,還有許多通用的工具、插件。接下來咱們就對這些插件進行學習。css

首先咱們來看cli的目錄:
首先來看package.jsonhtml

{
    "name": "@vue/cli", // 名稱
  "version": "3.5.5", // 版本號
    "bin": {
        "vue": "bin/vue.js"
      }, // 這個是用於命令窗口執行的命令;若是是全局安裝了,那麼vue就是一個命令值 vue xxxx
"engines": {
    "node": ">=8.9"
  } // 須要的node版本號
}

咱們如今咱們能夠去看bin/vue.js文件,對該文件給出註釋,方便閱讀vue

#!/usr/bin/env node
// 這邊備註是node來解析, 固然若是不寫也沒事
// Check node version before requiring/doing anything else
// The user may be on a very old node version
const chalk = require('chalk') // 用於輸出有色彩
const semver = require('semver') // 用於比較版本號
const requiredVersion = require('../package.json').engines.node // 獲取node版本號要求
// 檢測node的版本號,若是不符合要求就給提示
function checkNodeVersion (wanted, id) {
  if (!semver.satisfies(process.version, wanted)) { // process.version表示當前node版本
    console.log(chalk.red(
      'You are using Node ' + process.version + ', but this version of ' + id +
      ' requires Node ' + wanted + '.\nPlease upgrade your Node version.'
    )) // 給出當前vue-cli須要的版本爲多少
    process.exit(1)
  }
}

checkNodeVersion(requiredVersion, 'vue-cli')

if (semver.satisfies(process.version, '9.x')) {
  console.log(chalk.red(
    `You are using Node ${process.version}.\n` +
    `Node.js 9.x has already reached end-of-life and will not be supported in future major releases.\n` +
    `It's strongly recommended to use an active LTS version instead.`
  ))
} // 若是node爲9.x那麼給出相應的提示

const fs = require('fs') // 文件
const path = require('path') // 路徑
const slash = require('slash') // 用於轉換 Windows 反斜槓路徑轉換爲正斜槓路徑 \ => /
const minimist = require('minimist') // 用來解析從參數

// enter debug mode when creating test repo
if (
  slash(process.cwd()).indexOf('/packages/test') > 0 && ( // process.cwd()爲當前絕對路徑,如F:\packages\@vue\cli\bin
    fs.existsSync(path.resolve(process.cwd(), '../@vue')) ||
    fs.existsSync(path.resolve(process.cwd(), '../../@vue'))
  )
) {
  process.env.VUE_CLI_DEBUG = true
}

const program = require('commander') // node對話,輸入
const loadCommand = require('../lib/util/loadCommand') // 用於查找模塊

program
  .version(require('../package').version)
  .usage('<command> [options]')

上述是一些檢測的代碼。
以後就要開始交互式的命令了。node

1.create入口

咱們能夠看到program.command就是建立的一個命令,後面會有不少的命令create,add,invoke等等,這一節主要來說解creategit

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('-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('-c, --clone', 'Use git clone when fetching remote preset')
  .option('-x, --proxy', 'Use specified proxy when creating project')
  .option('-b, --bare', 'Scaffold project without beginner instructions')
  .option('--skipGetStarted', 'Skip displaying "Get started" instructions')
  .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)
  })

這邊建立了一個command('create <app-name>'),若是是全局安裝了@vue-cli3.0,那麼就可使用github

vue create xxxx yyy

xxx yyy爲文件名稱和option這些參數配置項。
咱們來看一下分別有哪些配置項:vuex

-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                      輸出使用幫助信息

在action中就是進入命令後的執行代碼,如下部分主要就是提取-g命令,vue-cli

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
    }

2.基礎信息驗證

接下來就是進入create.js文件typescript

async function create (projectName, options) {
  // 代理使用 -x 或--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)
  }
  // 檢測文件是否存在,
  if (fs.existsSync(targetDir)) {
    if (options.force) {
      await fs.remove(targetDir)
      // 這邊強制覆蓋
    } else {
      await clearConsole()
      if (inCurrent) {
        // 這邊提示是否在當前文件建立?
        const { ok } = await inquirer.prompt([
          {
            name: 'ok',
            type: 'confirm',
            message: `Generate project in current directory?`
          }
        ])
        if (!ok) {
          return
        }
      } else {
        // 文件已重複
        const { action } = await inquirer.prompt([
          {
            name: 'action',
            type: 'list',
            message: `Target directory ${chalk.cyan(targetDir)} already exists. Pick an action:`,
            choices: [
              { name: 'Overwrite', value: 'overwrite' },
              { name: 'Merge', value: 'merge' },
              { name: 'Cancel', value: false }
            ]
          }
        ])
        if (!action) {
          return
        } else if (action === 'overwrite') {
          console.log(`\nRemoving ${chalk.cyan(targetDir)}...`)
          await fs.remove(targetDir)
        }
      }
    }
  }
  // 新建構造器
  const creator = new Creator(name, targetDir, getPromptModules()) // getPromptModules()爲內置插件對話對象
  await creator.create(options)
}

以上大部分都是定義文件,目錄和一名稱效驗,文件效驗,比較簡單易懂,接下來就是建立Creator構造器了npm

3.Creator構造器

這一節的內容會比較繞一點。
首先咱們先來了解一下vue-cli-preset,這是一個包含建立新項目所需預約義選項和插件的 JSON 對象,讓用戶無需在命令提示中選擇它們。
vue create 過程當中保存的 preset 會被放在你的用戶目錄下的一個配置文件中 (~/.vuerc)。你能夠經過直接編輯這個文件來調整、添加、刪除保存好的 preset。這裏有一個 preset 的示例:

{
  "useConfigFiles": true,
  "router": true,
  "vuex": true,
  "cssPreprocessor": "sass",
  "plugins": {
    "@vue/cli-plugin-babel": {},
    "@vue/cli-plugin-eslint": {
      "config": "airbnb",
      "lintOn": ["save", "commit"]
    }
  }
}

Preset 的數據會被插件生成器用來生成相應的項目文件。除了上述這些字段,你也能夠爲集成工具添加配置:

{
  "useConfigFiles": true,
  "plugins": {...},
  "configs": {
    "vue": {...},
    "postcss": {...},
    "eslintConfig": {...},
    "jest": {...}
  }
}

這些額外的配置將會根據 useConfigFiles 的值被合併到 package.json 或相應的配置文件中。例如,當 "useConfigFiles": true 的時候,configs 的值將會被合併到 vue.config.js 中。

更多關於 preset 能夠前往 vue-cli 官網 插件和 Preset https://cli.vuejs.org/zh/guid...

在基礎驗證完後會建立一個Creator實例

const creator = new Creator(name, targetDir, getPromptModules())

3.1getPromptModules

在分析Creator以前,咱們先來看一下getPromptModules是什麼。getPromptModules源碼

exports.getPromptModules = () => {
  return [
    'babel',
    'typescript',
    'pwa',
    'router',
    'vuex',
    'cssPreprocessors',
    'linter',
    'unit',
    'e2e'
  ].map(file => require(`../promptModules/${file}`))
}

咱們能夠在promptModules中分別看到

clipboard.png

其中好比unit.js:

module.exports = cli => {
  cli.injectFeature({
    name: 'Unit Testing',
    value: 'unit',
    short: 'Unit',
    description: 'Add a Unit Testing solution like Jest or Mocha',
    link: 'https://cli.vuejs.org/config/#unit-testing',
    plugins: ['unit-jest', 'unit-mocha']
  })

  cli.injectPrompt({
    name: 'unit',
    when: answers => answers.features.includes('unit'),
    type: 'list',
    message: 'Pick a unit testing solution:',
    choices: [
      {
        name: 'Mocha + Chai',
        value: 'mocha',
        short: 'Mocha'
      },
      {
        name: 'Jest',
        value: 'jest',
        short: 'Jest'
      }
    ]
  })

  cli.onPromptComplete((answers, options) => {
    if (answers.unit === 'mocha') {
      options.plugins['@vue/cli-plugin-unit-mocha'] = {}
    } else if (answers.unit === 'jest') {
      options.plugins['@vue/cli-plugin-unit-jest'] = {}
    }
  })
}

咱們能夠看到這部其實就是對一些內置插件的一些配置項,用於對話後來進行安裝,

cli.injectFeature:是用來注入featurePrompt,即初始化項目時,選擇的babel、typescript等
cli.injectPrompt:是根據選擇的 featurePrompt 而後注入對應的 prompt,當選擇了 unit,接下來會有如下的 prompt,選擇 Mocha + Chai 仍是 Jest
cli.onPromptComplete: 就是一個回調,會根據選擇來添加對應的插件, 當選擇了 mocha ,那麼就會添加 @vue/cli-plugin-unit-mocha 插件

3.2 new Creator

接下來咱們來看一下其構造函數

constructor (name, context, promptModules) {
    super()

    this.name = name // 目錄名稱
    this.context = process.env.VUE_CLI_CONTEXT = context // 當前目錄
    const { presetPrompt, featurePrompt } = this.resolveIntroPrompts() // 以前預製的插件,和項目的一些feature
    this.presetPrompt = presetPrompt
    this.featurePrompt = featurePrompt
    this.outroPrompts = this.resolveOutroPrompts() // 其餘的插件
    this.injectedPrompts = [] // 當選擇featurePrompt時,注入的prompts
    this.promptCompleteCbs = []
    this.createCompleteCbs = []

    this.run = this.run.bind(this)

    const promptAPI = new PromptModuleAPI(this)
    promptModules.forEach(m => m(promptAPI))
  }

上述代碼咱們主要來看一下PromptModuleAPI,其餘都是一些變量初始化的定義

module.exports = 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)
  }
}

這邊是建立一個PromptModuleAPI實例,並經過promptModules.forEach(m => m(promptAPI)),將預設的內置插件加入到
this.creator.featurePrompt,this.creator.injectedPrompts和this.creator.promptCompleteCbs中

3.3getPreset

在建立了Creator實例後,而後調用了create方法

await creator.create(options)

create方法源碼,這段代碼比較簡單,主要是判斷是否有-p,-d,-i的配置項來直接安裝,若是沒有的話,就進入對話模式this.promptAndResolvePreset,來選擇性的安裝

const isTestOrDebug = process.env.VUE_CLI_TEST || process.env.VUE_CLI_DEBUG
    const { run, name, context, createCompleteCbs } = this

    if (!preset) {
      // 是否存在 -p 或--preset
      if (cliOptions.preset) {
        // vue create foo --preset bar
        preset = await this.resolvePreset(cliOptions.preset, cliOptions.clone)
      } else if (cliOptions.default) {
        // 是否有-d或--default的命令,若是有則,默認直接安裝
        // vue create foo --default
        preset = defaults.presets.default
      } else if (cliOptions.inlinePreset) {
        // 是否有--inlinePreset或-i來注入插件
        // 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()
      }
    }

先判斷 vue create 命令是否帶有 -p 選項,若是有的話會調用 resolvePreset 去解析 preset。resolvePreset 函數會先獲取 ~/.vuerc 中保存的 preset, 而後進行遍歷,若是裏面包含了 -p 中的 <presetName>,則返回~/.vuerc 中的 preset。若是沒有則判斷是不是採用內聯的 JSON 字符串預設選項,若是是就會解析 .json 文件,並返回 preset,還有一種狀況就是從遠程獲取 preset(利用 download-git-repo 下載遠程的 preset.json)並返回。

上面的狀況是當 vue create 命令帶有 -p 選項的時候纔會執行,若是沒有就會調用 promptAndResolvePreset 函數利用 inquirer.prompt 以命令後交互的形式來獲取 preset,下面看下 promptAndResolvePreset 函數的源碼:

async promptAndResolvePreset (answers = null) {
    // prompt
    if (!answers) {
      await clearConsole(true)
      answers = await inquirer.prompt(this.resolveFinalPrompts()) 
      // 交互式命令對話,安裝defalut和Manually select features
    }
    debug('vue-cli:answers')(answers)

    if (answers.packageManager) {
      saveOptions({
        packageManager: answers.packageManager
      })
    }

    let preset
    if (answers.preset && answers.preset !== '__manual__') { // 若是是選擇本地保存的preset(.vuerc)
      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)
    }

    debug('vue-cli:preset')(preset)
    return preset
  }

看到這裏會比較亂的,preset會比較多,咱們再來看一下resolveFinalPrompts源碼

resolveFinalPrompts () {
    // patch generator-injected prompts to only show in manual mode
    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)
    console.log(1, prompts)
    return prompts
  }

這裏咱們能夠看到presetPrompt, featurePrompt, injectedPrompts, outroPrompts 合併成一個數組進行返回,
presetPrompt是預設的,當上一次選擇manually模式進行了預設,並保存到.vuerc中,那麼初始化的時候會列出已保存的插件
featurePrompt就是內置的一些插件
injectedPrompts是經過-i命令來手動注入的插件
outroPrompts是一些其餘的插件。
這邊對話完以後,就要開始依賴的安裝了。

4依賴安裝

咱們繼把create中的代碼往下走

const packageManager = (
      cliOptions.packageManager ||
      loadOptions().packageManager ||
      (hasYarn() ? 'yarn' : 'npm')
    )

    await clearConsole() // 清空控制檯
    logWithSpinner(`✨`, `Creating project in ${chalk.yellow(context)}.`)
    this.emit('creation', { event: 'creating' })

    // get latest CLI version
    const { latest } = await getVersions()
    const latestMinor = `${semver.major(latest)}.${semver.minor(latest)}.0`
    // generate package.json with plugin dependencies
    const pkg = {
      name,
      version: '0.1.0',
      private: true,
      devDependencies: {}
    }
    const deps = Object.keys(preset.plugins)
    deps.forEach(dep => {
      if (preset.plugins[dep]._isPreset) {
        return
      }

      // Note: the default creator includes no more than `@vue/cli-*` & `@vue/babel-preset-env`,
      // so it is fine to only test `@vue` prefix.
      // Other `@vue/*` packages' version may not be in sync with the cli itself.
      pkg.devDependencies[dep] = (
        preset.plugins[dep].version ||
        ((/^@vue/.test(dep)) ? `^${latestMinor}` : `latest`)
      )
    })
    // write package.json
    await writeFileTree(context, {
      'package.json': JSON.stringify(pkg, null, 2)
    })

這邊主要就是獲取cli的版本和生產package.json,其中主要是獲取版本號

module.exports = async function getVersions () {
  if (sessionCached) {
    return sessionCached
  }

  let latest
  const local = require(`../../package.json`).version
  if (process.env.VUE_CLI_TEST || process.env.VUE_CLI_DEBUG) {
    return (sessionCached = {
      current: local,
      latest: local
    })
  }

  const { latestVersion = local, lastChecked = 0 } = loadOptions()
  const cached = latestVersion
  const daysPassed = (Date.now() - lastChecked) / (60 * 60 * 1000 * 24)

  if (daysPassed > 1) { // 距離上次檢查更新超過一天
    // if we haven't check for a new version in a day, wait for the check
    // before proceeding
    latest = await getAndCacheLatestVersion(cached)
  } else {
    // Otherwise, do a check in the background. If the result was updated,
    // it will be used for the next 24 hours.
    getAndCacheLatestVersion(cached) // 後臺更新
    latest = cached
  }

  return (sessionCached = {
    current: local,
    latest
  })
}

// fetch the latest version and save it on disk
// so that it is available immediately next time
async function getAndCacheLatestVersion (cached) {
  const getPackageVersion = require('./getPackageVersion')
  const res = await getPackageVersion('vue-cli-version-marker', 'latest')
  if (res.statusCode === 200) {
    const { version } = res.body
    if (semver.valid(version) && version !== cached) {
      saveOptions({ latestVersion: version, lastChecked: Date.now() })
      return version
    }
  }
  return cached
}

這邊主要是有2個版本變量,一個是local本地cli版本。另外一個laset遠程cli版本
另外getAndCacheLatestVersion而是經過 vue-cli-version-marker npm 包獲取的 CLI 版本。
生產package.json以後,咱們在繼續看後面代碼

const shouldInitGit = this.shouldInitGit(cliOptions)
    if (shouldInitGit) {
      logWithSpinner(`🗃`, `Initializing git repository...`)
      this.emit('creation', { event: 'git-init' })
      await run('git init')
    }

    // install plugins
    stopSpinner()
    log(`⚙  Installing CLI plugins. This might take a while...`)
    log()
    this.emit('creation', { event: 'plugins-install' })
    if (isTestOrDebug) {
      // in development, avoid installation process
      await require('./util/setupDevProject')(context)
    } else {
      await installDeps(context, packageManager, cliOptions.registry)
    }

這段代碼首先會調用shouldInitGit來判斷是否須要git初始化,判斷的情景是:
是否安裝了git;命令中是否有-g或--git,或--no-git或-n;生成的目錄是否包含了git
判斷完以後須要git初始化項目後,接下來就會調用installDeps來安裝依賴

exports.installDeps = async function installDeps (targetDir, command, cliRegistry) {
  const args = []
  if (command === 'npm') {
    args.push('install', '--loglevel', 'error')
  } else if (command === 'yarn') {
    // do nothing
  } else {
    throw new Error(`Unknown package manager: ${command}`)
  }

  await addRegistryToArgs(command, args, cliRegistry)

  debug(`command: `, command) // DEBUG=vue-cli:install vue create demo
  debug(`args: `, args)

  await executeCommand(command, args, targetDir)
}

5 Generator

在下載完依賴以後就會resolvePlugins,其做用就是加載每一個插件的generator,而且若是插件須要進行命令式交互的話,會執行inquirer.prompt獲取option。

async resolvePlugins (rawPlugins) {
    // ensure cli-service is invoked first
    rawPlugins = sortObject(rawPlugins, ['@vue/cli-service'], true)
    const plugins = []
    for (const id of Object.keys(rawPlugins)) {
      const apply = loadModule(`${id}/generator`, this.context) || (() => {})
      let options = rawPlugins[id] || {}
      if (options.prompts) {
        const prompts = loadModule(`${id}/prompts`, this.context)
        if (prompts) {
          log()
          log(`${chalk.cyan(options._isPreset ? `Preset options:` : id)}`)
          options = await inquirer.prompt(prompts)
        }
      }
      plugins.push({ id, apply, options })
    }
    return plugins
  }

根據vue-cli插件規範,差價內部是須要配置generator, prompts選填,index.js必填。具體查看官網插件的開發文檔。
因此resolvePlugins這邊就是對這些插件進行解析。若是不是按照官網的規範,那麼這邊就不能解析正確了。這一步中全部的交互式對話都會完成。
以後就開始實例化Generator,把解析的插件傳入

const generator = new Generator(context, {
      pkg,
      plugins,
      completeCbs: createCompleteCbs
    })

在來看一下其構造函數:

constructor (context, {
    pkg = {},
    plugins = [],
    completeCbs = [],
    files = {},
    invoking = false
  } = {}) {
    this.context = context
    this.plugins = plugins
    this.originalPkg = pkg
    this.pkg = Object.assign({}, pkg)
    this.imports = {}
    this.rootOptions = {}
    this.completeCbs = completeCbs
    this.configTransforms = {} // 插件經過 GeneratorAPI 暴露的 addConfigTransform 方法添加如何提取配置文件
    this.defaultConfigTransforms = defaultConfigTransforms // 默認的配置文件
    this.reservedConfigTransforms = reservedConfigTransforms // 保留的配置文件
    this.invoking = invoking
    // for conflict resolution
    this.depSources = {}
    // virtual file tree
    this.files = files
    this.fileMiddlewares = []
    this.postProcessFilesCbs = []
    // exit messages
    this.exitLogs = []

    const cliService = plugins.find(p => p.id === '@vue/cli-service')
    const rootOptions = cliService
      ? cliService.options
      : inferRootOptions(pkg)
    // apply generators from plugins
    plugins.forEach(({ id, apply, options }) => {
      // 每一個插件對應生成一個 GeneratorAPI 實例,並將實例 api 傳入插件暴露出來的 generator 函數
      const api = new GeneratorAPI(id, this, options, rootOptions)
      apply(api, options, rootOptions, invoking)
    })
  }

這邊主要就是聲明一些變量,並建立每一個插件的GeneratorAPI 。咱們來看一下GeneratorAPI,GeneratorAPI 是比較重要的模塊,若是插件須要自定義項目模板、修改模塊該怎麼處理?都是這個GeneratorAPI來實現的。@vue/cli插件所提供的generator向外暴露一個函數,接收第一個參數api,而後經過該api提供的一些方法來完成應用的拓展工做。咱們來看一下具體提供了哪些方法:

hasPlugin: 判斷項目中是否有某個插件
extendPackage: 拓展package.json配置
render: 利用ejs渲染模板文件
onCreateComplete: 內存中保留的文件字符串所有被寫入文件後的回調函數
exitLog: 當generator退出時候的信息
genJsConfig: 將json文件生成爲js配置文件
injectImports: 向文件當中注入import語法方法
injectRootoptions: 向vue實例中添加選項
....等等 看看官網文檔

GeneratorAPI方法能夠具體在根據須要在詳細看看。

const api = new GeneratorAPI(id, this, options, rootOptions)
apply(api, options, rootOptions, invoking)

這一段代碼就是運行了各個插件內部的generator方法。
再回首看看resolvePlugins 就明白了

resolvePlugins () {
    ....
    const apply = loadModule(`${id}/generator`, this.context) || (() => {}) 
    ...
}

在Generator實例化時候,還能夠分紅3步走:extractConfigFiles, resolveFiles和writeFileTree

async generate ({
    extractConfigFiles = false,
    checkExisting = false
  } = {}) {
    // save the file system before applying plugin for comparison
    const initialFiles = Object.assign({}, this.files)
    // extract configs from package.json into dedicated files.
    this.extractConfigFiles(extractConfigFiles, checkExisting) //提取配置文件
    // wait for file resolve
    await this.resolveFiles() // 模板渲染
    // set package.json
    this.sortPkg()
    this.files['package.json'] = JSON.stringify(this.pkg, null, 2) + '\n'
    // write/update file tree to disk
    await writeFileTree(this.context, this.files, initialFiles) // 在磁盤上生成文件
  }

extractConfigFiles
提取配置文件指的是將一些插件(好比 eslint,babel)的配置從 package.json 的字段中提取到專屬的配置文件中。
resolveFiles主要分爲如下三個部分執行
fileMiddlewares
injectImportsAndOptions
postProcessFilesCbs
fileMiddlewares 裏面包含了 ejs render 函數,全部插件調用 api.render 時候只是把對應的渲染函數 push 到了 fileMiddlewares 中,等全部的 插件執行完之後纔會遍歷執行 fileMiddlewares 裏面的全部函數,即在內存中生成模板文件字符串。

injectImportsAndOptions 就是將 generator 注入的 import 和 rootOption 解析到對應的文件中,好比選擇了 vuex, 會在 src/main.js 中添加 import store from './store',以及在 vue 根實例中添加 router 選項。

postProcessFilesCbs 是在全部普通文件在內存中渲染成字符串完成以後要執行的遍歷回調。例如將 @vue/cli-service/generator/index.js 中的 render 是放在了 fileMiddlewares 裏面,而將 @vue/cli-service/generator/router/index.js 中將替換 src/App.vue 文件的方法放在了 postProcessFiles 裏面,緣由是對 src/App.vue 文件的一些替換必定是發生在 render 函數以後,若是在以前,修改後的 src/App.vue 在以後 render 函數執行時又會被覆蓋,這樣顯然不合理。

writeFileTree在提取了配置文件和模板渲染以後調用了 sortPkg 對 package.json 的字段進行了排序並將 package.json 轉化爲 json 字符串添加到項目的 files 中。 此時整個項目的文件已經在內存中生成好了(在源碼中就是對應的 this.files),接下來就調用 writeFileTree 方法將內存中的字符串模板文件生成在磁盤中。

相關文章
相關標籤/搜索