vue-cli系列之vue-cli-service整体架构浅析


Posted in Javascript onJanuary 14, 2019

概述

vue启动一个项目的时候,需要执行npm run serve,其中这个serve的内容就是vue-cli-service serve。可见,项目的启动关键是这个vue-cli-service与它的参数serve。接下来我们一起看看service中主要写了什么东东(主要内容以备注形式写到代码中。)。

关键代码

vue-cli-service.js

const semver = require('semver')
const { error } = require('@vue/cli-shared-utils')
const requiredVersion = require('../package.json').engines.node

// 检测node版本是否符合vue-cli运行的需求。不符合则打印错误并退出。
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)
}

// cli-service的核心类。
const Service = require('../lib/Service')
// 新建一个service的实例。并将项目路径传入。一般我们在项目根路径下运行该cli命令。所以process.cwd()的结果一般是项目根路径
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这个实例并启动后续工作。如果我们运行的是npm run serve。则command = "serve"。
service.run(command, args, rawArgv).catch(err => {
 error(err)
 process.exit(1)
})

Service.js

上面实例化并调用了service的run方法,这里从构造函数到run一路浏览即可。

const fs = require('fs')
const path = require('path')
const debug = require('debug')
const chalk = require('chalk')
const readPkg = require('read-pkg')
const merge = require('webpack-merge')
const Config = require('webpack-chain')
const PluginAPI = require('./PluginAPI')
const loadEnv = require('./util/loadEnv')
const defaultsDeep = require('lodash.defaultsdeep')
const { warn, error, isPlugin, loadModule } = require('@vue/cli-shared-utils')

const { defaults, validate } = require('./options')

module.exports = class Service {
 constructor (context, { plugins, pkg, inlineOptions, useBuiltIn } = {}) {
  process.VUE_CLI_SERVICE = this
  this.initialized = false
  // 一般是项目根目录路径。
  this.context = context
  this.inlineOptions = inlineOptions
  // webpack相关收集。不是本文重点。所以未列出该方法实现
  this.webpackChainFns = []
  this.webpackRawConfigFns = []
  this.devServerConfigFns = []
  //存储的命令。
  this.commands = {}
  // Folder containing the target package.json for plugins
  this.pkgContext = context
  // 键值对存储的pakcage.json对象,不是本文重点。所以未列出该方法实现
  this.pkg = this.resolvePkg(pkg)
  // **这个方法下方需要重点阅读。**
  this.plugins = this.resolvePlugins(plugins, useBuiltIn)
  
  // 结果为{build: production, serve: development, ... }。大意是收集插件中的默认配置信息
  // 标注build命令主要用于生产环境。
  this.modes = this.plugins.reduce((modes, { apply: { defaultModes }}) => {
   return Object.assign(modes, defaultModes)
  }, {})
 }

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

  // 加载.env文件中的配置
  if (mode) {
   this.loadEnv(mode)
  }
  // load base .env
  this.loadEnv()

  // 读取用户的配置信息.一般为vue.config.js
  const userOptions = this.loadUserOptions()
  // 读取项目的配置信息并与用户的配置合并(用户的优先级高)
  this.projectOptions = defaultsDeep(userOptions, defaults())

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

  // 注册插件。
  this.plugins.forEach(({ id, apply }) => {
   apply(new PluginAPI(id, this), this.projectOptions)
  })

  // wepback相关配置收集
  if (this.projectOptions.chainWebpack) {
   this.webpackChainFns.push(this.projectOptions.chainWebpack)
  }
  if (this.projectOptions.configureWebpack) {
   this.webpackRawConfigFns.push(this.projectOptions.configureWebpack)
  }
 }


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

  let plugins
  
  
  // 主要是这里。map得到的每个插件都是一个{id, apply的形式}
  // 其中require(id)将直接import每个插件的默认导出。
  // 每个插件的导出api为
  // module.exports = (PluginAPIInstance,projectOptions) => {
  //  PluginAPIInstance.registerCommand('cmdName(例如npm run serve中的serve)', args => {
  //    // 根据命令行收到的参数,执行该插件的业务逻辑
  //  })
  //  // 业务逻辑需要的其他函数
  //}
  // 注意着里是先在构造函数中resolve了插件。然后再run->init->方法中将命令,通过这里的的apply方法,
  // 将插件对应的命令注册到了service实例。
  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)
  
  // inlinePlugins与非inline得处理。默认生成的项目直接运行时候,除了上述数组的插件['./commands/serve'...]外,还会有
  // ['@vue/cli-plugin-babel','@vue/cli-plugin-eslint','@vue/cli-service']。
  // 处理结果是两者的合并,细节省略。
  if (inlinePlugins) {
    //...
  } else {
    //...默认走这条路线
   plugins = builtInPlugins.concat(projectPlugins)
  }

  // Local plugins 处理package.json中引入插件的形式,具体代码省略。

  return plugins
 }

 async run (name, args = {}, rawArgv = []) {
  // mode是dev还是prod?
  const mode = args.mode || (name === 'build' && args.watch ? 'development' : this.modes[name])

  // 收集环境变量、插件、用户配置
  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()
  }
  // 执行命令。例如vue-cli-service serve 则,执行serve命令。
  const { fn } = command
  return fn(args, rawArgv)
 }

 // 收集vue.config.js中的用户配置。并以对象形式返回。
 loadUserOptions () {
  // 此处代码省略,可以简单理解为
  // require(vue.config.js)
  return resolved
 }
}

PluginAPI

这里主要是连接了plugin的注册和service实例。抽象过的代码如下

