vue-cli 插件机制实现方式

前言

我们知道,使用vue-cli创建的项目,其启动或者打包等命令是使用npm run serve或者npm run build等。而这些命令实际上执行的是vue-cli-service servevue-cli-service build等。也就是这些都是执行的vue-cli-service下的子命令。其插件化机制的实现也是在里面完成的。vue-cli-service的插件化机制与前边分析的cli-ui的插件化机制类似。vue-cli ui插件化机制详解
为了帮助我们理解后续的逻辑,我们先看一下cli插件的写法(和ui插件非常相似):

module.exports = (api, options) => {
  api.registerCommand('build', {
    description: 'build for production',
    usage: 'vue-cli-service build [options] [entry|pattern]',
    options: {
      '--mode': `specify env mode (default: production)`,
     // ...
    }
  }, async (args, rawArgs) => {
     // ...
    // 具体逻辑实现
      await build(args, api, options)
    }
  });
  // ...
}

正文

我们仍然是先从入口看起。
我们执行vue-cli-service xxxxx实际上是使用node执行了packages/@vue/cli-service/bin/vue-cli-service.js文件。我们就从此文件看起。

#!/usr/bin/env node

const { semver, error } = require('@vue/cli-shared-utils')

// ...

// 引入Service,并创建实例
const Service = require('../lib/Service')
const service = new Service(process.env.VUE_CLI_CONTEXT || process.cwd())

// 处理参数
const rawArgv = process.argv.slice(2)
const args = require('minimist')(rawArgv, {
  boolean: [
    // build
    'modern',
    'report',
    // ...
  ]
})
const command = args._[0]

// 执行service实例上的run方法,将参数传递进去
service.run(command, args, rawArgv).catch(err => {
  error(err)
  process.exit(1)
})

入口文件很简单:

  • 创建Service实例
  • 处理命令行参数
  • 执行实例上的run方法

主要逻辑都在Service类中实现。
我们接下来看一下具体实现:

先看其构造函数:

module.exports = class Service {
  constructor (context, { plugins, pkg, inlineOptions, useBuiltIn } = {}) {
    // 初始化一系列的值
    process.VUE_CLI_SERVICE = this
    this.initialized = false
    this.context = context
    this.inlineOptions = inlineOptions
    this.webpackChainFns = []
    this.webpackRawConfigFns = []
    this.devServerConfigFns = []
    this.commands = {}
    // 从目录下边的package.json中解析插件信息
    this.pkgContext = context
    this.pkg = this.resolvePkg(pkg)
    /*
      如果使用了内置插件,将会使用他们去代替package.json中的插件;
      useBuildIn为false时,内置插件会被禁用(这个在大多时候是被用于测试)。
      在这里会去解析package.json中的依赖信息,将找到的插件require进来,做一些简单处理后放入this.plugins数组中保存
    */
    this.plugins = this.resolvePlugins(plugins, useBuiltIn)
    // 在运行run方法时,填充这个Set
    this.pluginsToSkip = new Set()
    // 为每个command解析默认mode
    this.modes = this.plugins.reduce((modes, { apply: { defaultModes }}) => {
      return Object.assign(modes, defaultModes)
    }, {})
  }
// ...
}

构造函数中做的事情主要是

  • 初始化一些变量
  • 加载插件信息(仍然是从依赖、以及package.json中的vuePlugins字段中加载,然后将其保存在一个数组中)。

回想一下,在入口处,创建插件实例之后,就是处理参数,将其传入run方法中去执行。关键的就是run方法的实现。

