Vue組件化實踐:2. 實現(xiàn)自定義彈窗插件

這個實現(xiàn)過程呢,會涉及render、Vue.extend、Vue插件等知識

分析需求

彈窗的特點:

  1. 位置不相對某個元素,而是相對于整個視窗,通常掛載于body,也就是要在vue的根實例app之外的。這樣,不會影響別的內(nèi)容的布局,也方便我們調(diào)整彈窗的位置
  2. 通過js創(chuàng)建的,不需要在任何組件中聲明,即開即用型
    大概是要這樣的效果:
// 可定制彈窗的標題,內(nèi)容,以及顯示幾秒,提示的類型,這個有點類似element的toast
this.$notice({
  title: "自定義彈窗標題",
  content: "彈窗內(nèi)容",
  duration: 1000,
  type: "success" // 類似element組件,可選success, warning, error
})

實現(xiàn)過程

1. 先寫要實現(xiàn)的組件Notice組件

<!-- Notice.vue -->
<template>
  <div ref="notice" class="notice-wrap">
    <h1 class="title">{{ title }}</h1>
    <p class="content">
      <span class="iconfont" :class="iconType"></span>
      <span class="text">{{ content }}</span>
    </p>
  </div>
</template>

<script>
export default {
  name: "notice",
  components: {},
  props: {
    // 標題
    title: {
      type: String,
      default: "提示",
    },
    // 提示內(nèi)容
    content: {
      type: String,
      default: "內(nèi)容",
    },
    // 幾秒后,關(guān)閉彈窗,默認1s
    duration: {
      type: Number,
      default: 1000,
    },
    // 要顯示的圖標的類型:success, warning ,error
    type: {
      type:String,
      default: 'success'
    }
  },
  data() {
    return  {
      isShow: false,
    }
  },
  computed: {
    // 這里的圖標,我是用iconfont來實現(xiàn),所以要先去官網(wǎng)把這個圖下下來: https://www.iconfont.cn/collections/detail?spm=a313x.7781069.1998910419.d9df05512&cid=22664
    // iconfont這里我就不詳細介紹了,就是個圖標,你不要這個,直接注釋掉也不會影響你了解過程
    iconType() {
      if(this.type === 'success') {
        return 'icon-success-filling'
      }else if(this.type === 'warning'){
        return 'icon-prompt-filling'
      }else {
        return 'icon-delete-filling'
      }
    }
  },
  methods: {}
};
</script>

<style scoped>
.notice-wrap {
  /* height: 200px; */
  width: 400px;
  position: absolute;
  top: 10%;
  left: 30%;
  border: 1px solid rgba(58,58,58, 0.2);
  border-radius: 4px;
  box-shadow: 10px 10px 5px #888888;
  padding: 16px;
}
.title {
  margin: 0;
  font-size: 20px;
}
.content {
  font-size: 14px;
}
.icon-success-filling{
  color: green;
}
.icon-prompt-filling {
  color: orange;
}
.icon-delete-filling {
  color: red;
}
.text {
  display: inline-block;
  margin-left: 8px;
}
</style>

2. 將組件實例化,并轉(zhuǎn)化成真實dom

組件的實例化

一說到實例化,學過面向?qū)ο蟮耐瑢W第一反應就是構(gòu)造函數(shù)了。所以我們要先創(chuàng)建一個組件的構(gòu)造函數(shù),將我們的Notice.vue組件(配置對象)轉(zhuǎn)化成一個虛擬節(jié)點。然后再將虛擬節(jié)點轉(zhuǎn)化成真實dom,然后掛載到頁面上。

構(gòu)造函數(shù)的創(chuàng)建一般會有兩種方法:

使用render渲染函數(shù)
使用render渲染函數(shù),在我們可能還不知道怎么操作時,看看main.js文件中是怎么做的:

new Vue({
  render: h => h(App),
}).$mount('#app')

其實它做這幾件事:

  • render函數(shù)中,h其實是createElement的意思,因其頻繁使用,且在源碼中被命名為h, 固我們也就都叫h。h的作用是將xxx.vue組件轉(zhuǎn)化成了一個虛擬節(jié)點(VNode)。
  • $mount的作用,是將VNode轉(zhuǎn)化為真實Dom,并掛載在指定的真實節(jié)點中,這里,也就是掛載到App.vue組件中的id為app的div上,相當于:
    js document.getElememntById("app").appendChild(this.$el)
  • 如果$mount的函數(shù)不寫任何參數(shù)(注意不能直接寫body,官方說了不允許!),那么它依然會將VNode轉(zhuǎn)化為真實Dom,但是不進行掛載追加。生成的dom呢,我們可以在它的實例對象$el獲取
// notice/index.js
import Vue from 'vue'
import Notice from './Notice.vue'

function create(props) {
  // 類似main.js中的用法
  const vm = new Vue({
    // props是傳給Notice組件中的props屬性
    render: h => h(Notice, {props})
  })
  // 將vm轉(zhuǎn)化成真實dom
  vm.$mount()
  // 將真實dom,掛載到body上
  document.body.appendChild(vm.$el)
}
export default create;

使用Vue.extends
學習東西呢,老樣子,官網(wǎng)先走一波,官網(wǎng)傳送門: https://cn.vuejs.org/v2/api/index.html#Vue-extend

這里,白話解釋一下,Vue.extend()其實就是用來創(chuàng)建組件的構(gòu)造函數(shù)的,然后使用這個構(gòu)造函數(shù)創(chuàng)建出Vue的虛擬節(jié)點Vnode

