Vue-cli(3.0 + ) 源码分析(二)

上一篇文章(Vue-cli(3.0 + ) 源码分析(一)_夜跑者的博客-CSDN博客)分析了vue create my-project  命令执行过程中创建preset,往preset注入@vue/cli-service 插件,写入package.json,初始化git,npm install 依赖等过程。这篇文章继续往下分析核心部分:generate过程。

在分析generate过程之前,我们看看不做generate创建的工程项目是什么样的:

 只是安装了cli-plugin-babel、cli-plugin-eslint、cli-service,package.json 只有几个字段。我们再看看实际创建一个可用的项目工程package.json 长什么样:

差距有点大,接下来看看generate过程是怎么做到的。

 

 代码非常简单,处理一下preset中的插件,创建一个Generator实例,然后执行实例的generate方法,我们详细看下整个过程:

1. 处理preset中的插件

  // { id: options } => [{ id, apply, options }]
  async 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)) {
      //加载插件中的generator.js文件或/generator/index.js文件
      const apply = loadModule(`${id}/generator`, this.context) || (() => {})
      let options = rawPlugins[id] || {}

      if (options.prompts) {
        //如果此插件中设置了prompts为true,则加载prompts对象。弹出交互对话框,让用户选择。
        //例如vue create -p xxx test-project 命令中创建一个基于preset的工程项目
        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
  }

代码也比较简单,在主要位置增加了注释。主要是加载了插件的generator.js(generator/index.js)中export出来的方法赋值给apply,如果插件带有prompts,则要处理prompts把结果赋值给options。最后把插件的三个元素{id, apply, options}组成object推入数组。从这里也能推断出之后会遍历这些插件并且执行插件的apply方法。

2. 创建Generator实例

module.exports = class Generator {
  constructor (context, {
    pkg = {},
    plugins = [],
    afterInvokeCbs = [],
    afterAnyInvokeCbs = [],
    files = {},
    invoking = false
  } = {}) {
    this.context = context
    this.plugins = sortPlugins(plugins) //对插件进行排序,See leetcode 210
    this.originalPkg = pkg
    this.pkg = Object.assign({}, pkg)
    this.pm = new PackageManager({ context })



    this.files = Object.keys(files).length
      // when execute `vue add/invoke`, only created/modified files are written to disk
      ? watchFiles(files, this.filesModifyRecord = new Set())
      // all files need to be written to disk
      : files
    this.fileMiddlewares = [] //GeneratorAPI实例中的render方法会把插件中的模板文件回调函数推入到此数组
    this.postProcessFilesCbs = []
    // exit messages
    this.exitLogs = []

    // load all the other plugins
    this.allPlugins = this.resolveAllPlugins() //把@vue/cli-service给过滤掉了

    const cliService = plugins.find(p => p.id === '@vue/cli-service')
    const rootOptions = cliService
      ? cliService.options
      : inferRootOptions(pkg)
      console.log('liubbc rootOptions: ', rootOptions)
    this.rootOptions = rootOptions

new一个Generator实例,在构造函数中,把之前处理后的plugins赋值给Generator实例的plugins属性。如果插件中有@vue/cli-service则 rootOptions采用@vue/cli-service的options,

3. 执行Generator实例的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)
  }

 generate方法非常短,先看一下initPlugins方法,initPlugins 主要有两个for循环:

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


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

第一个for循环主要处理allPlugins插件,如果插件存在hooks则执行hooks,不详细阐述了。第二个for循环为每个插件创建了一个GeneratorAPI实例,并执行插件的apply方法,之前分析过apply方法就是插件的generator.js(generator/index.js)export出来的方法,需要注意的是apply方法的第一个参数是GeneratorAPI实例。我们自己想实现一个插件,在插件中就可以调用GeneratorAPI实例提供的方法。GeneratorAPI实例提供了哪些方法呢?

hasPlugin:判断项目中是否存在此插件

extendPackage:扩展package.json配置

render:利用 ejs 渲染模板文件

genJSConfig:将 json 文件生成为 js 配置文件

injectImports:向文件当中注入import语法的方法

injectRootOptions:向 Vue 根实例中添加选项

我们看一下@vue/cli-service中的generator.js,

调用 GeneratorAPI实例的extendPackage方法往package.json里面扩展一些选项;重点看一下调用的render方法。

  /**
   * Render template files into the virtual files tree object.
   *
   * @param {string | object | FileMiddleware} source -
   *   Can be one of:
   *   - relative path to a directory;
   *   - Object hash of { sourceTemplate: targetFile } mappings;
   *   - a custom file middleware function.
   * @param {object} [additionalData] - additional data available to templates.
   * @param {object} [ejsOptions] - options for ejs.
   */
  render (source, additionalData = {}, ejsOptions = {}) {
    const baseDir = extractCallDir()
    if (isString(source)) {
      source = path.resolve(baseDir, source)
      if(this.id.includes('cli-service')){
        console.log('liubbc cli-service render source: ', source)
      }
      this._injectFileMiddleware(async (files) => {
        const data = this._resolveData(additionalData)
        const globby = require('globby')
        const _files = await globby(['**/*'], { cwd: source, dot: true })
        if(this.id.includes('cli-service')){
          console.log('liubbc cli-service render files: ', _files)
        }
        for (const rawPath of _files) {
          if(this.id.includes('cli-service')){
            console.log('liubbc cli-service render rawPath: ', rawPath)
          }
          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)
          if(this.id.includes('cli-service')){
            console.log('liubbc cli-service render targetPath: ', targetPath, '; sourcePath: ', sourcePath)
          }
          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
          }
        }
        if(this.id.includes('cli-service')){
          console.log('liubbc cli-service render files: ', files)
        }
      })

  _injectFileMiddleware (middleware) {
    this.generator.fileMiddlewares.push(middleware)
  }

注释Render template files into the virtual files tree object.  说的比较明白,具体看下怎么做的。

@vue/cli-service的调用render方法传入的第一个参数是字符串'./template',是个模板相对路径,把它转换为绝对路径。用globby提取出该路径下所有的文件,打印如下:

接下来通过ejs.render 把文件中的内容处理一下并提取出来生成虚拟文件树对象,打印如下:

接下来就是将 generator 注入的 import 和 rootOption 解析到对应的文件中,比如选择了 vuex, 会在 src/main.js 中添加 import store from './store',以及在 vue 根实例中添加 router 选项。

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

最后把package.json 里面的属性做个排序,把生成的虚拟文件数写到磁盘中

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值