VUE-CLI解析初探

vue-cli,vue/cli-service,vue/cli-ui等

Vue CLI 是一个基于 Vue.js 进行快速开发的完整系统,通过 @vue/cli 实现的交互式的项目脚手架。通过 @vue/cli + @vue/cli-service-global 实现的零配置原型开发。

  • 一个运行时依赖 (@vue/cli-service),该依赖:
  • 可升级;
  • 基于 webpack 构建,并带有合理的默认配置;
  • 可以通过项目内的配置文件进行配置;
  • 可以通过插件进行扩展。
  • 一个丰富的官方插件集合,集成了前端生态中最好的工具。
  • 一套完全图形化的创建和管理 Vue.js 项目的用户界面

Vue CLI 致力于将 Vue 生态中的工具基础标准化。

CLI 服务是构建于 webpack 和 webpack-dev-server 之上的。它包含了:

  • 加载其它 CLI 插件的核心服务;
  • 一个针对绝大部分应用优化过的内部的 webpack 配置;
  • 项目内部的 vue-cli-service 命令,提供 servebuild 和 inspect 命令

CLI 插件是向你的 Vue 项目提供可选功能的 npm 包,当你在项目内部运行 vue-cli-service 命令时,它会自动解析并加载 package.json 中列出的所有 CLI 插件。包含serve,build, inspect命令

vue-cli-service serve 命令会启动一个开发服务器 (基于 webpack-dev-server) 并附带开箱即用的模块热重载 (Hot-Module-Replacement)。

vue-cli-service build 会在 dist/ 目录产生一个可用于生产环境的包,带有 JS/CSS/HTML 的压缩,和为更好的缓存而做的自动的 vendor chunk splitting。它的 chunk manifest 会内联在 HTML 里。

vue-cli-service inspect 来审查一个 Vue CLI 项目的 webpack config。

进入主题,使用vue-cli创建一个项目

从lib文件夹下的create.js文件入手

 首先获取和校验工程名称,如果名称包含.,则取相对路径如果出错会报错直接退出

 const inCurrent = projectName === '.'

  const name = inCurrent ? path.relative('../', cwd) : projectName

  const targetDir = path.resolve(cwd, projectName || '.')

  const result = validateProjectName(name)

 接下来就要判断该工程是否已经存在,已存在则判断是否强制移除,强制则直接移除

 if (options.force) {

      await fs.remove(targetDir)

    }

 如果工程名称包含. 则询问是否在当前目录创建工程,OK则继续往下

const { ok } = await inquirer.prompt([

          {

            name: 'ok',

            type: 'confirm',

            message: `Generate project in current directory?`

          }

        ])

        if (!ok) {

          return

        }

否则就询问覆盖、合并、取消,如果命令等于覆盖,则删除当前工程

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.js文件

 首先是输入输出的提示,我们来看这个函数resolveIntroPrompts,获取输出提示函数resolveOutroPrompts

通过getPresets获取默认的配置,默认在你电脑用户目录下的 .vuerc文件。选择你需要类型确认即可

 resolveIntroPrompts () {

    const presets = this.getPresets()

    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)'

      }

      return {

        name: `${displayName} (${formatFeatures(preset)})`,

        value: name

      }

    })

    const presetPrompt = {

      name: 'preset',

      type: 'list',

      message: `Please pick a preset:`,

      choices: [

        ...presetChoices,

        {

          name: 'Manually select features',

          value: '__manual__'

        }

      ]

    }

    const featurePrompt = {

      name: 'features',

      when: isManualMode,

      type: 'checkbox',

      message: 'Check the features needed for your project:',

      choices: [],

      pageSize: 10

    }

    return {

      presetPrompt,

      featurePrompt

    }

  }

获取各个模块的提示配置,并注入PromptModuleAPI:

getPromptModules = () => {

  return [

    'vueVersion',

    'babel',

    'typescript',

    'pwa',

    'router',

    'vuex',

    'cssPreprocessors',

    'linter',

    'unit',

    'e2e'

  ].map(file => require(`../promptModules/${file}`))

}

const promptAPI = new PromptModuleAPI(this)

promptModules.forEach(m => m(promptAPI))

开始运行我们的creator的create函数:

首先是默认配置中存在cliOptions.preset则走resolvePreset函数,后者inlinePreset

存在直接返回,否则返回默认的配置内容

 resolvePreset:

 如果默认配置中存在则直接返回,判断是本地文件或者远程文件,相应的进行加载

