@vue/cli-service version:3.1.2 development 模式源碼解讀

2018-11-04 06:30:00 Cloudy and rainy
又是一個(gè)美好的周末撒桨,早晨六點(diǎn)半就沒(méi)有了睡意煤禽,起床,穿衣服振亮,刷牙(突然想起來(lái)巧还,下周要開(kāi)始新項(xiàng)目了,基礎(chǔ)的前端架構(gòu)該如何搭建呢坊秸?繼續(xù)使用ivew-admin麸祷?好像是挺雞賊的,不過(guò)感覺(jué)用多了褒搔,有點(diǎn)兒弱弱的味道阶牍。嗯,快速地刷完牙星瘾,打開(kāi)電腦走孽,clone下來(lái)之前寫好的的vue-admin,準(zhǔn)備愉快地寫寫代碼琳状,然后就發(fā)生了接下來(lái)的一切......)
入口文件main.js 里面有這么一行代碼:

if (process.env.NODE_ENV !== 'production') require('@/mock')

process.env.NODE_ENV 是什么鬼磕瓷,又是如何設(shè)置為production的呢?我大概翻了翻目錄念逞,發(fā)現(xiàn)沒(méi)有類似env的配置文件生宛,于是我走向了一條不歸路....
項(xiàng)目中的package.json如下所示:

"scripts": {
    "dev": "vue-cli-service serve --open",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint",
    "test:unit": "vue-cli-service test:unit",
    "test:e2e": "vue-cli-service test:e2e"
  },

在node_modules里面找到@vue/cli-service/bin 下的vue-cli-service.js

#!/usr/bin/env node

const semver = require('semver')
const { error } = require('@vue/cli-shared-utils')
const requiredVersion = require('../package.json').engines.node
if (!semver.satisfies(process.version, requiredVersion)) {
  error(
    `You are using Node ${process.version}, but vue-cli-service ` +
    `requires Node ${requiredVersion}.\nPlease upgrade your Node version.`
  )
  process.exit(1)
}
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',
    'report-json',
    'watch',
    // serve
    'open',
    'copy',
    'https',
    // inspect
    'verbose'
  ]
})
const command = args._[0]
service.run(command, args, rawArgv).catch(err => {
  error(err)
  process.exit(1)
})

嗯,大概瀏覽一遍肮柜,實(shí)例話了 Service 類陷舅,執(zhí)行了 run 函數(shù);process.env.VUE_CLI_CONTEXT 首先這玩意如果不定義是肯定 undefined 审洞,所以讀取了當(dāng)前項(xiàng)目的根目錄位置莱睁;


Service Class

  async run (name, args = {}, rawArgv = []) {
    // name = serve;args = {},芒澜;rawArgv = []
    const mode = args.mode || (name === 'build' && args.watch ? 'development' : this.modes[name])
    // mode的取值應(yīng)該是this.modes[name]仰剿,那么讓我們看一下this.modes是什么(暫且忽略下面的代碼哈)
    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) {
      command = this.commands.help
    } else {
      args._.shift() // remove command itself
      rawArgv.shift()
    }
    const { fn } = command
    return fn(args, rawArgv)
  }

this.modes[name]

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 = {}
    // Folder containing the target package.json for plugins
    this.pkgContext = context
    // package.json containing the plugins
    this.pkg = this.resolvePkg(pkg)
    // If there are inline plugins, they will be used instead of those
    // found in package.json.
    // When useBuiltIn === false, built-in plugins are disabled. This is mostly
    // for testing.
    this.plugins = this.resolvePlugins(plugins, useBuiltIn)
    // resolve the default mode to use for each command
    // this is provided by plugins as module.exports.defaultModes
    // so we can get the information without actually applying the plugin.
    this.modes = this.plugins.reduce((modes, { apply: { defaultModes }}) => {
      return Object.assign(modes, defaultModes)
    }, {})
  }

在最后一行看到 this.modes 進(jìn)行了一個(gè)初始賦值,來(lái)源是 this.plugins痴晦, this.plugins 又來(lái)源于 this.resolvePlugins(plugins, useBuiltIn)南吮,讓我們看一下這個(gè)函數(shù)的內(nèi)容,同時(shí)需要注意的是誊酌,this.resolvePlugins(plugins, useBuiltIn) 的第一個(gè)參數(shù)是undefined