class PluginAPI {

 constructor (id, service) {
  this.id = id
  this.service = service
 }
 // 在service的init方法中
 // 该函数会被调用,调用处如下。
 // // apply plugins.
 // 这里的apply就是插件暴露出来的函数。该函数将PluginAPI实例和项目配置信息(例如vue.config.js)作为参数传入
 // 通过PluginAPIInstance.registerCommand方法,将命令注册到service实例。
 // this.plugins.forEach(({ id, apply }) => {
 //  apply(new PluginAPI(id, this), this.projectOptions)
 // })
 registerCommand (name, opts, fn) {
  if (typeof opts === 'function') {
   fn = opts
   opts = null
  }
  this.service.commands[name] = { fn, opts: opts || {}}
 }


}

module.exports = PluginAPI

总结

通过vue-cli-service中的new Service,加载插件信息,缓存到Service实例的plugins变量中。

当得到命令行参数后,在通过new Service的run方法,执行命令。

该run方法中调用了init方法获取到项目中的配置信息(默认&用户的合并),例如用户的配置在vue.config.js中。

init过程中通过pluginAPI这个类,将service和插件plugins建立关联。关系存放到service.commands中。
最后通过commands[cmdArgName]调用该方法,完成了插件方法的调用。

初次阅读,只是看到了命令模式的实际应用。能想到的好就是,新增加一个插件的时候,只需要增加一个插件的文件,并不需要更改其他文件的逻辑。其他的部分,再慢慢体会吧。。。以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
js动态调用css属性的小规律及实例说明
Dec 28 Javascript
js中定义一个变量并判断其是否为空的方法
May 13 Javascript
Javascript核心读书有感之语言核心
Feb 01 Javascript
使用OpenLayers3 添加地图鼠标右键菜单
Dec 29 Javascript
探讨:JavaScript ECAMScript5 新特性之get/set访问器
May 05 Javascript
JSON字符串转换JSONObject和JSONArray的方法
Jun 03 Javascript
jQuery页面弹出框实现文件上传
Feb 09 Javascript
easyui datagrid 表格中操作栏 按钮图标不显示的解决方法
Jul 27 Javascript
JS笛卡尔积算法与多重数组笛卡尔积实现方法示例
Dec 01 Javascript
详解ES6 Promise的生命周期和创建
Aug 18 Javascript
js实现炫酷光感效果
Sep 05 Javascript
vue项目中企业微信使用js-sdk时config和agentConfig配置方式详解
Dec 15 Vue.js
JS数组求和的常用方法总结【5种方法】
Jan 14 #Javascript
JS实现根据数组对象的某一属性排序操作示例
Jan 14 #Javascript
vue项目中使用vue-i18n报错的解决方法
Jan 13 #Javascript
vscode下vue项目中eslint的使用方法
Jan 13 #Javascript
jQuery实现的中英文切换功能示例
Jan 11 #jQuery
JavaScript寄生组合式继承原理与用法分析
Jan 11 #Javascript
JavaScript常见继承模式实例小结
Jan 11 #Javascript
You might like
dedecms中显示数字验证码的修改方法
2007/03/21 PHP
php+jQuery+Ajax实现点赞效果的方法(附源码下载)
2020/07/21 PHP
利用PHP实现开心消消乐的算法示例
2017/10/12 PHP
Ubuntu上安装yaf扩展的方法
2018/01/29 PHP
jquery 简单导航实现代码
2009/09/11 Javascript
JavaScript 开发规范要求(图文并茂)
2010/06/11 Javascript
JQuery表格拖动调整列宽效果(自己动手写的)
2014/09/01 Javascript
node.js中的console用法总结
2014/12/15 Javascript
JavaScript正则表达式匹配 div  style标签
2016/03/15 Javascript
js中setTimeout的妙用--防止循环超时
2017/03/06 Javascript
Vuex 进阶之模块化组织详解
2018/01/12 Javascript
vue toggle做一个点击切换class(实例讲解)
2018/03/13 Javascript
解决bootstrap模态框数据缓存的问题方法
2018/08/10 Javascript
Angular项目如何升级至Angular6步骤全纪录
2018/09/03 Javascript
详解element-ui日期时间选择器的日期格式化问题
2019/04/08 Javascript
通过实例解析JavaScript for in及for of区别
2020/06/15 Javascript
django中send_mail功能实现详解
2018/02/06 Python
python实现泊松图像融合
2018/07/26 Python
python科学计算之narray对象用法
2019/11/25 Python
利用4行Python代码监测每一行程序的运行时间和空间消耗
2020/04/22 Python
Python Scrapy多页数据爬取实现过程解析
2020/06/12 Python
意大利简约的休闲品牌:Aspesi
2018/02/08 全球购物
英国手机零售商:Metrofone
2019/03/18 全球购物
Delphi软件工程师试题
2013/01/29 面试题
关于感恩的演讲稿800字
2014/08/26 职场文书
关于感恩的演讲稿200字
2014/08/26 职场文书
3.15消费者权益日活动总结
2015/02/09 职场文书
文员岗位职责范本
2015/04/16 职场文书
惹女朋友生气检讨书
2015/05/06 职场文书
介绍信范文大全
2015/05/07 职场文书
退伍军人感言
2015/08/01 职场文书
感恩老师主题班会
2015/08/12 职场文书
人民调解协议书
2016/03/21 职场文书
情况说明书格式及范文
2019/06/24 职场文书
Python中的套接字编程是什么?
2021/06/21 Python
用Python可视化新冠疫情数据
2022/01/18 Python