// notice/index.js
import Vue from 'vue'
import Notice from './Notice.vue'

function create(props) {
  // 使用Vue.extend創(chuàng)建構(gòu)造函數(shù),MyComponent是自定義的vue組件(MyComponent.vue)
  const NoticeConstrutor = Vue.extend(Notice)
  // 構(gòu)造函數(shù)的參數(shù),propsData相當于我們組件MyComponent.vue里需要的props,這里為了和vue文件中的props沖突,所以官方取了個別名 
  // 然后實例化后,會生成一個vue組件對應的虛擬節(jié)點
  const notice = new NoticeConstrutor({propsData:props})
  // 有了實例后,最后和render一樣,使用$mount進行掛載
  notice.$mount()
  // 將真實dom,掛載到body上,注意,這里的實例變成notice
  document.body.appendChild(notice.$el)
}

export default create;

到了這一步,我們就可以實現(xiàn)一個彈窗的功能了,新建一個組件測試:

<!-- notice/test.vue -->
<script>
import create from '@/notice/index.js'

export default {
  name: "Test",
  mounted() {
    create({
      title: "測試彈窗",
      content: "測試彈窗內(nèi)容",
      duration: 2000,
      type: "error",
    });
  },
}
</script>

3. 銷毀操作

我們彈窗設(shè)計,肯定某個動作后會觸發(fā),比如提交表單之類的,那么這個動作肯定也會不只一次觸發(fā),如果觸發(fā)多次后,就會多次調(diào)用create方法后,如果不做銷毀操作,就會一直往body上追加彈窗節(jié)點,這不是我們想看到的,所以做一下收尾工作了:

  • 將彈窗的dom從body上銷毀
  • 將彈窗的實例對象銷毀,釋放內(nèi)存
import Vue from 'vue'
import Notice from './Notice.vue'

function create(props) {
  // 2. 使用Vue.extend的方法創(chuàng)建
  const NoticeCons = Vue.extend(Notice)
  const notice = new NoticeCons({propsData: props})
  notice.$mount()
  document.body.appendChild(notice.$el)

  // 添加銷毀操作
  function remove() {
    // 將真實dom節(jié)點干掉
    document.body.removeChild(vm.$el)
    // 將虛擬節(jié)點占的內(nèi)存也釋放掉
    vm.$destroy()
  }
  // 在幾秒后,進行銷毀操作
  if(props.duration) {
    setTimeout(() => {
      remove()
    }, props.duration)
  }
}
export default create;

這時候,再進行測試,就會發(fā)現(xiàn)彈窗2s后自動消失了


Q11.png

4. 使用插件的方式,將彈窗注入Vue原型上

彈窗肯定是不只一個地方會用到的,想想,如果我們在多個文件里要用到彈窗,是不是每次都得:

import create from '@/notice/index.js'
create({ //...
});
  • 很麻煩,我們就想像element的彈窗一樣,只使用this.$message就可以創(chuàng)建調(diào)用,這時候插件就派上用場了:
// notice/index
import Vue from 'vue'
import Notice from './Notice.vue'

function create(props) {
  // 1. 使用render
  // 類似main.js中的用法
  // const vm = new Vue({
  //   render: h => h(Notice, {props})
  // })
  // // 將vm轉(zhuǎn)化成真實dom
  // vm.$mount()
  // // 將真實dom,掛載到body上
  // document.body.appendChild(vm.$el)
  // console.log('vm.$el:', vm.$el);
  // function remove() {
  //   // 將真實dom節(jié)點干掉
  //   document.body.removeChild(vm.$el)
  //   // 將虛擬節(jié)點占的內(nèi)存也釋放掉
  //   vm.$destroy()
  // }

  // 2. 使用Vue.extend
  const NoticeCons = Vue.extend(Notice)
  const notice = new NoticeCons({propsData: props})
  notice.$mount()
  document.body.appendChild(notice.$el)

  // 將remove掛載到實例上,這樣組件里,以后可以調(diào)用this.remove()來執(zhí)行這個方法
  notice.remove = function() {
    // 將真實dom節(jié)點干掉
    document.body.removeChild(notice.$el)
    // 將虛擬節(jié)點占的內(nèi)存也釋放掉
    notice.$destroy()
  }
  if(props.duration) {
    setTimeout(() => {
      notice.remove()
    }, props.duration)
  }
  return notice;
}

// 插件走一波
const noticePlugin = {
  install: function(Vue, options) {
    // 將這個方法掛載到Vue.prototype.$notice上,就可以使用this.$notice來調(diào)用了
    Vue.prototype.$notice = create
  }
}
export default noticePlugin;
  • 在入口文件main.js中調(diào)用插件:
// main.js
import Vue from 'vue'
// 導入插件
import noticePlugin from '@/notice/index'
// 使用插件
Vue.use(noticePlugin)

new Vue({
  router, // 注意key是小寫
  store,
  render: h => h(App),
}).$mount('#app')
  • 這樣,調(diào)用時就可以省了導入操作,直接使用this.$notice來調(diào)用
<!-- notice/test.vue -->
<script>
export default {
  name: "Test",
  mounted() {
    this.$notice({
      title: "測試彈窗",
      content: "測試彈窗內(nèi)容",
      duration: 2000,
      type: "error",
    });
  },
  methods: {},
};
</script>
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容