Vue动态创建组件实例并挂载到body

发布时间 2023-04-11 18:18:42作者: front-gl

方式一

import Vue from 'vue'

/**
 * @param Component 组件实例的选项对象
 * @param props 组件实例中的prop
 */
export function create(Component, props) {
  const comp = new (Vue.extend(Component))({ propsData: props }).$mount()
  
  document.body.appendChild(comp.$el)

  comp.remove = () => {
    document.body.removeChild(comp.$el)

    comp.$destroy()
  }

  return comp
}

方式二

import Vue from 'vue'

export function create(Component, props) {
  // 借鸡生蛋new Vue({render() {}}),在render中把Component作为根组件
  const vm = new Vue({
    // h是createElement函数,它可以返回虚拟dom
    render(h) {
      console.log(h(Component,{ props }));
      
      // 将Component作为根组件渲染出来
      // h(标签名称或组件配置对象,传递属性、事件等,孩子元素)
      return h(Component, { props })
    }
  }).$mount() // 挂载是为了把虚拟dom变成真实dom
  // 不挂载就没有真实dom
  // 手动追加至body
  // 挂载之后$el可以访问到真实dom
  document.body.appendChild(vm.$el)

  console.log(vm.$children);
  
  // 实例
  const comp = vm.$children[0]

  // 淘汰机制
  comp.remove = () => {
    // 删除dom
    document.body.removeChild(vm.$el)

    // 销毁组件
    vm.$destroy()
  }

  // 返回Component组件实例
  return comp
}

 

使用

  • A组件(要动态创建的组件)

 

<template>
  <div class="a">
    <h2>{{ title }}</h2>
    <p>{{ data }}</p>
  </div>
</template>

<script>
export default {
  props: {
    title: {
      type: String,
      default: "hello world!"
    },
    message: {
      type: String,
      default: "o(∩_∩)o 哈哈"
    },
    duration: {
      type: Number,
      default: 1000
    }
  },
  data() {
    return {
      data: "我是a组件",
    };
  },
  created() {
    let num = 1
    
    const timer = setInterval(() => {
      this.data = num++
    }, this.duration)

    this.$once("hook: beforeDestroy", () => clearInterval(timer))
  }
};
</script>

<style>
.a {
  position: fixed;
  width: 100%;
  top: 16px;
  left: 0;
  text-align: center;
  pointer-events: none;
  background-color: #fff;
  border: grey 3px solid;
  box-sizing: border-box;
}
</style>

  

  • B组件(操作动态创建组件的地方)
    <template>
      <div class="b">
        <button @click="createA">创建</button>
      </div>
    </template>
    
    <script>
    import A from "@/components/A.vue"
    import { create } from "@/utils/create.js"
    export default {
    
      components: {
        A,
      },
      methods: {
        createA() {
          // 创建A组件,并挂载到body上
          create(A, { title: "vue", message: "么么哒?" })   // 可以实现功能。,没问题,但是每次引入那么多组件,方法很不方便,下面改写成用this.$comp.来调用
        }
      },
    };
    </script>

     

  图片点击放大组件改写:

   需求: 组件需要挂载到body节点上,因为弹窗里面再弹窗会导致遮罩层很小的问题,这里就需要重新挂载到body上

          

   imgBig.vue 组件

 

<template>
  <div v-if="imgBigVisible1" tabindex="-1" class="img-viewer__wrapper" style="z-index: 9999"> <!--v-if="imgBigVisible1"-->
    <div class="img-viewer__mask"></div>
    <span class="img-viewer__btn img-viewer__close" @click="close">
      <i class="el-icon-circle-close"></i>
    </span>
    <div class="img-viewer__btn img-viewer__actions">
      <div class="img-viewer__actions__inner">
        <i class="el-icon-zoom-out" @click="zoomOut"></i>
        <i class="el-icon-zoom-in" @click="zoomIn"></i>
        <i class="img-viewer__actions__divider"></i>
        <i :class="[original?'el-icon-full-screen':'el-icon-c-scale-to-original']" @click="zoom"></i>
        <i class="img-viewer__actions__divider"></i>
        <i class="el-icon-refresh-left" @click="rotateLeft"></i>
        <i class="el-icon-refresh-right" @click="rotateRight"></i>
      </div>
    </div>
    <div class="img-viewer__canvas">
      <img
        :src="srcURL"
        class="img-viewer__img"
        :style="[{transform: `scale(${scale}) rotate(${deger}deg)`}, original?maxStyle:'']"
      />
    </div>
  </div>
