淺析vue封裝自定義插件

在使用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)用豪墅。比如:

  1. Vue.extend作用是組裝組件的構(gòu)造方法VueComponent

  2. new TempConstructor()是實(shí)例化組件實(shí)例。實(shí)例化構(gòu)造方法黔寇,只是對(duì)組件的狀態(tài)數(shù)據(jù)進(jìn)行了初始化偶器,并沒有解析組件的template,也沒有后續(xù)的生成vnode和解析vnode

  3. instance.$mount(div)的作用是解析模板文件缝裤,生成render函數(shù)屏轰,進(jìn)而調(diào)用createElement生成vnode,最后生成真實(shí)DOM,將生成的真實(shí)DOM掛載到實(shí)例instance$el屬性上憋飞,也就是說霎苗,實(shí)例instance.$el即為組件實(shí)例化最終的結(jié)果。

  4. 組件中榛做,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)式的聲明屬性的getset。當(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>
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末宋列,一起剝皮案震驚了整個(gè)濱河市昭抒,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌炼杖,老刑警劉巖灭返,帶你破解...
    沈念sama閱讀 216,496評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異坤邪,居然都是意外死亡熙含,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,407評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門艇纺,熙熙樓的掌柜王于貴愁眉苦臉地迎上來怎静,“玉大人,你說我怎么就攤上這事黔衡◎酒福” “怎么了?”我有些...
    開封第一講書人閱讀 162,632評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵盟劫,是天一觀的道長夜牡。 經(jīng)常有香客問我,道長侣签,這世上最難降的妖魔是什么塘装? 我笑而不...
    開封第一講書人閱讀 58,180評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮硝岗,結(jié)果婚禮上氢哮,老公的妹妹穿的比我還像新娘。我一直安慰自己型檀,他們只是感情好冗尤,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,198評(píng)論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著胀溺,像睡著了一般裂七。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上仓坞,一...
    開封第一講書人閱讀 51,165評(píng)論 1 299
  • 那天背零,我揣著相機(jī)與錄音,去河邊找鬼无埃。 笑死徙瓶,一個(gè)胖子當(dāng)著我的面吹牛毛雇,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播侦镇,決...
    沈念sama閱讀 40,052評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼灵疮,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了壳繁?” 一聲冷哼從身側(cè)響起震捣,我...
    開封第一講書人閱讀 38,910評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎闹炉,沒想到半個(gè)月后蒿赢,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,324評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡渣触,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,542評(píng)論 2 332
  • 正文 我和宋清朗相戀三年羡棵,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片嗅钻。...
    茶點(diǎn)故事閱讀 39,711評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡晾腔,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出啊犬,到底是詐尸還是另有隱情灼擂,我是刑警寧澤,帶...
    沈念sama閱讀 35,424評(píng)論 5 343
  • 正文 年R本政府宣布觉至,位于F島的核電站剔应,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏语御。R本人自食惡果不足惜峻贮,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,017評(píng)論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望应闯。 院中可真熱鬧纤控,春花似錦、人聲如沸碉纺。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,668評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽骨田。三九已至耿导,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間态贤,已是汗流浹背舱呻。 一陣腳步聲響...
    開封第一講書人閱讀 32,823評(píng)論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留悠汽,地道東北人箱吕。 一個(gè)月前我還...
    沈念sama閱讀 47,722評(píng)論 2 368
  • 正文 我出身青樓芥驳,卻偏偏與公主長得像,于是被迫代替她去往敵國和親茬高。 傳聞我的和親對(duì)象是個(gè)殘疾皇子晚树,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,611評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容

  • 回憶 首先,render函數(shù)中手寫h=>h(app)雅采,new Vue()實(shí)例初始化init()和原來一樣。$mou...
    LoveBugs_King閱讀 2,279評(píng)論 1 2
  • 基本用法 一慨亲、vuejs簡介 是一個(gè)構(gòu)建用戶界面的框架 是一個(gè)輕量級(jí)的MVVM(Model-View-ViewMo...
    深度剖析JavaScript閱讀 18,249評(píng)論 0 8
  • ## 框架和庫的區(qū)別?> 框架(framework):一套完整的軟件設(shè)計(jì)架構(gòu)和**解決方案**婚瓜。> > 庫(lib...
    Rui_bdad閱讀 2,906評(píng)論 1 4
  • 冒泡排序的原理是一直比較相鄰的兩個(gè)數(shù)的值,如果左邊比右邊位置的值要大刑棵,則將這兩個(gè)位置的值互換巴刻。當(dāng)?shù)谝惠喲h(huán)完成,最...
    lkmc2閱讀 1,415評(píng)論 0 0
  • 本次學(xué)習(xí)內(nèi)容: 目標(biāo)詞匯: Actions: put on, take off, first, then 目標(biāo)句型...
    TimmySHENX閱讀 186評(píng)論 0 0