這里面引入了isPlugin函數(shù)部凑,我也貼進(jìn)來(lái)
//const pluginRE = /^(@vue\/|vue-|@[\w-]+\/vue-)cli-plugin-/
//exports.isPlugin = id => pluginRE.test(id)

resolvePlugins (inlinePlugins, useBuiltIn) {
    const idToPlugin = id => ({
      id: id.replace(/^.\//, 'built-in:'),
      apply: require(id)
    })

    let plugins

    const builtInPlugins = [
      './commands/serve',
      './commands/build',
      './commands/inspect',
      './commands/help',
      // config plugins are order sensitive
      './config/base',
      './config/css',
      './config/dev',
      './config/prod',
      './config/app'
    ].map(idToPlugin) // 這一步露乏,把內(nèi)置的模塊進(jìn)行拼裝成id,apply(是一個(gè)require)的形式
   // 代碼看到這里涂邀,我們大概可以知道瘟仿,builtInPlugins就是一個(gè)二維數(shù)組,每一項(xiàng)都有一個(gè)id和apply函數(shù)
   // 由形參可以知道比勉,下面會(huì)走else的代碼
    if (inlinePlugins) {
      plugins = useBuiltIn !== false
        ? builtInPlugins.concat(inlinePlugins)
        : inlinePlugins
    } else {
      // this.pkg就是package.json(項(xiàng)目的)的json結(jié)構(gòu)劳较,感興趣了可以自己去看看代碼如何獲取的就行了
      const projectPlugins = Object.keys(this.pkg.devDependencies || {})
        .concat(Object.keys(this.pkg.dependencies || {}))
        .filter(isPlugin)  // 過(guò)濾了咱們的包里面是@vue/cli-plugin 插件的
        .map(id => {
        // 下面代碼也是走的else
          if (
            this.pkg.optionalDependencies &&
            id in this.pkg.optionalDependencies
          ) {
            let apply = () => {}
            try {
              apply = require(id)
            } catch (e) {
              warn(`Optional dependency ${id} is not installed.`)
            }
            return { id, apply }
          } else {
            return idToPlugin(id)
          }
        })
      plugins = builtInPlugins.concat(projectPlugins) // 合并了builtIn插件和project插件
    }
    ******** (暫時(shí)省略這么多代碼)
  }

看到這里,我們可以知道 resolvePlugins 返回的就是一個(gè)數(shù)組浩聋,數(shù)組的每一項(xiàng)分別是id和require的內(nèi)容(這個(gè)后面是需要了解的观蜗,但是我們暫且不做分析,繼續(xù)往下走)
回到this.modes[name]剛才的邏輯中:

this.modes = this.plugins.reduce((modes, { apply: { defaultModes }}) => {
      return Object.assign(modes, defaultModes)
    }, {})

apply不是函數(shù)么衣洁?怎么又出來(lái)一個(gè)defaultModes嫂便,這讓我好慌啊闸与!我們就隨便找一個(gè)去看看為啥把。
@vue/cli-service/lib/commands/serve 挑出來(lái)它看一下把岸售,翻回文件的末尾:

module.exports.defaultModes = {
  serve: 'development'
}

好吧践樱,原來(lái)除了暴露了一個(gè)函數(shù),還寫了一個(gè)defaultModes的對(duì)象
最終this.modes是具有defaultModes的插件的一個(gè)集合凸丸,感興趣的可以自行打印出來(lái)看一下,拷邢,大概是這樣

{ serve: 'development',
  build: 'production',
  inspect: 'development' }

分析了這么久,才走了一行代碼.....
下一步是一個(gè)this.init(mode)屎慢,mode是development字符串瞭稼;看一下init的代碼把:

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

    if (mode) {   // 執(zhí)行這一步驟
      this.loadEnv(mode)
    }
    // load base .env
    this.loadEnv()
    // load user config
    const userOptions = this.loadUserOptions()
    this.projectOptions = defaultsDeep(userOptions, defaults())
    debug('vue:project-config')(this.projectOptions)
    // apply plugins.
    this.plugins.forEach(({ id, apply }) => {
      apply(new PluginAPI(id, this), this.projectOptions)
    })
    // apply webpack configs from project config file
    if (this.projectOptions.chainWebpack) {
      this.webpackChainFns.push(this.projectOptions.chainWebpack)
    }
    if (this.projectOptions.configureWebpack) {
      this.webpackRawConfigFns.push(this.projectOptions.configureWebpack)
    }
  }

