Vue外掛實現過程中遇到的問題總結
目錄
- 場景介紹
- 外掛實現
- 問題一、重複的頭部元件
- 問題二、另一種實現思路
- 問題三、是否可以不使用.extend
- 總結
場景介紹
最近做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.檔案,引入SFC,通過Vue.extend方法擴充套件獲取一個新的Vue建構函式並例項化。
- 例項化並通過函式呼叫更新Vue元件例項。
按http://www.cppcns.com照上面的步驟,我們來建立一個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外掛實現問題內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!