【Vue-CLI源码学习】- 构建流程

背景

当使用@vue/cli脚手架创建一个项目的时候,直接使用npm run dev就能成功启动项目,而这行命令帮我们做了什么,脚手架在这中间做了哪些工作,下面就是从我理解的源码(@vue/cli 5.0.8版本)角度来分析一下整个构建流程。

npm run xxx这行命令发生了什么,请查看另一篇文章 详解npm run xxx ,这里主要讲vue-cli-service serve相关。
在这里插入图片描述

实际上针对于当前script配置npm run serve 就等同于node ./node_modules/.bin/vue-cli-service

源码解析

1、cli-service/bin/vue-cli-service.js文件

这是脚手架的可执行文件,当npm run serve的之后会通过node_modules/bin/vue-cli-service文件映射到cli-service/bin/vue-cli-service.js文件,并执行。这个文件主要做了两件事:

  • 校验node版本。
  • 创建Service实例子,并解析命令行参数 ,执行实例子中暴露的run方法启动项目。
1.1、校验node版本

在这里插入图片描述
这里在package.json中要求的node版本必须为12.x.x或者>=14, 否则就会报错。

版本号 x.y.z :
z :表示一些小的bugfix, 更改z的号,
y :表示一些大的版本更改,比如一些API的变化
x :表示一些设计的变动及模块的重构之类的,会升级x版本号
在package.json里面dependencies依赖包的版本号前面的符号有两种,一种是~,一种是^。
~的意思是匹配最近的小版本 比如~1.0.2将会匹配所有的1.0.x版本,但不匹配1.1.0
^的意思是最近的一个大版本 比如1.0.2 将会匹配 所有 1.x.x, 但不包括2.x.x

1.2、创建Service实例,并解析命令行参数然后执行

在这里插入图片描述
这里通过process.argv.slice(2)来获取到用户在命令行输入的参数,然后通过minimist来进行解析终端命令,最后通过args._[0]来获取我们要执行的cli命令。

举例:
a: 当我们输入npm run serve,实际执行的是node /node_modules/bin/vue-cli-service

“scripts”: {
“serve”: “vue-cli-service serve --mode=dev --skip-plugins=skipPlugins”,
},

b: process.argv: 获取终端执行命令的参数,是一个数组 0: node路径 1: 被执行的js文件路径 2~n: 终端输入的参数。

[
‘/usr/local/bin/node’,
‘/Users/qinyuhao/Desktop/项目/qyh-test/test_11/node_modules/.bin/vue-cli-service’,
‘serve’,
‘–mode=dev’,
‘–skip-plugins=skipPlugins’
]

c: 通过minimist将获取的终端命令转换为对象。
在这里插入图片描述

{
_: [ ‘serve’ ],
modern: false,
report: false,
‘report-json’: false,
‘inline-vue’: false,
watch: false,
open: false,
copy: false,
https: false,
verbose: false,
mode: ‘dev’,
‘skip-plugins’: ‘skipPlugins’
}

d: 通过args._[0]获取cli命令 // [ ‘serve’ ]

e: 通过传入参数到Service实例暴露的run方法,并执行,进行初始化启动项目
在这里插入图片描述

2、Service类

从入口可以看出,vue-cli-service 的核心是使用了 Service 类,实例化并调用run方法。下面我们看看 Service 类里面做了什么。

2.1 首先在执行run方法之前进入到构造函数做一些初始化配置。
constructor (context, { plugins, pkg, inlineOptions, useBuiltIn } = {}) {
	// 绑定当前Service实例在process环境变量上,后续可直接获取上面暴露的钩子
    process.VUE_CLI_SERVICE = this
    // 是否是第一次启动,已经初始化过了
    this.initialized = false
    // 当前实例的上下文,一般是当前运行的工作目录
    this.context = context
    // 创建Service实例时传入的配置项,最后会通过webpack-chain的方式
    this.inlineOptions = inlineOptions //undefined
    // 通过webpack-chain方式配置的webpack 配置
    this.webpackChainFns = []
    // 通过confuregure方式配置的webpack 配置
    this.webpackRawConfigFns = []
    // dev-server配置
    this.devServerConfigFns = []
    // 内置的命令,包括cli命令和配置命令: serve、build、help...
    this.commands = {}
    // 项目的package.json的运行目录路径
    this.pkgContext = context
    // package.json containing the plugins 工作目录下package.json信息
    // 如果依赖的vuePlugins有其他不在package.json的配置,就要通过res调用olveFrom来指向其他配置文件地址,
    // 并递归resolvePkg获取所有配置信息
    this.pkg = this.resolvePkg(pkg)
    // inlinePlugins是创建Service实例时传入的配置,会覆盖package.json的配置, 优先级inlinePlugins 》 package.json中配置的plugins
    // useBuiltIn是vue-cli-serve内置的插件
    this.plugins = this.resolvePlugins(plugins, useBuiltIn)
    // 只会使用到部分api,启动项目过程中不会完全加载这个插件
    this.pluginsToSkip = new Set() // 在运行期间会将跳过的插件
    // 为命令指定模式
    // 注册的插件可以通过 module.exports.defaultModes 指定特定的模式
    /*{
      serve: 'development',
      build: 'production',
      inspect: 'development',
      'test:unit': 'test'
    }*/
    this.modes = this.plugins.reduce((modes, { apply: { defaultModes } }) => {
      return Object.assign(modes, defaultModes)
    }, {})
  }

