vue-cli 插件機(jī)制實(shí)現(xiàn)方式

前言

我們知道,使用vue-cli創(chuàng)建的項(xiàng)目,其啟動(dòng)或者打包等命令是使用npm run serve或者npm run build等。而這些命令實(shí)際上執(zhí)行的是vue-cli-service serve、vue-cli-service build等。也就是這些都是執(zhí)行的vue-cli-service下的子命令。其插件化機(jī)制的實(shí)現(xiàn)也是在里面完成的。vue-cli-service的插件化機(jī)制與前邊分析的cli-ui的插件化機(jī)制類似。vue-cli ui插件化機(jī)制詳解。
為了幫助我們理解后續(xù)的邏輯,我們先看一下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) => {
     // ...
    // 具體邏輯實(shí)現(xiàn)
      await build(args, api, options)
    }
  });
  // ...
}

正文

我們?nèi)匀皇窍葟娜肟诳雌稹?br> 我們執(zhí)行vue-cli-service xxxxx實(shí)際上是使用node執(zhí)行了packages/@vue/cli-service/bin/vue-cli-service.js文件。我們就從此文件看起。

#!/usr/bin/env node

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

// ...

// 引入Service,并創(chuàng)建實(shí)例
const Service = require('../lib/Service')
const service = new Service(process.env.VUE_CLI_CONTEXT || process.cwd())

// 處理參數(shù)
const rawArgv = process.argv.slice(2)
const args = require('minimist')(rawArgv, {
  boolean: [
    // build
    'modern',
    'report',
    // ...
  ]
})
const command = args._[0]

// 執(zhí)行service實(shí)例上的run方法,將參數(shù)傳遞進(jìn)去
service.run(command, args, rawArgv).catch(err => {
  error(err)
  process.exit(1)
})

入口文件很簡單:

  • 創(chuàng)建Service實(shí)例
  • 處理命令行參數(shù)
  • 執(zhí)行實(shí)例上的run方法

主要邏輯都在Service類中實(shí)現(xiàn)。
我們接下來看一下具體實(shí)現(xiàn):

先看其構(gòu)造函數(shù):

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)
    /*
      如果使用了內(nèi)置插件,將會(huì)使用他們?nèi)ゴ鎝ackage.json中的插件;
      useBuildIn為false時(shí),內(nèi)置插件會(huì)被禁用(這個(gè)在大多時(shí)候是被用于測試)。
      在這里會(huì)去解析package.json中的依賴信息,將找到的插件require進(jìn)來,做一些簡單處理后放入this.plugins數(shù)組中保存
    */
    this.plugins = this.resolvePlugins(plugins, useBuiltIn)
    // 在運(yùn)行run方法時(shí),填充這個(gè)Set
    this.pluginsToSkip = new Set()
    // 為每個(gè)command解析默認(rèn)mode
    this.modes = this.plugins.reduce((modes, { apply: { defaultModes }}) => {
      return Object.assign(modes, defaultModes)
    }, {})
  }
// ...
}

構(gòu)造函數(shù)中做的事情主要是

  • 初始化一些變量
  • 加載插件信息(仍然是從依賴、以及package.json中的vuePlugins字段中加載,然后將其保存在一個(gè)數(shù)組中)。

回想一下,在入口處,創(chuàng)建插件實(shí)例之后,就是處理參數(shù),將其傳入run方法中去執(zhí)行。關(guān)鍵的就是run方法的實(shí)現(xiàn)。

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

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

    // 根據(jù)--skip-plugins設(shè)置pluginsToSkip
    this.setPluginsToSkip(args)

    // 加載環(huán)境變量、用戶配置、應(yīng)用插件。
    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方法本身非常簡單。

  • 他接受命令名稱、命令參數(shù)以及原生命令參數(shù)作為參數(shù)。
  • 執(zhí)行init方法之后,從this.commands上拿到當(dāng)前命令name對(duì)應(yīng)的命令信息對(duì)象command。
  • 然后將參數(shù)傳入command.fn函數(shù)去執(zhí)行。

到這里,使用vue-cli-service執(zhí)行子命令的過程結(jié)束。
可是,我們?nèi)匀粵]有看到是如何加載的插件。也不知道this.commands中的信息是如何而來的。
實(shí)際上,關(guān)鍵也正是在run函數(shù)中間的init方法內(nèi)完成的,也就是做了初始化的工作。
看下其具體實(shí)現(xiàn):

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

    // 加載當(dāng)前mode對(duì)應(yīng)的環(huán)境變量
    if (mode) {
      this.loadEnv(mode)
    }
    // 加載基礎(chǔ)環(huán)境變量
    this.loadEnv()

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

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

    // 從項(xiàng)目配置文件中應(yīng)用webpack配置
    if (this.projectOptions.chainWebpack) {
      this.webpackChainFns.push(this.projectOptions.chainWebpack)
    }
    if (this.projectOptions.configureWebpack) {
      this.webpackRawConfigFns.push(this.projectOptions.configureWebpack)
    }
  }