</template>
<script>
export default {
  props: {
    // imgBigVisible: {
    //   type: Boolean,
    //   default: false
    // },
    srcURL: {
      type: String,
      default: ''
    }
  },
  data() {
    return {
      imgBigVisible1: false,
      scale: 1,
      deger: 0,
      original: false,
      maxStyle: {
        maxHeight: '100%',
        maxWidth: '100%'
      }
    };
  },
  computed: {
    
  },
  watch: {
    // imgBigVisible (v, ov) {
    //   console.log('props imgBigVisible', v)
    //   this.imgBigVisible1 = v
    // },
    srcURL (v, ov) {   // props传值
      console.log('src====', typeof v)
    }
  },
  methods: {
    show () {
      this.imgBigVisible1 = true
    },
    close () {
      this.imgBigVisible1 = false
      this.$emit('update:imgBigVisible', this.imgBigVisible1)
    },
    zoom () {
      this.original = !this.original
      this.scale = 1
      this.deger = 0
    },
    zoomOut () {
      this.scale -= 0.2
      if (this.scale < 0.2) {
        this.scale = 0.2
        return 
      }
    },
    zoomIn () {
      this.scale += 0.2
    },
    rotateLeft () {
      this.deger -= 90
    },
    rotateRight () {
      this.deger += 90
    }
  },
};
</script>
<style lang="scss" scoped>
.img-viewer__wrapper {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  .img-viewer__mask {
    position: absolute;
    width: 100%;
    height: 100%;
    top: 0;
    left: 0;
    opacity: .5;
    background: #000; 
  }
  .img-viewer__btn {
    position: absolute;
    z-index: 1;
    display: -ms-flexbox;
    display: flex;
    -ms-flex-align: center;
    align-items: center;
    -ms-flex-pack: center;
    justify-content: center;
    border-radius: 50%;
    opacity: .8;
    cursor: pointer;
    box-sizing: border-box;
    -webkit-user-select: none;
    -moz-user-select: none;
    -ms-user-select: none;
    user-select: none;
  }
  .img-viewer__close {
    top: 40px;
    right: 40px;
    width: 40px;
    height: 40px;
    font-size: 40px;
    color: #c7c7c7;
  }
  
  .img-viewer__actions {
    left: 50%;
    bottom: 30px;
    transform: translateX(-50%);
    width: 282px;
    height: 44px;
    padding: 0 23px;
    background-color: #606266;
    border-color: #fff;
    border-radius: 22px;
    .img-viewer__actions__inner {
        width: 100%;
        height: 100%;
        text-align: justify;
        cursor: default;
        font-size: 23px;
        color: #fff;
        display: -ms-flexbox;
        display: flex;
        -ms-flex-align: center;
        align-items: center;
        -ms-flex-pack: distribute;
        justify-content: space-around;
    }
  }
  .img-viewer__canvas {
    width: 100%;
    height: 100%;
    display: -ms-flexbox;
    display: flex;
    -ms-flex-pack: center;
    justify-content: center;
    -ms-flex-align: center;
    align-items: center;
  }
  .img-viewer__img {
    margin-left: 0px;
    margin-top: 0px;
    transition: transform 0.3s ease 0s;
  }
}
</style>

 index.js 

 

import Vue from 'vue'
import imgBig from './imgBig'

let imgBigConstructor = Vue.extend(imgBig)
const myImgBig = (props) => {
  const instance = new imgBigConstructor({ // 生成实例
    propsData: props  // 注意这里propsData不要写错了
  })
  instance.$mount() // 实例挂载
  document.body.appendChild(instance.$el);//3原生方法插入body
  return instance
}

export default myImgBig

 

main.js 引入

import imgBig from '@/temp/imgBig/index.js'
Vue.prototype.$imgBig = imgBig

父组件调用

<span v-if="isImgFile(item.name)" style="cursor:pointer;" @click="viewImgBig(item.url)">{{item.name}}</span>
...

isImgFile(value){
   return value.endWith('.jpg') || value.endWith('.jpeg') || value.endWith('.png');
},

viewImgBig (src) {
 this.$imgBig({srcURL: src}).show()
},