Vue Cli3 与 2的区别

915 阅读6分钟

CLI 2.x生成的项目有build和config目录与webpack打包相关, 如下

// 图1 2 
旧版本下, 使用CLI 创建项目之后, 相关配置就本地化了, 未来如果想要升级webpack, 必须自己来升级
Vue CLI 3将webpack相关的打包配置内置在一个npm包@vue/cli-service中, 由官方维护, 更加省事. 提供了修改webpack配置的接口, 可以是在vue.config.js中配置, 或者在插件中调用.
webpack-chain 提供链式调用API来生成/修改webpack配置
一个运行时依赖 (@vue/cli-service),该依赖: 可升级; 基于 webpack 构建,并带有合理的默认配置; 可以通过项目内的配置文件进行配置; 可以通过插件进行扩展。
// 图3
旧版本下, 如果想要增加新功能, 比如组内使用easy-tool同步多语言, 我们需要自己在项目中增加easy-tool配置文件等操作
脚手架肯定是有在项目中生成指定文件的能力, Vue CLI 3有插件系统, 并给插件提供了接口, 可以在项目中去生成指定文件, 给项目增加easy-tool配置文件这些可以自动化操作的事情可以在插件中完成, 组内成员可以给项目安装上插件, 执行相关操作去调用插件.

Vue CLI 3 插件 这里看

当然, 不止这些

相关工具包

commander node.js 命令行接口的完整解决方案
inquirer interactive command line, 命令行交互相关
webpack-chain 提供链式调用API来生成/修改webpack配置
fs-extra 系统fs模块的扩展,提供了更多便利的 API,并继承了fs模块的 API
execa Process execution for humans, improves child_process
ejs 模板引擎, 类似的有Jade / Pug, mustache
deepmerge, 合并两个对象的可枚举属性
semver (Semantic Versioning) 用于npm版本号的语义化操作
dotenv loads environment variables from a .env file into process.env
recast 将代码转换成ast, 修改ast等

vue create

@vue/cli包提供vue [options] 命令 可以通过 vue create 快速创建一个新项目 preset.json: 包含预设配置数据的主要文件

   // 本地preset
   vue create -p ./preset.json dolphin_template
   // 加载远程preset.json
要生成一个项目, 需要按照这样的流程. 首先官方给我们一个最初的模板(由官方插件提供), 然后, 我们写一些vue-cli插件, 在官方模板的基础上, 缝缝补补, 最后得到符合我们要求的初始项目.
preset中可以通过plugins指定插件, 从而在官方默认模版的基础上, 去新加/覆盖一些文件, 修改配置文件(vue.config.js, babel.config.js等), 在package.json中注入script命令等.

##preset.json
    {
      "plugins": {
        "@vue/cli-plugin-babel": {
          "version": "^3.5.1"
        },
        "@vue/cli-plugin-eslint": {
          "version": "^3.5.1",
          "config": "standard",
          "lintOn": [
            "save"
          ]
        },
        "vue-cli-plugin-dolphin-base": {
          "version": "2.6.5",
          "prompts": true
        },
        "vue-cli-plugin-dolphin-theme": {
          "version": "2.6.5"
        },
        "vue-cli-plugin-changelog": {
          "version": "2.6.5"
        },
        "vue-cli-plugin-easytool": {
          "version": "2.6.5"
        },
        "vue-cli-plugin-easymock": {
          "version": "2.6.5",
          "prompts": true
        },
        "vue-cli-plugin-lego": {
          "version": "2.6.5"
        }
      }
    }

不使用任何外部插件

如果我们在preset.json中没有添加任何插件, 利用vue create 生成的项目是什么样的? 没有主动添加任何插件所生成的项目是最简单的, 对于理解vue create很有帮助
图四
上面命令行中的流程, 可以用下面的图来概括, 项目目录和package.json的变化也在图中, 可以放大看
图图图图

命令行工具的执行代码在哪

vue create的相关代码在@vue/cli包中,
图

@vue/cli/bin/vue.js 配合package.json的bin字段使用
https://docs.npmjs.com/files/package.json#bin

##Execute vue create
@vue/cli/bin/vue.js
      const program = require('commander')
      // ...
      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('-c, --clone', 'Use git clone when fetching remote preset')
        .action((name, cmd) => {
          const options = cleanArgs(cmd)
          // ...一些边界条件处理
          require('../lib/create')(name, options)
        })
      //...
      program.parse(process.argv)
使用commander来进行命令行参数处理, 能够
获取用户输入的相关配置参数options
获取所要创建的项目名name, 假设为dolphin_template
在这之后, 开始创建一个项目的正式流程:

1. 校验, 获取输出目录

validateProjectName, 比如用户可能用vue-cli创建通用库放npm上, 库名称是需要出现在url中的, 避免使用non-url-safe characters, 从而规避一些风险 目标目录dolphin_tempalte 是否存在? 根据实际情况, 提示overwrite/merge/cancel (inquirer.js) cli通过commander收集到的信息包括: 参数配置信息, 项目名称, 以及targetDir @vue/cli/lib/create.js

      const cwd = options.cwd || process.cwd()
      const inCurrent = projectName === '.'
      const name = inCurrent ? path.relative('../', cwd) : projectName // 项目目录名称
      const targetDir = path.resolve(cwd, projectName || '.')

targetDir是个很重要的信息, 后续的依赖安装, 生成模板, 获取插件目录, 依赖查找等, 都需要基于这个上下文.

2.获取preset信息