const savedPresets = this.getPresets()

    if (name in savedPresets) {

      preset = savedPresets[name]

    } else if (name.endsWith('.json') || /^\./.test(name) || path.isAbsolute(name)) {

      preset = await loadLocalPreset(path.resolve(name))

    } else if (name.includes('/')) {

      log(`Fetching remote preset ${chalk.cyan(name)}...`)

      this.emit('creation', { event: 'fetch-remote-preset' })

      try {

        preset = await loadRemotePreset(name, clone)

      } catch (e) {

        error(`Failed fetching remote preset ${chalk.cyan(name)}:`)

        throw e

      }

    }

接下来注入cli-service插件

preset.plugins['@vue/cli-service'] = Object.assign({

      projectName: name

    }, preset)

判断router和vuex是否配置,如果配置则添加该插件

// 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'] = {}

    }

安装包的管理过程:将配置中的插件写入package.json的devDependencies中

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 ? `latest` : `~${latestMinor}`

        } else {

          version = 'latest'

        }

      }

      pkg.devDependencies[dep] = version

    })

生成package.json文件,判断packageManager的管理方式并生成相应的文件

await writeFileTree(context, {

      'package.json': JSON.stringify(pkg, null, 2)

    })

如果是测试模式则直接链接到cli-service,否则执行安装依赖

setupDevProject (targetDir) {

  return linkBin(

    require.resolve('@vue/cli-service/bin/vue-cli-service'),

    path.join(targetDir, 'node_modules', '.bin', 'vue-cli-service')

  )

}

if (isTestOrDebug && !process.env.VUE_CLI_TEST_DO_INSTALL_PLUGIN) {

      // in development, avoid installation process

      await require('./util/setupDevProject')(context)

    } else {

      await pm.install()

    }

加载各个插件和插件的提示:

resolvePlugins (rawPlugins, pkg) {

    // 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) {

        let pluginPrompts = loadModule(`${id}/prompts`, this.context)

        if (pluginPrompts) {

          const prompt = inquirer.createPromptModule()

          if (typeof pluginPrompts === 'function') {

            pluginPrompts = pluginPrompts(pkg, prompt)

          }

          if (typeof pluginPrompts.getPrompts === 'function') {

            pluginPrompts = pluginPrompts.getPrompts(pkg, prompt)

          }

          log()

          log(`${chalk.cyan(options._isPreset ? `Preset options:` : id)}`)

          options = await prompt(pluginPrompts)

        }

      }

      plugins.push({ id, apply, options })

    }

    return plugins

  }

生成generate并执行

const generator = new Generator(context, {

      pkg,

      plugins,

      afterInvokeCbs,

      afterAnyInvokeCbs

    })

    await generator.generate({

      extractConfigFiles: preset.useConfigFiles

    })

接下来解析generate函数里的内容:

async generate ({

    extractConfigFiles = false,

    checkExisting = false

  } = {}) {

    await this.initPlugins()

    // 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, this.filesModifyRecord)

  }

我们看一下initPlugins这个方法做了什么

遍历所有插件allPlugins,plugins, 并将插件回调放入afterAnyInvokeCbs

并执行插件的hooks。

initPlugins () {

    const { rootOptions, invoking } = this

    const pluginIds = this.plugins.map(p => p.id)

    // avoid modifying the passed afterInvokes, because we want to ignore them from other plugins

    const passedAfterInvokeCbs = this.afterInvokeCbs

    this.afterInvokeCbs = []

    // apply hooks from all plugins to collect 'afterAnyHooks'

    for (const plugin of this.allPlugins) {

      const { id, apply } = plugin

      const api = new GeneratorAPI(id, this, {}, rootOptions)

      if (apply.hooks) {

        await apply.hooks(api, {}, rootOptions, pluginIds)

      }

    }

    // We are doing save/load to make the hook order deterministic

    // save "any" hooks

    const afterAnyInvokeCbsFromPlugins = this.afterAnyInvokeCbs

    // reset hooks

    this.afterInvokeCbs = passedAfterInvokeCbs

    this.afterAnyInvokeCbs = []

    this.postProcessFilesCbs = []

    // apply generators from plugins

    for (const plugin of this.plugins) {

      const { id, apply, options } = plugin

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

      await apply(api, options, rootOptions, invoking)

      if (apply.hooks) {

        // while we execute the entire `hooks` function,

        // only the `afterInvoke` hook is respected

        // because `afterAnyHooks` is already determined by the `allPlugins` loop above

        await apply.hooks(api, options, rootOptions, pluginIds)

      }

    }

    // restore "any" hooks

    this.afterAnyInvokeCbs = afterAnyInvokeCbsFromPlugins

  }

GeneratorAPI的render函数的主要作用,

比较有趣的是这个函数extractCallDir,通过错误栈来获取路径,并通过正则匹配获取模板的根目录:

