在使用vue的過程中结啼,經(jīng)常會(huì)用到Vue.use
,但是大部分對(duì)它一知半解屈芜,不了解在調(diào)用的時(shí)候具體做了什么郊愧,因此,本文簡要概述下在vue中井佑,如何封裝自定義插件属铁。
在開始之前,先補(bǔ)充一句躬翁,其實(shí)利用Vue
封裝自定義插件的本質(zhì)就是組件實(shí)例化的過程或者指令等公共屬性方法的定義過程焦蘑,比較大的區(qū)別在于封裝插件需要手動(dòng)干預(yù),就是一些實(shí)例化方法需要手動(dòng)調(diào)用盒发,而Vue
的實(shí)例化例嘱,很多邏輯內(nèi)部已經(jīng)幫忙處理掉了。插件相對(duì)于組件的優(yōu)勢(shì)就是插件封裝好了之后宁舰,可以開箱即用拼卵,而組件是依賴于項(xiàng)目的。對(duì)組件初始化過程不是很熟悉的可以參考這篇博文蛮艰。
我們從vue源碼中腋腮,可以看到Vue.use
的方法定義如下:
Vue.use = function (plugin: Function | Object) {
const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
// 已經(jīng)存在插件,則直接返回插件對(duì)象
if (installedPlugins.indexOf(plugin) > -1) {
return this
}
// additional parameters
const args = toArray(arguments, 1)
args.unshift(this)
// vue插件形式可以是對(duì)象印荔,也可以是方法,默認(rèn)會(huì)傳遞一個(gè)Vue的構(gòu)造方法作為參數(shù)
if (typeof plugin.install === 'function') {
plugin.install.apply(plugin, args)
} else if (typeof plugin === 'function') {
plugin.apply(null, args)
}
installedPlugins.push(plugin)
return this
}
從上述代碼中详羡,我們可以看出仍律,Vue.use
代碼比較簡潔,處理邏輯不是很多实柠。我們常用的Vue.use(xxx)
水泉,xxx可以是方法,也可以是對(duì)象。在Vue.use
中草则,通過apply
調(diào)用插件方法钢拧,傳入一個(gè)參數(shù),Vue的構(gòu)造方法炕横。舉個(gè)栗子源内,最簡單的Vue插件封裝如下:
// 方法
function vuePlugins (Vue) {
Vue.directive('test', {
bind (el) {
el.addEventListener('click', function (e) {
alert('hello world')
})
}
})
}
// 對(duì)象
const vuePlugins = {
install (Vue) {
Vue.directive('test', {
bind (el) {
el.addEventListener('click', function (e) {
alert('hello world')
})
}
})
}
}
以上兩種封裝方法都可以,說白了份殿,就是將全局注冊(cè)的指令封裝到一個(gè)方法中膜钓,在Vue.use
時(shí)調(diào)用。這個(gè)比較顯然易懂∏涑埃現(xiàn)在舉一個(gè)稍微復(fù)雜點(diǎn)的例子颂斜,tooltip
在前端開發(fā)中經(jīng)常會(huì)用到,直接通過方法能夠調(diào)用顯示拾枣,防止不必要的組件注冊(cè)引入沃疮,如果我們單獨(dú)封裝一個(gè)tooltip
組件,應(yīng)該如何封裝呢梅肤?這種封裝方式需要了解組件的初始化過程司蔬。區(qū)別在于將組件封裝成插件時(shí),不能通過template
將組件實(shí)例化掛載到真實(shí)DOM
中凭语,這一步需要手動(dòng)去調(diào)用對(duì)應(yīng)組件實(shí)例化生命周期中的方法葱她。具體實(shí)現(xiàn)代碼如下:
// component
let toast = {
props: {
show: {
type: Boolean,
default: false
},
msg: {
type: String
}
},
template: '<div v-show="show" class="toast">{{msg}}</div>'
}
組件初始化過程:
// JavaScript初始化邏輯
// 獲取toast構(gòu)造實(shí)例
const TempConstructor = Vue.extend(toast)
// 實(shí)例化toast
let instance = new TempConstructor()
// 手動(dòng)創(chuàng)建toast的掛載容器
let div = document.createElement('div')
// 解析掛載toast
instance.$mount(div)
// 將toast掛載到body中
document.body.append(instance.$el)
// 將toast的調(diào)用封裝成一個(gè)方法,掛載到Vue的原型上
Vue.prototype.$toast = function (msg) {
instance.show = true
instance.msg = msg
setTimeout(() => {
instance.show = false
}, 5000)
}
組件的定義似扔,和普通的組件聲明一致吨些。組件的插件化過程,和普通的組件實(shí)例化一致炒辉,區(qū)別在于插件化時(shí)組件部分初始化方法需要手動(dòng)調(diào)用豪墅。比如:
Vue.extend
作用是組裝組件的構(gòu)造方法VueComponent
new TempConstructor()
是實(shí)例化組件實(shí)例。實(shí)例化構(gòu)造方法黔寇,只是對(duì)組件的狀態(tài)數(shù)據(jù)進(jìn)行了初始化偶器,并沒有解析組件的template
,也沒有后續(xù)的生成vnode
和解析vnode
instance.$mount(div)
的作用是解析模板文件缝裤,生成render
函數(shù)屏轰,進(jìn)而調(diào)用createElement
生成vnode
,最后生成真實(shí)DOM,將生成的真實(shí)DOM掛載到實(shí)例instance
的$el
屬性上憋飞,也就是說霎苗,實(shí)例instance.$el
即為組件實(shí)例化最終的結(jié)果。組件中榛做,
props
屬性最終會(huì)聲明在組件實(shí)例上唁盏,所以直接通過實(shí)例的屬性内狸,也可以響應(yīng)式的更改屬性的傳參。組件的屬性初始化方法如下:
function initProps (Comp) {
const props = Comp.options.props
for (const key in props) {
proxy(Comp.prototype, `_props`, key)
}
}
// 屬性代理厘擂,從一個(gè)原對(duì)象中拿數(shù)據(jù)
export function proxy (target: Object, sourceKey: string, key: string) {
// 設(shè)置對(duì)象屬性的get/set,將data中的數(shù)據(jù)代理到組件對(duì)象vm上
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key]
}
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
從上述可以看出昆淡,最終會(huì)在構(gòu)造方法中,給所有的屬性聲明一個(gè)變量刽严,本質(zhì)上是讀取_props
中的內(nèi)容昂灵,_props
中的屬性,會(huì)在實(shí)例化組件,initState
中的InitProps
中進(jìn)行響應(yīng)式的聲明港庄,具體代碼如下:
function initProps (vm: Component, propsOptions: Object) {
const propsData = vm.$options.propsData || {}
const props = vm._props = {}
// cache prop keys so that future props updates can iterate using Array
// instead of dynamic object key enumeration.
const keys = vm.$options._propKeys = []
const isRoot = !vm.$parent
// root instance props should be converted
if (!isRoot) {
toggleObserving(false)
}
for (const key in propsOptions) {
keys.push(key)
const value = validateProp(key, propsOptions, propsData, vm)
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
const hyphenatedKey = hyphenate(key)
if (isReservedAttribute(hyphenatedKey) ||
config.isReservedAttr(hyphenatedKey)) {
warn(
`"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`,
vm
)
}
defineReactive(props, key, value, () => {
if (!isRoot && !isUpdatingChildComponent) {
warn(
`Avoid mutating a prop directly since the value will be ` +
`overwritten whenever the parent component re-renders. ` +
`Instead, use a data or computed property based on the prop's ` +
`value. Prop being mutated: "${key}"`,
vm
)
}
})
} else {
defineReactive(props, key, value)
}
// static props are already proxied on the component's prototype
// during Vue.extend(). We only need to proxy props defined at
// instantiation here.
if (!(key in vm)) {
proxy(vm, `_props`, key)
}
}
toggleObserving(true)
}
這里會(huì)遍歷所有訂單props
倔既,響應(yīng)式的聲明屬性的get
和set
。當(dāng)對(duì)屬性進(jìn)行讀寫時(shí)鹏氧,會(huì)調(diào)用對(duì)應(yīng)的get/set
渤涌,進(jìn)而會(huì)觸發(fā)視圖的更新,vue
的響應(yīng)式原理在后面的篇章中會(huì)進(jìn)行介紹把还。這樣实蓬,我們可以通過方法參數(shù)的傳遞,來動(dòng)態(tài)的去修改組件的props
吊履,進(jìn)而能夠?qū)⒔M件插件化安皱。
有些人可能會(huì)有疑問,到最后掛載到body
上的元素是通過document.createElement('div')
創(chuàng)建的div
艇炎,還是模板的template
解析后的結(jié)果酌伊。其實(shí),最終掛載只是組件解析后的結(jié)果缀踪。在調(diào)用__patch__
的過程中居砖,執(zhí)行流程是,首先驴娃,記錄老舊的節(jié)點(diǎn)奏候,也就是$mount(div)
中的div
;然后唇敞,根據(jù)模板解析后的render
生成的vnode
的節(jié)點(diǎn)蔗草,去創(chuàng)建DOM
節(jié)點(diǎn),創(chuàng)建后的DOM
節(jié)點(diǎn)會(huì)放到instance.$el
中疆柔;最后一步咒精,會(huì)將老舊節(jié)點(diǎn)給移除掉。所以旷档,在我們封裝一個(gè)插件的過程中模叙,實(shí)際上手動(dòng)創(chuàng)建的元素只是一個(gè)中間變量,并不會(huì)保留在最后彬犯∠蚵ィ可能大家還會(huì)注意到,插件實(shí)例化完成后的DOM
掛載也是我們手動(dòng)掛載的谐区,執(zhí)行的代碼是document.body.append(instance.$el)
湖蜕。
附:test.html 測(cè)試代碼
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<style>
.toast{
position: absolute;
left: 45%;
top: 10%;
width: 10%;
height: 5%;
background: #ccc;
border-radius: 5px;
}
</style>
<title>Hello World</title>
<script src="https://cdn.bootcss.com/vue/2.6.10/vue.js"></script>
</head>
<body>
<div id='app' v-test>
<button @click="handleClick">我是按鈕</button>
</div>
<script>
function vuePlugins (Vue) {
Vue.directive('test', {
bind (el) {
el.addEventListener('click', function (e) {
alert('hello world')
})
}
})
}
// const vuePlugins = {
// install (Vue) {
// Vue.directive('test', {
// bind (el) {
// el.addEventListener('click', function (e) {
// alert('hello world')
// })
// }
// })
// }
// }
Vue.use(vuePlugins)
let toast = {
props: {
show: {
type: Boolean,
default: false
},
msg: {
type: String
}
},
template: '<div v-show="show" class="toast">{{msg}}</div>'
}
// 獲取toast構(gòu)造實(shí)例
const TempConstructor = Vue.extend(toast)
// 實(shí)例化toast
let instance = new TempConstructor()
// 手動(dòng)創(chuàng)建toast的掛載容器
let div = document.createElement('div')
// 解析掛載toast
instance.$mount(div)
// 將toast掛載到body中
document.body.append(instance.$el)
// 將toast的調(diào)用封裝成一個(gè)方法,掛載到Vue的原型上
Vue.prototype.$toast = function (msg) {
instance.show = true
instance.msg = msg
setTimeout(() => {
instance.show = false
}, 5000)
}
var vm = new Vue({
el: '#app',
data: {
msg: 'Hello World',
a: 11
},
methods: {
test () {
console.log('這是一個(gè)主方法')
},
handleClick () {
this.$toast('hello world')
}
},
created() {
console.log('執(zhí)行了主組件上的created方法')
},
})
</script>
</body>
</html>