loadEnv (mode) {
    const logger = debug('vue:env')
    const basePath = path.resolve(this.context, `.env${mode ? `.${mode}` : ``}`)
    const localPath = `${basePath}.local`

    const load = path => {
      try {
        const res = loadEnv(path)
        logger(path, res)
      } catch (err) {
        // only ignore error if file is not found
        if (err.toString().indexOf('ENOENT') < 0) {
          error(err)
        }
      }
    }
    load(localPath)
    load(basePath)
    // by default, NODE_ENV and BABEL_ENV are set to "development" unless mode
    // is production or test. However the value in .env files will take higher
    // priority.
    if (mode) {
      // always set NODE_ENV during tests
      // as that is necessary for tests to not be affected by each other
      const shouldForceDefaultEnv = (
        process.env.VUE_CLI_TEST &&
        !process.env.VUE_CLI_TEST_TESTING_ENV
      )
      const defaultNodeEnv = (mode === 'production' || mode === 'test')
        ? mode
        : 'development'
      if (shouldForceDefaultEnv || process.env.NODE_ENV == null) {
        process.env.NODE_ENV = defaultNodeEnv
      }
      if (shouldForceDefaultEnv || process.env.BABEL_ENV == null) {
        process.env.BABEL_ENV = defaultNodeEnv
      }
    }
  }

// 非類的函數(shù)loadEnv
module.exports = function loadEnv (path = '.env') {
  const config = parse(fs.readFileSync(path, 'utf-8'))
  Object.keys(config).forEach(key => {
    if (typeof process.env[key] === 'undefined') {
      process.env[key] = config[key]
    }
  })
  return config
}

核心的步驟就是readFileSync一下 .env.env.local內(nèi)容,然后賦值給 process.env腻惠,不過(guò)關(guān)于.env這一塊兒我暫時(shí)沒(méi)看明白环肘,隨后再看;然后開(kāi)始執(zhí)行

this.plugins.forEach(({ id, apply }) => {
   apply(new PluginAPI(id, this), this.projectOptions)
})

在我們看PluginAPI構(gòu)造函數(shù)之前集灌,我們還是線看一下plugins里面apply是干了啥把悔雹。

