Vue-cli@3.0 插件系统简析
vue-cli@3.0 是一个全新的 vue 项目脚手架。不同于 1.x/2.x 基于模板的脚手架,vue-cli@3.0 采用了一套基于插件的架构,它将部分核心功能收敛至 cli 内部,同时对开发者暴露可拓展的 api 以供开发者对 cli 的功能进行灵活的拓展和配置。接下来我们就通过 vue-cli@3.0 的源码来看下这套插件架构是如何设计的。
整个插件系统当中包含2个重要的组成部分:
- @vue/cli,提供 cli 命令服务,例如vue create创建一个新的项目;
- @vue/cli-service,提供了本地开发构建服务。
@vue/cli-service
当你使用 vue create <project-name>
创建一个新的 vue 项目,你会发现生成的项目相较于 1.x/2.x 初始化一个项目时从远程拉取的模板发生了很大的变化,其中关于 webpack 相关的配置以及 npm script 都没有在模板里面直接暴露出来,而是提供了新的 npm script:
// package.json "scripts": { "serve": "vue-cli-service serve", "build": "vue-cli-service build", "lint": "vue-cli-service lint" }
前 2 个脚本命令是项目本地安装的 @vue/cli-service 所提供的基于 webpack 及相关的插件进行封装的本地开发/构建的服务。@vue/cli-service 将 webpack 及相关插件提供的功能都收敛到 @vue/cli-service 内部来实现。
这 2 个命令对应于 下的 serve.js 和 build/index.js。
在 serve.js 和 build/index.js 的内部分别暴露了一个函数及一个 defaultmodes 属性供外部来使用。 事实上这两者都是作为 built-in(内置)插件来供 vue-cli-service 来使用的 。
说到这里那么就来看看 @vue/cli-service 内部是如何搭建整个插件系统的。就拿执行 npm run serve 启动本地开发服务来说,大概流程是这样的:
首先来看下 @vue/cli-service 提供的 cli 启动入口服务(@vue/cli-service/bin/vue-cli-service.js):
#!/usr/bin/env node const semver = require('semver') const { error } = require('@vue/cli-shared-utils') const service = require('../lib/service') // 引入 service 基类 const service = new service(process.env.vue_cli_context || process.cwd()) // 实例化 service const rawargv = process.argv.slice(2) const args = require('minimist')(rawargv) const command = args._[0] service.run(command, args, rawargv).catch(err => { // 开始执行对应的 service 服务 error(err) process.exit(1) })
看到这里你会发现在 bin 里面并未提供和本地开发 serve 相关的服务,事实上在项目当中本地安装的 @vue/cli-service 提供的不管是内置的还是插件提供的服务都是动态的去完成相关 cli 服务的注册。
在 lib/service.js 内部定义了一个核心的类 service,它作为 @vue/cli 的运行时的服务而存在。在执行 npm run serve 后,首先完成 service 的实例化工作:
class service { constructor(context) { ... this.webpackchainfns = [] // 数组内部每项为一个fn this.webpackrawconfigfns = [] // 数组内部每项为一个 fn 或 webpack 对象字面量配置项 this.devserverconfigfns = [] this.commands = {} // 缓存动态注册 cli 命令 ... this.plugins = this.resolveplugins(plugins, usebuiltin) // 完成插件的加载 this.modes = this.plugins.reduce((modes, { apply: { defaultmodes }}) => { // 缓存不同 cli 命令执行时所对应的mode值 return object.assign(modes, defaultmodes) }, {}) } }
在实例化 service 的过程当中完成了两个比较重要的工作:
加载插件
将插件提供的不同命令服务所使用的 mode 进行缓存
当 service 实例化完成后,调用实例上的 run 方法来启动对应的 cli 命令所提供的服务。
async run (name, args = {}, rawargv = []) { const mode = args.mode || (name === 'build' && args.watch ? 'development' : this.modes[name]) // load env variables, load user config, apply plugins // 执行所有被加载进来的插件 this.init(mode) ... const { fn } = command return fn(args, rawargv) // 开始执行对应的 cli 命令服务 } init (mode = process.env.vue_cli_mode) { ... // 执行plugins // apply plugins. this.plugins.foreach(({ id, apply }) => { // 传入一个实例化的pluginapi实例,插件名作为插件的id标识,在插件内部完成注册 cli 命令服务和 webpack 配置的更新的工作 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) } }
接下来我们先看下 @vue/cli-service
当中的 service 实例化的过程:通过 resolveplugins 方法去完成插件的加载工作:
resolveplugins(inlineplugins, usebuiltin) { const idtoplugin = id => ({ id: id.replace(/^.\//, 'built-in:'), apply: require(id) // 加载对应的插件 }) let plugins // @vue/cli-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) if (inlineplugins) { plugins = usebuiltin !== false ? builtinplugins.concat(inlineplugins) : inlineplugins } else { // 加载项目当中使用的插件 const projectplugins = object.keys(this.pkg.devdependencies || {}) .concat(object.keys(this.pkg.dependencies || {})) .filter(isplugin) .map(idtoplugin) plugins = builtinplugins.concat(projectplugins) } // local plugins if (this.pkg.vueplugins && this.pkg.vueplugins.service) { const files = this.pkg.vueplugins.service if (!array.isarray(files)) { throw new error(`invalid type for option 'vueplugins.service', expected 'array' but got ${typeof files}.`) } plugins = plugins.concat(files.map(file => ({ id: `local:${file}`, apply: loadmodule(file, this.pkgcontext) }))) } return plugins }
在这个 resolveplugins 方法当中,主要完成了对于 @vue/cli-service 内部提供的插件以及项目应用(package.json)当中需要使用的插件的加载,并将对应的插件进行缓存。在其提供的内部插件当中又分为两类:
'./commands/serve'
'./commands/build'
'./commands/inspect'
'./commands/help'
这一类插件在内部动态注册新的 cli 命令,开发者即可通过 npm script 的形式去启动对应的 cli 命令服务。
'./config/base'
'./config/css'
'./config/dev'
'./config/prod'
'./config/app'
这一类插件主要是完成 webpack 本地编译构建时的各种相关的配置。@vue/cli-service 将 webpack 的开发构建功能收敛到内部来完成。
插件加载完成,开始调用 service.run 方法,在这个方法内部开始执行所有被加载的插件:
this.plugins.foreach(({ id, apply }) => { apply(new pluginapi(id, this), this.projectoptions) })
在每个插件执行的过程中,接收到的第一个参数都是 pluginapi 的实例,pluginapi 也是整个 @vue/cli-service 服务当中一个核心的基类:
class pluginapi { constructor (id, service) { this.id = id // 对应这个插件名 this.service = service // 对应 service 类的实例(单例) } ... registercommand (name, opts, fn) { // 注册自定义 cli 命令 if (typeof opts === 'function') { fn = opts opts = null } this.service.commands[name] = { fn, opts: opts || {}} } chainwebpack (fn) { // 缓存变更的 webpack 配置 this.service.webpackchainfns.push(fn) } configurewebpack (fn) { // 缓存变更的 webpack 配置 this.service.webpackrawconfigfns.push(fn) } ... }
每个由 pluginapi 实例化的 api 实例都提供了:
- 注册 cli 命令服务( api.registercommand )
- 通过 api 形式去更新的 webpack 配置( api.chainwebpack )
- 通过 raw 配置形式去更新的 webpack 配置( api.configurewebpack ),与 api.chainwebpack 提供的链式 api 操作 webpack 配置的方式不同, api.configurewebpack 可接受raw式的配置形式,并通过 webpack-merge 对 webpack 配置进行合并。
- resolve wepack 配置( api.resolvewebpackconfig ),调用之前通过 chainwebpack 和 configurewebpack 上完成的对于 webpack 配置的改造,并生成最终的 webpack 配置
- ...
首先我们来看下 @vue/cli-service 提供的关于动态注册 cli 服务的插件,拿 serve 服务( ./commands/serve )来说:
// commands/serve 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) { // do something } ) }
./commands/serve 对外暴露一个函数,接收到的第一个参数 pluginapi 的实例 api,并通过 api 提供的 registercommand 方法来完成 cli 命令(即 serve 服务)的注册。
再来看下 @vue/cli-service 内部提供的关于 webpack 配置的插件( ./config/base ):
module.exports = (api, options) => { api.chainwebpack(webpackconfig => { webpackconfig.module .rule('vue') .test(/\.vue$/) .use('cache-loader') .loader('cache-loader') .options(vueloadercacheconfig) .end() .use('vue-loader') .loader('vue-loader') .options( object.assign( { compileroptions: { preservewhitespace: false } }, vueloadercacheconfig ) ) }) }
这个插件完成了 webpack 的基本配置内容,例如 entry、output、加载不同文件类型的 loader 的配置。 不同于之前使用的配置式的 webpack 使用方式,@vue/cli-service 默认使用 webpack-chain( ) 来完成 webpack 配置的修改 。这种方式也使得 webpack 的配置更加灵活,当你的项目迁移至 @vue/cli@3.0,使用的 webpack 插件也必须要使用 api 式的配置,同时插件不仅仅要提供插件自身的功能,同时也需要帮助调用方完成插件的注册等工作。
@vue/cli-service 将基于 webpack 的本地开发构建配置收敛至内部来实现,当你没有特殊的开发构建需求的时候,内部配置可以开箱即用,不用开发者去关心一些细节。当然在实际团队开发当中,内部配置肯定是无法满足的,得益于 @vue-cli@3.0 的插件构建设计,开发者不需要将内部的配置进行 eject,而是直接使用 @vue/cli-service 暴露出来的 api 去完成对于特殊的开发构建需求。
以上介绍了 @vue/cli-service 插件系统当中几个核心的模块,即:
service.js 提供服务的基类,它提供了 @vue/cli 生态当中本地开发构建时:插件加载(包括内部插件和项目应用插件)、插件的初始化,它的单例被所有的插件所共享,插件使用它的单例去完成 webpack 的更新。
pluginapi.js 提供供插件使用的对象接口,它和插件是一一对应的关系。所有供 @vue/cli-service 使用的本地开发构建的插件接收的第一个参数都是 pluginapi 的实例( api ),插件使用这个实例去完成 cli 命令的注册及对应服务的执行、webpack 配置的更新等。
以上就是 @vue/cli-service 插件系统简单的分析,感兴趣的同学可以深入阅读相关源码( )进行学习。
@vue/cli
不同于之前 1.x/2.x 的 vue-cli 工具都是基于远程模板去完成项目的初始化的工作,它属于那种大而全的方式,当你需要完成自定义的脚手架工具时,你可能要对 vue-cli 进行源码级别的改造,或者是在远程模板里面帮开发者将所有的配置文件初始化完成好。而 @vue/cli@3.0 主要是基于插件的 generator 去完成项目的初始化的工作,它将原来的大而全的模板拆解为现在基于插件系统的工作方式,每个插件完成自己所要对于项目应用的模板拓展工作。
@vue/cli 提供了终端里面的 vue 命令,例如:
vue create <project> vue ui
当你需要对 vue-cli 进行改造,自定义符合自己开发要求的脚手架的时候,那么你需要通过 开发 vue-cli 插件来对 vue-cli 提供的服务进行拓展来满足相关的要求 。vue-cli 插件始终包含一个 service 插件作为其主要导出,且可选的包含一个 generator 和一个 prompt 文件。这里不细讲如何去开发一个 vue-cli 插件了,大家感兴趣的可以阅读
这里主要是来看下 vue-cli 是如何设计整个插件系统以及整个插件系统是如何工作的。
@vue/cli@3.0 提供的插件安装方式为一个 cli 服务: vue add <plugin> :
install a plugin and invoke its generator in an already created project
执行这条命令后,@vue/cli 会帮你完成插件的下载,安装以及执行插件所提供的 generator。整个流程的执行顺序可通过如下的流程图去概括:
我们来看下具体的代码逻辑:
// @vue/cli/lib/add.js async function add (pluginname, options = {}, context = process.cwd()) { ... const packagemanager = loadoptions().packagemanager || (hasprojectyarn(context) ? 'yarn' : 'npm') // 开始安装这个插件 await installpackage(context, packagemanager, null, packagename) log(`${chalk.green('✔')} successfully installed plugin: ${chalk.cyan(packagename)}`) log() // 判断插件是否提供了 generator const generatorpath = resolvemodule(`${packagename}/generator`, context) if (generatorpath) { invoke(pluginname, options, context) } else { log(`plugin ${packagename} does not have a generator to invoke`) } }
首先 cli 内部会安装这个插件,并判断这个插件是否提供了 generator,若提供了那么去执行对应的 generator。
// @vue/cli/lib/invoke.js async function invoke (pluginname, options = {}, context = process.cwd()) { const pkg = getpkg(context) ... // 从项目应用package.json中获取插件名 const id = findplugin(pkg.devdependencies) || findplugin(pkg.dependencies) ... // 加载对应插件提供的generator方法 const plugingenerator = loadmodule(`${id}/generator`, context) ... const plugin = { id, apply: plugingenerator, options } // 开始执行generator方法 await rungenerator(context, plugin, pkg) } async function rungenerator (context, plugin, pkg = getpkg(context)) { ... // 实例化一个generator实例 const generator = new generator(context, { pkg plugins: [plugin], // 插件提供的generator方法 files: await readfiles(context), // 将项目当中的文件读取为字符串的形式保存到内存当中,被读取的文件规则具体见readfiles方法 completecbs: createcompletecbs, invoking: true }) ... // resolvefiles 将内存当中的所有缓存的 files 输出到文件当中 await generator.generate({ extractconfigfiles: true, checkexisting: true }) }
和 @vue/cli-service 类似,在 @vue/cli 内部也有一个核心的类 generator ,每个 @vue/cli 的插件对应一个 generator 的实例。在实例化 generator 方法的过程当中,完成插件提供的 generator 的执行。
// @vue/cli/lib/generator.js module.exports = class generator { constructor (context, { pkg = {}, plugins = [], completecbs = [], files = {}, invoking = false } = {}) { this.context = context this.plugins = plugins this.originalpkg = pkg this.pkg = object.assign({}, pkg) this.imports = {} this.rootoptions = {} ... this.invoking = invoking // for conflict resolution this.depsources = {} // virtual file tree this.files = files this.filemiddlewares = [] this.postprocessfilescbs = [] ... const cliservice = plugins.find(p => p.id === '@vue/cli-service') const rootoptions = cliservice ? cliservice.options : inferrootoptions(pkg) // apply generators from plugins // 每个插件对应生成一个 generatorapi 实例,并将实例 api 传入插件暴露出来的 generator 函数 plugins.foreach(({ id, apply, options }) => { const api = new generatorapi(id, this, options, rootoptions) apply(api, options, rootoptions, invoking) }) } }
和 @vue/cli-service 所使用的插件类似,@vue/cli 插件所提供的 generator 也是向外暴露一个函数,接收的第一个参数 api,然后通过该 api 提供的方法去完成应用的拓展工作。
开发者利用这个 api 实例去完成项目应用的拓展工作,这个 api 实例提供了:
拓展 package.json 配置方法( api.extendpackage )
利用 ejs 渲染模板文件的方法( api.render )
内存中保存的文件字符串全部被写入文件后的回调函数( api.oncreatecomplete )
向文件当中注入 import 语法的方法( api.injectimports )
...
例如 @vue/cli-plugin-eslint 插件的 generator 方法主要是完成了:vue-cli-service cli lint 服务命令的添加、相关 lint 标准库的依赖添加等工作:
module.exports = (api, { config, linton = [] }, _, invoking) => { if (typeof linton === 'string') { linton = linton.split(',') } const eslintconfig = require('./eslintoptions').config(api) const pkg = { scripts: { lint: 'vue-cli-service lint' }, eslintconfig, devdependencies: {} } if (config === 'airbnb') { eslintconfig.extends.push('@vue/airbnb') object.assign(pkg.devdependencies, { '@vue/eslint-config-airbnb': '^3.0.0-rc.10' }) } else if (config === 'standard') { eslintconfig.extends.push('@vue/standard') object.assign(pkg.devdependencies, { '@vue/eslint-config-standard': '^3.0.0-rc.10' }) } else if (config === 'prettier') { eslintconfig.extends.push('@vue/prettier') object.assign(pkg.devdependencies, { '@vue/eslint-config-prettier': '^3.0.0-rc.10' }) } else { // default eslintconfig.extends.push('eslint:recommended') } ... api.extendpackage(pkg) ... // lint & fix after create to ensure files adhere to chosen config if (config && config !== 'base') { api.oncreatecomplete(() => { require('./lint')({ silent: true }, api) }) } }
以上介绍了 @vue/cli 和插件系统相关的几个核心的模块,即:
add.js 提供了插件下载的 cli 命令服务和安装的功能;
invoke.js 完成插件所提供的 generator 方法的加载和执行,同时将项目当中的文件转化为字符串缓存到内存当中;
generator.js 和插件进行桥接,@vue/cli 每次 add 一个插件时,都会实例化一个 generator 实例与之对应;
generatorapi.js 和插件一一对应,是 @vue/cli 暴露给插件的 api 对象,提供了很多项目应用的拓展工作。
总结
以上是对 vue-cli@3.0 的插件系统当中两个主要部分:@vue/cli 和 @vue/cli-service 简析。
- @vue/cli 提供 vue cli 命令,负责偏好设置,生成模板、安装插件依赖的工作,例如 vue create <projectname> 、 vue add <pluginname>
- @vue/cli-service 作为 @vue/cli 整个插件系统当中的内部核心插件,提供了 webpack 配置更新,本地开发构建服务
前者主要完成了对于插件的依赖管理,项目模板的拓展等,后者主要是提供了在运行时本地开发构建的服务,同时后者也作为 @vue/cli 整个插件系统当中的内部核心插件而存在。在插件系统内部也对核心功能进行了插件化的拆解,例如 @vue/cli-service 内置的基础 webpack 配置,npm script 命令等。二者使用约定式的方式向开发者提供插件的拓展能力,具体到如何开发 @vue/cli 的插件,请参考官方文档。
以上所述是小编给大家介绍的vue-cli@3.0 插件系统简析,希望对大家有所帮助
上一篇: PHP常见漏洞攻击分析