场景介绍
最近做H5遇到了一个场景:每个页面需要展示一个带有标题的头部。一个实现思路是使用全局组件。假设我们创建一个名为TheHeader.vue的全局组件,伪代码如下:
<template> <h2>{{ title }}</h2> </template> <script> export default { props: { title: { type: String, default: '' } } } </script>
创建好全局组件后,在每个页面组件中引用该组件并传入props中即可。例如我们在页面A中引用该组件,页面A对应的组件是A.vue
<template> <div> <TheHeader :title="title" /> </div> </template> <script> export default { data() { title: '' }, created(){ this.title = "我的主页" } } </script>
使用起来非常简单,不过有一点美中不足:如果头部组件需要传入的props很多,那么在页面组件中维护对应的props就会比较繁琐。针对这种情况,有一个更好的思路来实现这个场景,就是使用Vue插件。
同样是在A.vue组件调用头部组件,使用Vue插件的调用方式会更加简洁:
<template> <div /> </template> <script> export default { created(){ this.$setHeader('我的主页') } } </script>
我们看到,使用Vue插件来实现,不需要在A.vue中显式地放入TheHeader组件,也不需要在A.vue的data函数中放入对应的props,只需要调用一个函数即可。那么,这个插件是怎么实现的呢?
插件实现
它的实现具体实现步骤如下:
- 创建一个SFC(single file component),这里就是TheHeader组件
- 创建一个plugin.js文件,引入SFC,通过Vue.extend方法扩展获取一个新的Vue构造函数并实例化。
- 实例化并通过函数调用更新Vue组件实例。
按照上面的步骤,我们来创建一个plugin.js文件:
import TheHeader from './TheHeader.vue' import Vue from 'vue' const headerPlugin = { install(Vue) { const vueInstance = new (Vue.extend(TheHeader))().$mount() Vue.prototype.$setHeader = function(title) { vueInstance.title = title document.body.prepend(vueInstance.$el) } } } Vue.use(headerPlugin)
我们随后在main.js中引入plugin.js,就完成了插件实现的全部逻辑过程。不过,尽管这个插件已经实现了,但是有不少问题。
问题一、重复的头部组件
如果我们在单页面组件中使用,只要使用router.push方法之后,我们就会发现一个神奇的问题:在新的页面出现了两个头部组件。如果我们再跳几次,头部组件的数量也会随之增加。这是因为,我们在每个页面都调用了这个方法,因此每个页面都在文档中放入了对应DOM。
考虑到这点,我们需要对上面的组件进行优化,我们把实例化的过程放到插件外面:
import TheHeader from './TheHeader.vue' import Vue from 'vue' const vueInstance = new (Vue.extend(TheHeader))().$mount() const headerPlugin = { install(Vue) { Vue.prototype.$setHeader = function(title) { vueInstance.title = title document.body.prepend(vueInstance.$el) } } } Vue.use(headerPlugin)
这样处理,虽然还是会重复在文档中插入DOM。不过,由于是同一个vue实例,对应的DOM没有发生改变,所以插入的DOM始终只有一个。这样,我们就解决了展示多个头部组件的问题。为了不重复执行插入DOM的操作,我们还可以做一个优化:
import TheHeader from './TheHeader.vue' import Vue from 'vue' const vueInstance = new (Vue.extend(TheHeader))().$mount() const hasPrepend = false const headerPlugin = { install(Vue) { Vue.prototype.$setHeader = function(title) { vueInstance.title = title if (!hasPrepend) { document.body.prepend(vueInstance.$el) hasPrepend = true } } } } Vue.use(headerPlugin)
增加一个变量来控制是否已经插入了DOM,如果已经插入了,就不再执行插入的操作。优化以后,这个插件的实现就差不多了。不过,个人在实现过程中有几个问题,这里也一并记录一下。
问题二、另一种实现思路
在实现过程中突发奇想,是不是可以直接修改TheHeader组件的data函数来实现这个组件呢?看下面的代码:
import TheHeader from './TheHeader.vue' import Vue from 'vue' let el = null const headerPlugin = { install(Vue) { Vue.prototype.$setHeader = function(title) { TheHeader.data = function() { title } const vueInstance = new (Vue.extend(TheHeader))().$mount() el = vueInstance.$el if (el) { document.body.removeChild(el) document.body.prepend(el) } } } } Vue.use(headerPlugin)
看上去也没什么问题。不过实践后发现,调用$setHeader方法,只有第一次传入的值会生效。例如第一次传入的是'我的主页',第二次传入的是'个人信息',那么头部组件将始终展示我的主页,而不会展示个人信息。原因是什么呢?
深入Vue源码后发现,在第一次调用new Vue以后,Header多了一个Ctor属性,这个属性缓存了Header组件对应的构造函数。后续调用new Vue(TheHeader)时,使用的构造函数始终都是第一次缓存的,因此title的值也不会发生变化。Vue源码对应的代码如下:
Vue.extend = function (extendOptions) { extendOptions = extendOptions || {}; var Super = this; var SuperId = Super.cid; var cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {}); if (cachedCtors[SuperId]) { // 如果有缓存,直接返回缓存的构造函数 return cachedCtors[SuperId] } var name = extendOptions.name || Super.options.name; if (process.env.NODE_ENV !== 'production' && name) { validateComponentName(name); } var Sub = function VueComponent (options) { this._init(options); }; Sub.prototype = Object.create(Super.prototype); Sub.prototype.constructor = Sub; Sub.cid = cid++; Sub.options = mergeOptions( Super.options, extendOptions ); Sub['super'] = Super; // For props and computed properties, we define the proxy getters on // the Vue instances at extension time, on the extended prototype. This // avoids Object.defineProperty calls for each instance created. if (Sub.options.props) { initProps$1(Sub); } if (Sub.options.computed) { initComputed$1(Sub); } // allow further extension/mixin/plugin usage Sub.extend = Super.extend; Sub.mixin = Super.mixin; Sub.use = Super.use; // create asset registers, so extended classes // can have their private assets too. ASSET_TYPES.forEach(function (type) { Sub[type] = Super[type]; }); // enable recursive self-lookup if (name) { Sub.options.components[name] = Sub; } // keep a reference to the super options at extension time. // later at instantiation we can check if Super's options have // been updated. Sub.superOptions = Super.options; Sub.extendOptions = extendOptions; Sub.sealedOptions = extend({}, Sub.options); // cache constructor cachedCtors[SuperId] = Sub; // 这里就是缓存Ctor构造函数的地方 return Sub }
找到了原因,我们会发现这种方式也是可以的,我们只需要在plugin.js中加一行代码
import TheHeader from './TheHeader.vue' import Vue from 'vue' let el = null const headerPlugin = { install(Vue) { Vue.prototype.$setHeader = function(title) { TheHeader.data = function() { title } TheHeader.Ctor = {} const vueInstance = new Vue(TheHeader).$mount() el = vueInstance.$el if (el) { document.body.removeChild(el) document.body.prepend(el) } } } } Vue.use(headerPlugin)
每次执行$setHeader方法时,我们都将缓存的构造函数去掉即可。
问题三、是否可以不使用Vue.extend
实测其实不使用Vue.extend,直接使用Vue也是可行的,相关代码如下:
import TheHeader from './TheHeader.vue' import Vue from 'vue' const vueInstance = new Vue(TheHeader).$mount() const hasPrepend = false const headerPlugin = { install(Vue) { Vue.prototype.$setHeader = function(title) { vueInstance.title = title if (!hasPrepend) { document.body.prepend(vueInstance.$el) hasPrepend = true } } } } Vue.use(headerPlugin)
直接使用Vue来创建实例相较extend创建实例来说,不会在Header.vue中缓存Ctor属性,相较来说是一个更好的办法。但是之前有看过Vant实现Toast组件,基本上是使用Vue.extend方法而没有直接使用Vue,这是为什么呢?
总结
到此这篇关于Vue插件实现过程中遇到问题的文章就介绍到这了,更多相关Vue插件实现问题内容请搜索以前的文章或继续浏览下面的相关文章希望大家以后多多支持!