在构造函数中,通过resolvePlugins函数,注入了cli指令和配置指令,并使其能识别serve、build、inspect、help等命令。
这个函数主要做了以下一些事情:

  • 将cli指令和配置指令,注入以插件等形式注入
  • 加载package.json文件中需要的vue插件
  • 加载package.json中通过vue属性来配置的本地插件
  • 返回项目需要用到的所有插件(经过sortPlugins函数排序之后)

这里面分为4类插件,容易混淆,所以列举了出来各种插件的区别:

   主要解析插件为4类:
   1、内置插件: builtInPlugins(@vue/cli-serve内部提供的插件,大致分为两类)
        --a、动态注入CLI命令的插件,serve、build、inspect、help 这种可以通过npm script运行的指令
        --b、对webpack构建的相关配置,base、css、dev、prod @vue/cli-serve将webpack构建功能集成到内部,并暴露相关勾子来允许自定义配置
   2、inlinePlugins:在通过vue serve、vue build命令构建一个Service实例的时候传入的插件
   3package.josn插件:package.json的devDependencies 和 dependencies 中的 vue 插件 : @vue/cli-plugin-babel (通过isPlugin进行过滤)
   4package.vuePlugins; 在package.json里面的插件,但是不需要加载完整的插件,只需要调用插件的api,放在这里面,在package.json文件中有一个vuePlugins属性可以进行配置
