Vue组件化实践:2. 实现自定义弹窗插件

这个实现过程呢,会涉及render、Vue.extend、Vue插件等知识

分析需求

弹窗的特点:

  1. 位置不相对某个元素,而是相对于整个视窗,通常挂载于body,也就是要在vue的根实例app之外的。这样,不会影响别的内容的布局,也方便我们调整弹窗的位置
  2. 通过js创建的,不需要在任何组件中声明,即开即用型
    大概是要这样的效果:
// 可定制弹窗的标题,内容,以及显示几秒,提示的类型,这个有点类似element的toast
this.$notice({
  title: "自定义弹窗标题",
  content: "弹窗内容",
  duration: 1000,
  type: "success" // 类似element组件,可选success, warning, error
})

实现过程

1. 先写要实现的组件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: "提示",
    },
    // 提示内容
    content: {
      type: String,
      default: "内容",
    },
    // 几秒后,关闭弹窗,默认1s
    duration: {
      type: Number,
      default: 1000,
    },
    // 要显示的图标的类型:success, warning ,error
    type: {
      type:String,
      default: 'success'
    }
  },
  data() {
    return  {
      isShow: false,
    }
  },
  computed: {
    // 这里的图标,我是用iconfont来实现,所以要先去官网把这个图下下来: 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. 将组件实例化,并转化成真实dom

组件的实例化

一说到实例化,学过面向对象的同学第一反应就是构造函数了。所以我们要先创建一个组件的构造函数,将我们的Notice.vue组件(配置对象)转化成一个虚拟节点。然后再将虚拟节点转化成真实dom,然后挂载到页面上。

构造函数的创建一般会有两种方法:

使用render渲染函数
使用render渲染函数,在我们可能还不知道怎么操作时,看看main.js文件中是怎么做的:

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

其实它做这几件事:

  • render函数中,h其实是createElement的意思,因其频繁使用,且在源码中被命名为h, 固我们也就都叫h。h的作用是将xxx.vue组件转化成了一个虚拟节点(VNode)。
  • $mount的作用,是将VNode转化为真实Dom,并挂载在指定的真实节点中,这里,也就是挂载到App.vue组件中的id为app的div上,相当于:
    js document.getElememntById("app").appendChild(this.$el)
  • 如果$mount的函数不写任何参数(注意不能直接写body,官方说了不允许!),那么它依然会将VNode转化为真实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转化成真实dom
  vm.$mount()
  // 将真实dom,挂载到body上
  document.body.appendChild(vm.$el)
}
export default create;

使用Vue.extends
学习东西呢,老样子,官网先走一波,官网传送门: https://cn.vuejs.org/v2/api/index.html#Vue-extend

这里,白话解释一下,Vue.extend()其实就是用来创建组件的构造函数的,然后使用这个构造函数创建出Vue的虚拟节点Vnode

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

function create(props) {
  // 使用Vue.extend创建构造函数,MyComponent是自定义的vue组件(MyComponent.vue)
  const NoticeConstrutor = Vue.extend(Notice)
  // 构造函数的参数,propsData相当于我们组件MyComponent.vue里需要的props,这里为了和vue文件中的props冲突,所以官方取了个别名 
  // 然后实例化后,会生成一个vue组件对应的虚拟节点
  const notice = new NoticeConstrutor({propsData:props})
  // 有了实例后,最后和render一样,使用$mount进行挂载
  notice.$mount()
  // 将真实dom,挂载到body上,注意,这里的实例变成notice
  document.body.appendChild(notice.$el)
}

export default create;

到了这一步,我们就可以实现一个弹窗的功能了,新建一个组件测试:

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

export default {
  name: "Test",
  mounted() {
    create({
      title: "测试弹窗",
      content: "测试弹窗内容",
      duration: 2000,
      type: "error",
    });
  },
}
</script>

3. 销毁操作

我们弹窗设计,肯定某个动作后会触发,比如提交表单之类的,那么这个动作肯定也会不只一次触发,如果触发多次后,就会多次调用create方法后,如果不做销毁操作,就会一直往body上追加弹窗节点,这不是我们想看到的,所以做一下收尾工作了:

  • 将弹窗的dom从body上销毁
  • 将弹窗的实例对象销毁,释放内存
import Vue from 'vue'
import Notice from './Notice.vue'

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

  // 添加销毁操作
  function remove() {
    // 将真实dom节点干掉
    document.body.removeChild(vm.$el)
    // 将虚拟节点占的内存也释放掉
    vm.$destroy()
  }
  // 在几秒后,进行销毁操作
  if(props.duration) {
    setTimeout(() => {
      remove()
    }, props.duration)
  }
}
export default create;

这时候,再进行测试,就会发现弹窗2s后自动消失了


Q11.png

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

弹窗肯定是不只一个地方会用到的,想想,如果我们在多个文件里要用到弹窗,是不是每次都得:

import create from '@/notice/index.js'
create({ //...
});
  • 很麻烦,我们就想像element的弹窗一样,只使用this.$message就可以创建调用,这时候插件就派上用场了:
// 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转化成真实dom
  // vm.$mount()
  // // 将真实dom,挂载到body上
  // document.body.appendChild(vm.$el)
  // console.log('vm.$el:', vm.$el);
  // function remove() {
  //   // 将真实dom节点干掉
  //   document.body.removeChild(vm.$el)
  //   // 将虚拟节点占的内存也释放掉
  //   vm.$destroy()
  // }

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

  // 将remove挂载到实例上,这样组件里,以后可以调用this.remove()来执行这个方法
  notice.remove = function() {
    // 将真实dom节点干掉
    document.body.removeChild(notice.$el)
    // 将虚拟节点占的内存也释放掉
    notice.$destroy()
  }
  if(props.duration) {
    setTimeout(() => {
      notice.remove()
    }, props.duration)
  }
  return notice;
}

// 插件走一波
const noticePlugin = {
  install: function(Vue, options) {
    // 将这个方法挂载到Vue.prototype.$notice上,就可以使用this.$notice来调用了
    Vue.prototype.$notice = create
  }
}
export default noticePlugin;
  • 在入口文件main.js中调用插件:
// 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')
  • 这样,调用时就可以省了导入操作,直接使用this.$notice来调用
<!-- notice/test.vue -->
<script>
export default {
  name: "Test",
  mounted() {
    this.$notice({
      title: "测试弹窗",
      content: "测试弹窗内容",
      duration: 2000,
      type: "error",
    });
  },
  methods: {},
};
</script>
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 210,914评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 89,935评论 2 383
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,531评论 0 345
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,309评论 1 282
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,381评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,730评论 1 289
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,882评论 3 404
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,643评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,095评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,448评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,566评论 1 339
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,253评论 4 328
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,829评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,715评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,945评论 1 264
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,248评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,440评论 2 348