module.exports = (api, options) => {
  api.registerCommand('serve', {
    description: 'start development server',
    usage: 'vue-cli-service serve [options] [entry]',
    options: {
      '--open': `open browser on server start`,
      '--copy': `copy url to clipboard on server start`,
      '--mode': `specify env mode (default: development)`,
      '--host': `specify host (default: ${defaults.host})`,
      '--port': `specify port (default: ${defaults.port})`,
      '--https': `use https (default: ${defaults.https})`,
      '--public': `specify the public network URL for the HMR client`
    }
  }, async function serve (args) {
    info('Starting development server...')

    // although this is primarily a dev server, it is possible that we
    // are running it in a mode with a production env, e.g. in E2E tests.
    const isInContainer = checkInContainer()
    const isProduction = process.env.NODE_ENV === 'production'
      // 下面刪除的代碼比較多,可能大括號(hào)不對(duì)應(yīng)哈
    }

前面說(shuō)到欣喧,apply就是一個(gè)require嘛腌零,內(nèi)容就是這個(gè)暴露的函數(shù),含有兩個(gè)形參唆阿,一個(gè)是api益涧,一個(gè)是option,形參api應(yīng)該就是PluginAPI實(shí)例化后的一個(gè)方法了驯鳖,看一下PluginAPI的代碼:

class PluginAPI {
  /**
   * @param {string} id - Id of the plugin.
   * @param {Service} service - A vue-cli-service instance.
   */
  constructor (id, service) {
    this.id = id
    this.service = service
  }
 registerCommand (name, opts, fn) {
    if (typeof opts === 'function') {
      fn = opts
      opts = null
    }
    this.service.commands[name] = { fn, opts: opts || {}}  // this值得是Service實(shí)例闲询,主要是給實(shí)例的service的commands賦值
  }
  ********* // 有刪減
}

截至到現(xiàn)在久免,應(yīng)該至少知道了this.service.commands['serve']的值是{ fn: async 函數(shù),opts: 那一堆配置 }
然后在Servicerun函數(shù)知道嘹裂,async函數(shù)就要執(zhí)行了妄壶,傳遞了兩個(gè)參數(shù),argsrawArgv 然后就是webpack自己的事情了寄狼,開(kāi)啟服務(wù)啊丁寄,熱更新啊什么亂七八糟的...

感覺(jué)自己寫的不太好,后續(xù)再細(xì)讀修改把泊愧!美好的一天要開(kāi)始了

2018-11-01 10:46:00 Sleep

(完)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末伊磺,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子删咱,更是在濱河造成了極大的恐慌屑埋,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,743評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件痰滋,死亡現(xiàn)場(chǎng)離奇詭異摘能,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)敲街,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,296評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門团搞,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人多艇,你說(shuō)我怎么就攤上這事逻恐。” “怎么了峻黍?”我有些...
    開(kāi)封第一講書人閱讀 157,285評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵复隆,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我姆涩,道長(zhǎng)挽拂,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 56,485評(píng)論 1 283
  • 正文 為了忘掉前任骨饿,我火速辦了婚禮轻局,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘样刷。我一直安慰自己仑扑,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,581評(píng)論 6 386
  • 文/花漫 我一把揭開(kāi)白布置鼻。 她就那樣靜靜地躺著镇饮,像睡著了一般。 火紅的嫁衣襯著肌膚如雪箕母。 梳的紋絲不亂的頭發(fā)上储藐,一...
    開(kāi)封第一講書人閱讀 49,821評(píng)論 1 290
  • 那天俱济,我揣著相機(jī)與錄音,去河邊找鬼钙勃。 笑死蛛碌,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的辖源。 我是一名探鬼主播蔚携,決...
    沈念sama閱讀 38,960評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼克饶!你這毒婦竟也來(lái)了酝蜒?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書人閱讀 37,719評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤矾湃,失蹤者是張志新(化名)和其女友劉穎亡脑,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體邀跃,經(jīng)...
    沈念sama閱讀 44,186評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡霉咨,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,516評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了拍屑。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片途戒。...
    茶點(diǎn)故事閱讀 38,650評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖丽涩,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情裁蚁,我是刑警寧澤矢渊,帶...
    沈念sama閱讀 34,329評(píng)論 4 330
  • 正文 年R本政府宣布,位于F島的核電站枉证,受9級(jí)特大地震影響矮男,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜室谚,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,936評(píng)論 3 313
  • 文/蒙蒙 一毡鉴、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧秒赤,春花似錦猪瞬、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 30,757評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至潮售,卻和暖如春痊项,著一層夾襖步出監(jiān)牢的瞬間锅风,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 31,991評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工鞍泉, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留皱埠,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,370評(píng)論 2 360
  • 正文 我出身青樓咖驮,卻偏偏與公主長(zhǎng)得像边器,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子游沿,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,527評(píng)論 2 349

推薦閱讀更多精彩內(nèi)容

  • 在官方文檔中饰抒,我們可以看到在新版本的 Vue CLI 中去掉了我們熟悉的 `config` 目錄 比如之前在 pr...
    dailyvuejs閱讀 3,648評(píng)論 0 2
  • //Clojure入門教程: Clojure – Functional Programming for the J...
    葡萄喃喃囈語(yǔ)閱讀 3,629評(píng)論 0 7
  • Spring Cloud為開(kāi)發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見(jiàn)模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn)诀黍,斷路器袋坑,智...
    卡卡羅2017閱讀 134,633評(píng)論 18 139
  • 今天冰姐做一只巨型雪納瑞,特別不老實(shí)還張嘴咬人眯勾,冰姐在做的過(guò)程中也險(xiǎn)些被咬到枣宫,但是冰姐并沒(méi)有著急或是生氣,而是很有...
    王德彪閱讀 146評(píng)論 0 1
  • 徐行 20160810 說(shuō)起父親吃环,每一個(gè)人都有也颤,轉(zhuǎn)眼間,父親去年馬上就七周年了郁轻,時(shí)光飛快翅娶,只要閑下來(lái)的時(shí)候,腦海當(dāng)...
    徐行我看行閱讀 208評(píng)論 0 0