在init函數(shù)中,主要進(jìn)行了以下的過程:

  • 加載環(huán)境變量
  • 從vue.config.js中加載配置信息
  • 對(duì)在構(gòu)造函數(shù)中拿到的插件信息進(jìn)行遍歷,運(yùn)行插件
  • 將用戶的webpack配置加入到真正的webpack配置中

注意: 看運(yùn)行插件的方法,對(duì)于每一個(gè)插件都是新創(chuàng)建一個(gè)插件實(shí)例,將其傳入插件函數(shù)中去執(zhí)行。
而創(chuàng)建插件實(shí)例時(shí)是將this傳遞了進(jìn)去,也就是Service實(shí)例傳了過去,PluginApi實(shí)例內(nèi)的方法也是直接向傳過去的Service實(shí)例上去添加方法和屬性。
所有在運(yùn)行了這些插件之后,就會(huì)在當(dāng)前的Service實(shí)例上添加一系列的信息。

我們簡單看一下PluginApi的一些代碼片段:

class PluginAPI {
  // 第二個(gè)參數(shù)也就是我們?cè)赟ervice.init方法內(nèi)運(yùn)行插件時(shí)傳入的Service實(shí)例
  constructor (id, service) {
    this.id = id
    this.service = service
  }
  // 例如registerCommand方法,就是直接在Service實(shí)例上去添加信息
  registerCommand (name, opts, fn) {
    if (typeof opts === 'function') {
      fn = opts
      opts = null
    }
    this.service.commands[name] = { fn, opts: opts || {}}
  }
}

init方法執(zhí)行完畢后,所有插件的信息都已經(jīng)存在于Service實(shí)例上。

我們回到最初的起點(diǎn)。

執(zhí)行vue-cli-service xxx命令時(shí),實(shí)際進(jìn)行了如下過程的操作:

  • 創(chuàng)建Service實(shí)例;
  • 從package.json中加載插進(jìn)信息,將其保存在一個(gè)數(shù)組中;
  • 循環(huán)遍歷插件數(shù)組,對(duì)于每一個(gè)都將生成一個(gè)PluginApi實(shí)例,將其傳入插件函數(shù)中去執(zhí)行(執(zhí)行完畢就會(huì)在Service實(shí)例上添加一些信息);
  • 拿到xxx命令對(duì)應(yīng)的信息,將命令行參數(shù)傳入xxx命令對(duì)應(yīng)的處理函數(shù)中去執(zhí)行。

vue-cli 的插件化機(jī)制到此結(jié)束。

對(duì)比一下,我們發(fā)現(xiàn),該過程與我們前面所分析的cli-ui的插件化過程非常相似。甚至過程業(yè)基本如出一轍,但ui插件化的實(shí)現(xiàn)比這里稍微復(fù)雜一點(diǎn)點(diǎn)。(主要是ui插件化的代碼比較分散,代碼量較多,各個(gè)過程牽扯比較復(fù)雜,開始時(shí)難以分析。)
總的來說,cli插件化與cli-ui插件化的過程是一致的。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • ## 框架和庫的區(qū)別?> 框架(framework):一套完整的軟件設(shè)計(jì)架構(gòu)和**解決方案**。> > 庫(lib...
    Rui_bdad閱讀 3,155評(píng)論 1 4
  • 你很久沒有晨讀了,也很久沒有點(diǎn)開五班小灶群,你的心里有很多疑慮,有時(shí)候也覺得很慌張,但你就這么茫茫然過來了。 你可...
    檀子_閱讀 1,189評(píng)論 77 66
  • 先來個(gè)簡單的說明,為什么來到《簡書》app和寫(手機(jī))這個(gè)主題:接觸這個(gè)app很簡單,是在微信里一個(gè)公眾號(hào)上面...
    Uni招財(cái)貓閱讀 299評(píng)論 0 1
  • 人這一生每時(shí)每刻都在演戲 戲里戲外都在扮演著不同的角色 人生在世但不能演戲作假 很多年之前 他被他人拐跑了 一直都...
    揀書悅讀閱讀 262評(píng)論 1 3
  • 早上好!靜暖人生:每日一句正能量[玫瑰][玫瑰][玫瑰] (2019年3月31日 農(nóng)歷二月二十五 星期日) 心態(tài)一...
    俠姐27687閱讀 191評(píng)論 0 2

友情鏈接更多精彩內(nèi)容