下面就来看一下run方法中做了什么:

 async run (name, args = {}, rawArgv = []) {
    // 拿到当前的mode
    const mode = args.mode || (name === 'build' && args.watch ? 'development' : this.modes[name])

    // 根据--skip-plugins设置pluginsToSkip
    this.setPluginsToSkip(args)

    // 加载环境变量、用户配置、应用插件。
    this.init(mode)

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

可以看到run方法本身非常简单。

  • 他接受命令名称、命令参数以及原生命令参数作为参数。
  • 执行init方法之后,从this.commands上拿到当前命令name对应的命令信息对象command
  • 然后将参数传入command.fn函数去执行。

到这里,使用vue-cli-service执行子命令的过程结束。
可是,我们仍然没有看到是如何加载的插件。也不知道this.commands中的信息是如何而来的。
实际上,关键也正是在run函数中间的init方法内完成的,也就是做了初始化的工作。
看下其具体实现:

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

    // 加载当前mode对应的环境变量
    if (mode) {
      this.loadEnv(mode)
    }
    // 加载基础环境变量
    this.loadEnv()

    // 加载用户配置,也就是在vue.config.js中的定义。
    const userOptions = this.loadUserOptions()
    this.projectOptions = defaultsDeep(userOptions, defaults())

    // 应用插件
    this.plugins.forEach(({ id, apply }) => {
      if (this.pluginsToSkip.has(id)) return
      apply(new PluginAPI(id, this), this.projectOptions)
    })

    // 从项目配置文件中应用webpack配置
    if (this.projectOptions.chainWebpack) {
      this.webpackChainFns.push(this.projectOptions.chainWebpack)
    }
    if (this.projectOptions.configureWebpack) {
      this.webpackRawConfigFns.push(this.projectOptions.configureWebpack)
    }
  }

在init函数中,主要进行了以下的过程:

  • 加载环境变量
  • 从vue.config.js中加载配置信息
  • 对在构造函数中拿到的插件信息进行遍历,运行插件
  • 将用户的webpack配置加入到真正的webpack配置中

注意: 看运行插件的方法,对于每一个插件都是新创建一个插件实例,将其传入插件函数中去执行。
而创建插件实例时是将this传递了进去,也就是Service实例传了过去,PluginApi实例内的方法也是直接向传过去的Service实例上去添加方法和属性。
所有在运行了这些插件之后,就会在当前的Service实例上添加一系列的信息。

我们简单看一下PluginApi的一些代码片段:

class PluginAPI {
  // 第二个参数也就是我们在Service.init方法内运行插件时传入的Service实例
  constructor (id, service) {
    this.id = id
    this.service = service
  }
  // 例如registerCommand方法,就是直接在Service实例上去添加信息
  registerCommand (name, opts, fn) {
    if (typeof opts === 'function') {
      fn = opts
      opts = null
    }
    this.service.commands[name] = { fn, opts: opts || {}}
  }
}

init方法执行完毕后,所有插件的信息都已经存在于Service实例上。

我们回到最初的起点。

执行vue-cli-service xxx命令时,实际进行了如下过程的操作:

  • 创建Service实例;
  • 从package.json中加载插进信息,将其保存在一个数组中;
  • 循环遍历插件数组,对于每一个都将生成一个PluginApi实例,将其传入插件函数中去执行(执行完毕就会在Service实例上添加一些信息);
  • 拿到xxx命令对应的信息,将命令行参数传入xxx命令对应的处理函数中去执行。

vue-cli 的插件化机制到此结束。

对比一下,我们发现,该过程与我们前面所分析的cli-ui的插件化过程非常相似。甚至过程业基本如出一辙,但ui插件化的实现比这里稍微复杂一点点。(主要是ui插件化的代码比较分散,代码量较多,各个过程牵扯比较复杂,开始时难以分析。)
总的来说,cli插件化与cli-ui插件化的过程是一致的。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,463评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,868评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,213评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,666评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,759评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,725评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,716评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,484评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,928评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,233评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,393评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,073评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,718评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,308评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,538评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,338评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,260评论 2 352

推荐阅读更多精彩内容

  • ## 框架和库的区别?> 框架(framework):一套完整的软件设计架构和**解决方案**。> > 库(lib...
    Rui_bdad阅读 2,903评论 1 4
  • 你很久没有晨读了,也很久没有点开五班小灶群,你的心里有很多疑虑,有时候也觉得很慌张,但你就这么茫茫然过来了。 你可...
    檀子_阅读 1,128评论 77 66
  • 先来个简单的说明,为什么来到《简书》app和写(手机)这个主题:接触这个app很简单,是在微信里一个公众号上面...
    Uni招财猫阅读 279评论 0 1
  • 人这一生每时每刻都在演戏 戏里戏外都在扮演着不同的角色 人生在世但不能演戏作假 很多年之前 他被他人拐跑了 一直都...
    拣书悦读阅读 194评论 1 3
  • 早上好!静暖人生:每日一句正能量[玫瑰][玫瑰][玫瑰] (2019年3月31日 农历二月二十五 星期日) 心态一...
    侠姐27687阅读 169评论 0 2