function extractCallDir () {

  // extract api.render() callsite file location using error stack

  const obj = {}

  Error.captureStackTrace(obj)

  const callSite = obj.stack.split('\n')[3]

  // the regexp for the stack when called inside a named function

  const namedStackRegExp = /\s\((.*):\d+:\d+\)$/

  // the regexp for the stack when called inside an anonymous

  const anonymousStackRegExp = /at (.*):\d+:\d+$/

  let matchResult = callSite.match(namedStackRegExp)

  if (!matchResult) {

    matchResult = callSite.match(anonymousStackRegExp)

  }

  const fileName = matchResult[1]

  return path.dirname(fileName)

}

主要是这个render函数:

首先判断source是字符串,对象或者是函数,获取cli-service/generator/template目录下所有文件,如果想要新增自定义模板,可以在template增删文件。通过globby解析获取文件名,加载文件内容放入this.files中在后面的resolveFiles文件中执行

for (const middleware of this.fileMiddlewares) {

      await middleware(files, ejs.render)

    }

render (source, additionalData = {}, ejsOptions = {}) {

    const baseDir = extractCallDir()

    if (isString(source)) {

      source = path.resolve(baseDir, source)

      this._injectFileMiddleware(async (files) => {

        const data = this._resolveData(additionalData)

        const globby = require('globby')

        const _files = await globby(['**/*'], { cwd: source, dot: true })

        for (const rawPath of _files) {

          const targetPath = rawPath.split('/').map(filename => {

            // dotfiles are ignored when published to npm, therefore in templates

            // we need to use underscore instead (e.g. "_gitignore")

            if (filename.charAt(0) === '_' && filename.charAt(1) !== '_') {

              return `.${filename.slice(1)}`

            }

            if (filename.charAt(0) === '_' && filename.charAt(1) === '_') {

              return `${filename.slice(1)}`

            }

            return filename

          }).join('/')

          const sourcePath = path.resolve(source, rawPath)

          const content = renderFile(sourcePath, data, ejsOptions)

          // only set file if it's not all whitespace, or is a Buffer (binary files)

          if (Buffer.isBuffer(content) || /[^\s]/.test(content)) {

            files[targetPath] = content

          }

        }

      })

    } else if (isObject(source)) {

      this._injectFileMiddleware(files => {

        const data = this._resolveData(additionalData)

        for (const targetPath in source) {

          const sourcePath = path.resolve(baseDir, source[targetPath])

          const content = renderFile(sourcePath, data, ejsOptions)

          if (Buffer.isBuffer(content) || content.trim()) {

            files[targetPath] = content

          }

        }

      })

    } else if (isFunction(source)) {

      this._injectFileMiddleware(source)

    }

  }

resolveFiles的具体内容,./util/codemods/injectImports, './util/codemods/injectOptions'加载默认配置文件

 async resolveFiles () {

    const files = this.files

    // get project files  invoke render函数

    for (const middleware of this.fileMiddlewares) {

      await middleware(files, ejs.render)

    }

    // normalize file paths on windows

    // all paths are converted to use / instead of \

    normalizeFilePaths(files)

    // handle imports and root option injections

    Object.keys(files).forEach(file => {

      let imports = this.imports[file]

      imports = imports instanceof Set ? Array.from(imports) : imports

      if (imports && imports.length > 0) {

        files[file] = runTransformation(

          { path: file, source: files[file] },

          require('./util/codemods/injectImports'),

          { imports }

        )

      }

      let injections = this.rootOptions[file]

      injections = injections instanceof Set ? Array.from(injections) : injections

      if (injections && injections.length > 0) {

        files[file] = runTransformation(

          { path: file, source: files[file] },

          require('./util/codemods/injectOptions'),

          { injections }

        )

      }

    })

    for (const postProcess of this.postProcessFilesCbs) {

      await postProcess(files)

    }

    debug('vue:cli-files')(this.files)

  }

最后通过writeFileTree 把模板写入根目录下,一个模板就已经下载好了

writeFileTree (dir, files, previousFiles, include) {

  if (process.env.VUE_CLI_SKIP_WRITE) {

    return

  }

  if (previousFiles) {

    await deleteRemovedFiles(dir, files, previousFiles)

  }

  Object.keys(files).forEach((name) => {

    if (include && !include.has(name)) return

    const filePath = path.join(dir, name)

    fs.ensureDirSync(path.dirname(filePath))

    fs.writeFileSync(filePath, files[name])

  })

}

写入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仓库,shouldInitGit。

vue-cli下还有add, invoke, inspect文件, add和invoke是在项目中添加所需插件。

你可能感兴趣的:(vue.js,webpack,前端)