爱总结,爱搬砖,爱生活

引言

最近在板砖途中遇到一个需要动态创建Vue组件并插入到页面上的需求,之前没有遇到过这类需求,经过一番百度搞定这个问题,在这里写下总结,作为回顾。

心得

动态创建组件依靠Vue.extend

案例引入

现在有下面这样一个需求需要用vue实现,图来…
案例引入

案例分析
案例中用户通过点击新增按钮,新加下一个页签,要实现这个需求只能通过动态的创建组件,然后添加到页面中,下面来实现这个需求(页签内容部分就不做了,重点放在动态创建页签按钮)

案例实现

<div id="app" class="app">
  <div class="title">XXX页面</div>
  <div id="tabBox" class="tabBox"></div>
  <div class="add" @click="add">+</div>
</div>

<template id="tab">
  <div class="tab">{{tabname}}</div>
</template>

<script src="./lib/vue.js"></script>
<script>
  const tab = {
    template: '#tab',
    props: ['tabname']
  }
  const vm = new Vue({
    data: {
      tabName: '',
      base: '页签',
      num: 1
    },
    methods: {
      add() {
        this.tabName = this.base + this.num
        this.num++
        const tabCmp = new (Vue.extend(tab))({propsData: {tabname: this.tabName}}).$mount()
        document.getElementById('tabBox').appendChild(tabCmp.$el)
      }
    }
  }).$mount('#app')
</script>

案例代码

代码分析

  1. 创建一个vue页面,页面内包含3个divtitle是页面的标题, tabBox用来放所有的页签,add监听用户的点击添加页签;
  2. 局部注册一个组件tab,传入了templateprops两项属性;
  3. 在父组件的methods中添加add事件处理函数,重点就在这个事件处理函数中,首先生成tabName,然后利用Vue.extend一番操作得到组件tabCmp,最后将创建的组件tabCmp插入到tabBox这个元素中;
  4. 当目前为止,当用户每次点击add按钮,便会在tabBox中插入一个tab组件,页面上也会做更新;
  5. 在这一连串的操作中核心就是Vue.extend,下面具体认识以下这个API。

关于Vue.extend

先看Vue官网对于这个API的解释:

使用基础 Vue 构造器,创建一个“子类”。参数是一个包含组件选项的对象。
data 选项是特例,需要注意 - 在 Vue.extend() 中它必须是函数

  • 通俗的讲Vue.extend就是一个构造函数,它可以生成一个Vue实例(子类),可以像使用Vue构造函数一样使用他。特别注意的是他里面的data一定要是函数,这一点很好理解隔离作用域;
  • 将前面关键代码拆分一下理解,如下:
const tabCmp = new Vue.extend({
  template: "#tab",
  props: ['tabname']
}).$mount()
  • 这样看就他跟Vue构造函数没区别,$mount()负责挂载,不是这里的重点,不赘述;
  • 但这还不够,这还不能完成上面的动态创建组件,props的值怎么传到tabCmp组件中,这里可以看一下Vue源码中关于Vue. extend的实现

Vue.extend的源码实现

下面代码片段是我从源码中摘取出来的

// 取自vue-js/src/core/global-api/extend.js
Vue.extend = function (extendOptions: Object): Function {
  // ...
  const Super = this
  // ...
  const Sub = function VueComponent (options) {
    this._init(options)
  }
  // ...
  Sub['super'] = Super
  // ...
  return Sub
}

// 取自vue-js/src/core/instance/init.js
Vue.prototype._init = function (options?: Object) {
  // ...
  if (options && options._isComponent) {
    // ...
  } else {
    vm.$options = mergeOptions(
      resolveConstructorOptions(vm.constructor),
      options || {},
      vm
      )
    }
  // ...
}
  • 跟着源码走一遍new Vue.extend()得到一个Sub, 调用Sub会执行的_init方法,_init方法接受一个options对象,在这里只传入propsData就相当于在标签中绑定了一个props值,继续往下代码会执行mergeOptions(),在这个函数中将父组件中的options和刚传入的子组件的options做合并并返回,然后赋值给父组件的options,至此就完成了在父组件中给动态组件传值的过程;
  • 回到案例中动态创建的组件new Vue.extend(tab)得到实例Sub,调用它并传入参数(new Vue.extend(tab))({propsData: {tabname: this.tabName}}),最后调用$mount挂载组件,至此就完成了组件的动态创建和挂载;
  • 回顾一下Vue.extend其实就是Vue暴露了一个接口允许我们在一个父Vue实例下动态的去创建另外一个子Vue实例并挂载到父Vue上。

动态创建的组件怎么监听自定义事件

<div id="app" class="app">
    <div>XXX页面</div>
    <div id="tabBox" class="tabBox"></div>
    <div class="add" @click="add" ref="add">+</div>
  </div>

  <template id="tab">
    <div class="tab" @click="changeColor">{{tabname}}</div>
  </template>

  <script src="./lib/vue.js"></script>
  <script>
    const tab = {
      template: '#tab',
      props: ['tabname'],
      methods: {
        changeColor() {
          this.$emit('change-color')
        }
      }
    }

    const vm = new Vue({
      data: {
        tabName: '',
        base: '页签',
        num: 1
      },
      methods: {
        add() {
          this.tabName = this.base + this.num
          this.num++
          const tabCmp = new (Vue.extend(tab))({propsData: {tabname: this.tabName}}).$mount()
          // 直接这样监听自定义事件
          tabCmp.$on('change-color', () => {
            let refAdd = this.$refs.add
            refAdd.style.backgroundColor = 'red'
          })
          document.getElementById('tabBox').appendChild(tabCmp.$el)
        }
      }
    }).$mount('#app')

  </script>

案例代码

  • 在之前案例的基础上给tab页签增加了一个点击事件changeColor
  • methods中添加函数并发射change-color事件;
  • 在动态创建组件的位置使用tabCmp.$on监听自定义事件;
  • 最后得到的效果是当用户点击+增加新的tab页签,用户点击页签,添加页签按钮背景变为红色。

封装动态创建组件的函数

function Create(components, propsData, parentNode) {
  this.cmp = null
  this.components = components
  this.propsData = propsData
  this._init()
  this._insert(parentNode)
}

Create.prototype._init = function() {
  this.cmp = new (Vue.extend(this.components))({propsData: this.propsData}).$mount()
}

Create.prototype._insert = function(parentNode) {
  parentNode.appendChild(this.cmp.$el)
}

Create.prototype.on = function(eventName, callback) {
  this.cmp.$on(eventName, callback)
}

使用封装的方法动态创建组件

const parent = document.getElementById('tabBox')
let create = new Create(tab, {tabname: this.tabName}, parent)
create.on('change-color', () => {
  let refAdd = this.$refs.add
  refAdd.style.backgroundColor = 'red'
})

案例代码
create.js
爱总结,爱搬砖,爱生活

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