resolvePlugins (inlinePlugins, useBuiltIn) {
    const idToPlugin = (id, absolutePath) => ({
      id: id.replace(/^.\//, 'built-in:'),
      apply: require(absolutePath || id)
    })

    let plugins

    // 内置插件
    const builtInPlugins = [
      './commands/serve',
      './commands/build',
      './commands/inspect',
      './commands/help',
      // config plugins are order sensitive
      './config/base',
      './config/assets',
      './config/css',
      './config/prod',
      './config/app'
    ].map((id) => idToPlugin(id))

    if (inlinePlugins) {
      plugins = useBuiltIn !== false
        ? builtInPlugins.concat(inlinePlugins)
        : inlinePlugins
    } else {
      const projectPlugins = Object.keys(this.pkg.devDependencies || {})
        .concat(Object.keys(this.pkg.dependencies || {}))
        .filter(isPlugin) // 通过正则来判断是不是插件 /^(@vue\/|vue-|@[\w-]+(\.)?[\w-]+\/vue-)cli-plugin-/
        .map(id => {
          if (
            this.pkg.optionalDependencies &&
            id in this.pkg.optionalDependencies
          ) {
            let apply = loadModule(id, this.pkgContext) // 加载package.json中的插件
            if (!apply) {
              warn(`Optional dependency ${id} is not installed.`)
              apply = () => {}
            }

            return { id, apply }
          } else {
            return idToPlugin(id, resolveModule(id, this.pkgContext))
          }
        })

      plugins = builtInPlugins.concat(projectPlugins)
      console.log('plugins', plugins);
    }

    // Local plugins
    if (this.pkg.vuePlugins && this.pkg.vuePlugins.service) {
      const files = this.pkg.vuePlugins.service
      if (!Array.isArray(files)) {
        throw new Error(`Invalid type for option 'vuePlugins.service', expected 'array' but got ${typeof files}.`)
      }
      plugins = plugins.concat(files.map(file => ({
        id: `local:${file}`,
        apply: loadModule(`./${file}`, this.pkgContext)
      })))
    }
    debug('vue:plugins')(plugins)

    const orderedPlugins = sortPlugins(plugins)
    debug('vue:plugins-ordered')(orderedPlugins)

    return orderedPlugins
  }

2.2 run函数解析

run函数主要做了4件事情:

  • 获取当前环境模式,以便执行init函数的时候获取环境配置
  • 设置需要跳过的插件,在执行init函数的时候不会加载
  • 执行init函数,进行一些配置解析,插件加载等功能(下文会细说)
  • 校验命令合法性,并启动项目

2.2.1 获取当前环境模式

优先使用命令配置的模式,然后才根据参数(serve、build…)使用构造函数内置的模式,如果当前执行的是build命令,并且配置了watch参数,则回退为dev模式

const mode = args.mode || (name === 'build' && args.watch ? 'development' : this.modes[name])

2.2.2 设置不用完全加载插件

通过解析终端命令之后,会得到skip-plugins的值,会将其转换为数组并加入到pluginsToSkip数组中,在init加载插件时会跳过这些插件,并将skip-plugins从args、rawArgv中删除

setPluginsToSkip (args, rawArgv) {
    let skipPlugins = args['skip-plugins'] // 在启动期间不需要运行的插件
    const pluginsToSkip = new Set()
    if (skipPlugins) {
      // When only one appearence, convert to array to prevent duplicate code
      if (!Array.isArray(skipPlugins)) {
        skipPlugins = Array.from([skipPlugins])
      }
      // Iter over all --skip-plugins appearences
      for (const value of skipPlugins.values()) {
        for (const plugin of value.split(',').map(id => resolvePluginId(id))) {
          pluginsToSkip.add(plugin)
        }
      }
    }
    this.pluginsToSkip = pluginsToSkip

    delete args['skip-plugins']
    // Delete all --skip-plugin appearences
    let index
    while ((index = rawArgv.indexOf('--skip-plugins')) > -1) {
      rawArgv.splice(index, 2) // Remove the argument and its value
    }
  }

2.2.3 执行init函数
init函数主要做了如下几件事情:

  • 根据模式加载环境配置
  • 加载用户配置
  • 加载插件
  • 加载webpack配置

其中loadedUserOptions是将vue.config.js、vue.config.cjs、vue.config.mjs这几个文件的配置进行解析合并,获取到用户配置的所有信息。
PluginAPI类是给函数注入插件的api,因为我们知道cli指令和配置指令都是基于插件动态注入的。

init (mode = process.env.VUE_CLI_MODE) {
    if (this.initialized) {
      return
    }
    this.initialized = true
    this.mode = mode

    // load mode .env
    // 环境变量文件加载顺序: 特定模式环境文件(.env.development.local > .env.development) -》 一般环境文件(.env.local > .env),环境文件不可被覆盖,所以特定模式的优先级高
    if (mode) { // 先加载指定mode的环境配置
      this.loadEnv(mode)
    }
    // load base .env // 后加载默认的环境配置 - 会不会覆盖指定的环境配置? - 不会,环境变量不会被覆盖
    this.loadEnv()

    // load user config
    const userOptions = this.loadUserOptions()
    const loadedCallback = (loadedUserOptions) => {
      this.projectOptions = defaultsDeep(loadedUserOptions, defaults()) // 合并配置并深拷贝一份

      debug('vue:project-config')(this.projectOptions)

      // apply plugins. 执行插件
      this.plugins.forEach(({ id, apply }) => {
        if (this.pluginsToSkip.has(id)) return
        // apply就是service函数导入的一个函数
        // service 插件接受两个参数,一个 PluginAPI 实例,一个包含 vue.config.js 内指定的项目本地选项的对象,或者在 package.json 内的 vue 字段。
        apply(new PluginAPI(id, this), this.projectOptions)
      })

      // apply webpack configs from project config file
      // 获取webpack的配置
      if (this.projectOptions.chainWebpack) {
        this.webpackChainFns.push(this.projectOptions.chainWebpack)
      }
      if (this.projectOptions.configureWebpack) {
        this.webpackRawConfigFns.push(this.projectOptions.configureWebpack)
      }
    }

    if (isPromise(userOptions)) {
      return userOptions.then(loadedCallback)
    } else {
      return loadedCallback(userOptions)
    }
  }

2.2.4 校验命令合法性,并启动项目

校验输入的指令是否合法,并在args、rawArgv删除改指令,比如我们输入npm run serve 这里就是执行的注入的serve函数 -> node_modules/@vue/cli-service/lib/commands/serve.js

args._ = args._ || []
    let command = this.commands[name]
    if (!command && name) {
      error(`command "${name}" does not exist.`)
      process.exit(1)
    }
    if (!command || args.help || args.h) {
      command = this.commands.help
    } else {
      args._.shift() // remove command itself
      rawArgv.shift()
    }
    const { fn } = command
    return fn(args, rawArgv)

至此完成了vue-serve-cli部分,之后就到执行serve函数来最终执行完成vue-cli-service serve的命令。

有兴趣的朋友可以关注一下公众号,主要分享前端相关的小知识,方便随时随地一起交流学习。
在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值