这里只讨论与vue create -p相关的逻辑, 由于指定了想要安装的插件, 所以一些prompt相关的逻辑可以忽略. 解析preset/inline preset, 确定所要加载的插件, 如果没有使用preset, 则通过询问prompt来确定. @vue/cli/lib/Creator.js

     // 使用preset
     if (cliOptions.preset) {
        // vue create foo --preset bar
        preset = await this.resolvePreset(cliOptions.preset, cliOptions.clone)
      } else if (cliOptions.default) {
        // vue create foo --default
        preset = defaults.presets.default
      } else if (cliOptions.inlinePreset) {
        // 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()
      }

3. 确定使用的包管理器

优先级: 命令行参数packageManager > .vuerc中packageManager > yarn > pnpm > npm 在 vue create 过程中保存的预设配置会被放在你的 home 目录下的一个配置文件中 (~/.vuerc)。你可以通过直接编辑这个文件来调整、添加、删除保存好的配置。 cli.vuejs.org/zh/guide/pl…

    const packageManager = (
      cliOptions.packageManager ||
      loadOptions().packageManager ||
      (hasYarn() ? 'yarn' : null) ||
      (hasPnpm3OrLater() ? 'pnpm' : 'npm')
    )
    const pm = new PackageManager({ context, forcePackageManager: packageManager }

4. 注入内部插件

通过preset获取到用户要加载的插件列表, 并加入内部插件@vue/cli-service
    // 加入内部插件, 设置其options(projectName, preset中的属性)
    preset.plugins['@vue/cli-service'] = Object.assign({
      projectName: name
    }, preset)

5.确定插件version, 生成package.json

对于非官方插件, 优先使用preset中定义version, 否则使用latest 对于官方插件, 优先使用preset中定义的version, 否则使用最新的minor版本, 若@vue/cli monorepo的版本为3.2.4, 则使用^3.2.0. 因为可以认为monorepo维护的minor版本是统一的, 各个子repo path版本有差异

    const pkg = {
      name,
      version: '0.1.0',
      private: true,
      devDependencies: {}
    }
    //... 
    const deps = Object.keys(preset.plugins)

    pkg.devDependencies[dep] = (
      preset.plugins[dep].version || ((/^@vue/.test(dep)) ? `^${latestMinor}` : `latest`)
    )
    在项目名称dolphin_template文件夹, 下生成package.json
      await writeFileTree(context, {
        'package.json': JSON.stringify(pkg, null, 2)
      })
      async function writeFileTree (dir, files, previousFiles) {
      // ...
        Object.keys(files).forEach((name) => {
          const filePath = path.join(dir, name)
          fs.ensureDirSync(path.dirname(filePath)) // 不存在则创建项目文件夹
          fs.writeFileSync(filePath, files[name])
        })
      }

6. git init

如果环境中有git, 默认会执行git init

7. Install dep

    子进程执行yarn/ npm intall等安装package.json中的依赖(包括 @vue/cli-service, preset中配置的plugins)
    log(`⚙  Installing CLI plugins. This might take a while...`)
    execa('npm', ['install']) // 简化版

8. resolve plugins && invoke plugins

后续需要利用插件生成相关模板, preset.plugins是保存了插件信息的对象, 其中有在preset.json中定义的plugins, 也包括所注入的内部插件@vue/cli-service, 需要保证@vue/cli-service生成模板的逻辑最先执行. vue-cli利用Object.keys()来实现对象key的排序. 对象形式的plugins

    {
      "@vue/cli-service": {},
      // "vue-cli-plugin-dolphin-base": {
      //   "version": "2.6.5",
      //   "prompts": true
      // },
      //...
    }
    规范定义了Object.keys()输出顺序

    var foo = {a: 1, b:2, c: 3}
    var bar = {}
    bar.c = foo.c
    delete foo.c
    Object.keys(foo).forEach(key => {
        bar[key] = foo[key]
    })

    // Object.keys(foo) // ['a', b']
    // Object.keys(bar) // ['c', 'a', 'b']
对象foo, 对于非integer like的字符串(a,b,c), 按照属性创建的顺序排.
Firstly, integer-like keys in ascending order
Secondly, normal keys in insertion order
Then, Symbols in insertion order
Lastly, if mixed, order: interger-like, normal keys, Symbols
https://tc39.es/ecma262/#sec-ordinaryownpropertykeys
https://2ality.com/2015/10/property-traversal-order-es6.html
https://zhuanlan.zhihu.com/p/40601459
https://juejin.cn/post/6844903796062191624
    @vue/cli/lib/Creator.js

    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) {
        // ...
        }
        plugins.push({ id, apply, options })
      }
      return plugins
    }
    对象形式的plugins排序后, 生成对应数组
    {
      "@vue/cli-service": {
      //...
      },
      // "vue-cli-plugin-dolphin-base": {
      //   "version": "2.6.5",
      //   "prompts": true
      // },
      //...
    }

    已排序, 数组形式的plugins
    [
      {
        id: '@vue/cli-service'
        apply: require(require.resolve('@vue/cli-service/generator', context)),
        options: {
          //... 
        }
      }
    ]

    插件提供模板, 按顺序调用vue-cli-*插件的模板生成逻辑.

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

    各个插件都可能提供模板, 遍历记录下来, 使用一个对象记录要生成文件的路径和内容信息, 存在覆盖的情况
    官网一个插件generator例子
    一个典型的 CLI 插件的目录结构看起来是这样的:

    ├── README.md
    ├── generator.js  # generator (可选)
    ├── prompts.js    # prompt 文件 (可选)
    ├── index.js      # service 插件
    └── package.json