@vue/cli-service version:3.1.2 development 模式源码解读

2018-11-04 06:30:00 Cloudy and rainy
又是一个美好的周末,早晨六点半就没有了睡意,起床,穿衣服,刷牙(突然想起来,下周要开始新项目了,基础的前端架构该如何搭建呢?继续使用ivew-admin?好像是挺鸡贼的,不过感觉用多了,有点儿弱弱的味道。嗯,快速地刷完牙,打开电脑,clone下来之前写好的的vue-admin,准备愉快地写写代码,然后就发生了接下来的一切......)
入口文件main.js 里面有这么一行代码:

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

process.env.NODE_ENV 是什么鬼,又是如何设置为production的呢?我大概翻了翻目录,发现没有类似env的配置文件,于是我走向了一条不归路....
项目中的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)
})

嗯,大概浏览一遍,实例话了 Service 类,执行了 run 函数;process.env.VUE_CLI_CONTEXT 首先这玩意如果不定义是肯定 undefined ,所以读取了当前项目的根目录位置;


Service Class

  async run (name, args = {}, rawArgv = []) {
    // name = serve;args = {},;rawArgv = []
    const mode = args.mode || (name === 'build' && args.watch ? 'development' : this.modes[name])
    // mode的取值应该是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 进行了一个初始赋值,来源是 this.pluginsthis.plugins 又来源于 this.resolvePlugins(plugins, useBuiltIn),让我们看一下这个函数的内容,同时需要注意的是,this.resolvePlugins(plugins, useBuiltIn) 的第一个参数是undefined

这里面引入了isPlugin函数,我也贴进来
//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) // 这一步,把内置的模块进行拼装成id,apply(是一个require)的形式
   // 代码看到这里,我们大概可以知道,builtInPlugins就是一个二维数组,每一项都有一个id和apply函数
   // 由形参可以知道,下面会走else的代码
    if (inlinePlugins) {
      plugins = useBuiltIn !== false
        ? builtInPlugins.concat(inlinePlugins)
        : inlinePlugins
    } else {
      // this.pkg就是package.json(项目的)的json结构,感兴趣了可以自己去看看代码如何获取的就行了
      const projectPlugins = Object.keys(this.pkg.devDependencies || {})
        .concat(Object.keys(this.pkg.dependencies || {}))
        .filter(isPlugin)  // 过滤了咱们的包里面是@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插件
    }
    ******** (暂时省略这么多代码)
  }

看到这里,我们可以知道 resolvePlugins 返回的就是一个数组,数组的每一项分别是id和require的内容(这个后面是需要了解的,但是我们暂且不做分析,继续往下走)
回到this.modes[name]刚才的逻辑中:

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

apply不是函数么?怎么又出来一个defaultModes,这让我好慌啊!我们就随便找一个去看看为啥把。
@vue/cli-service/lib/commands/serve 挑出来它看一下把,翻回文件的末尾:

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

好吧,原来除了暴露了一个函数,还写了一个defaultModes的对象
最终this.modes是具有defaultModes的插件的一个集合,感兴趣的可以自行打印出来看一下,,大概是这样

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

分析了这么久,才走了一行代码.....
下一步是一个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) {   // 执行这一步骤
      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
      }
    }
  }

// 非类的函数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内容,然后赋值给 process.env,不过关于.env这一块儿我暂时没看明白,随后再看;然后开始执行

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

在我们看PluginAPI构造函数之前,我们还是线看一下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'
      // 下面删除的代码比较多,可能大括号不对应哈
    }

前面说到,apply就是一个require嘛,内容就是这个暴露的函数,含有两个形参,一个是api,一个是option,形参api应该就是PluginAPI实例化后的一个方法了,看一下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实例,主要是给实例的service的commands赋值
  }
  ********* // 有删减
}

截至到现在,应该至少知道了this.service.commands['serve']的值是{ fn: async 函数,opts: 那一堆配置 }
然后在Servicerun函数知道,async函数就要执行了,传递了两个参数,argsrawArgv 然后就是webpack自己的事情了,开启服务啊,热更新啊什么乱七八糟的...

感觉自己写的不太好,后续再细读修改把!美好的一天要开始了

2018-11-01 10:46:00 Sleep

(完)

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

推荐阅读更多精彩内容