全局挂载组件之Vue.extend

Vue.extend 属于Vue的全局 api,在实际业务开发中我们很少使用,因为相比常用的 Vue.component 写法使用 extend 步骤要更加繁琐一些。但是在一些独立组件开发场景中(例如:ElementUI库),所以Vue.extend + $mount 这对组合非常有必要需要我们了解下。

# Vue.component

文档 (opens new window)

官网用法

注册或获取全局组件。注册还会自动使用给定的 id 设置组件的名称

// 注册组件,传入一个扩展过的构造器
Vue.component('my-component', Vue.extend({ /* ... */ }))

// 注册组件,传入一个选项对象 (自动调用 Vue.extend)
Vue.component('my-component', { /* ... */ })

// 获取注册的组件 (始终返回构造器)
var MyComponent = Vue.component('my-component')
let ElInput = Vue.component('ElInput');
console.log(new ElInput);  // 就是Inout的实例
1
2
3
4
5
6
7
8
9
10

用法也特别的简单,你写好的组件,直接在main.js里面导入然后使用Vue.component('xx-xxx',xxx)就可以全局通用了。

  • 好处
    • 这样的好处就是所有页面基本上都是通过 router 来管理可以直接注册,组件的创建我们不需要去关注,相比 extend 要更省心一点
  • 缺点
    • 组件的名称都是自定义的,如果我要从接口动态渲染怎么办。【extend不用必须在初始化的时候完成,下面有实例】
    • 有内容都是在 #app 下渲染,注册组件都是在当前位置渲染。如果我要实现一个模态框的提示组件,就比较鸡肋了。

这时候,Vue.extend + vm.$mount 组合就派上用场了。

# Vue.extend

文档 (opens new window)

官网用法:

<div id="mount-point"></div>
1
// 创建构造器
var Profile = Vue.extend({
  template: '<p>{{firstName}} {{lastName}} aka {{alias}}</p>',
  data: function () {
    return {
      firstName: 'Walter',
      lastName: 'White',
      alias: 'Heisenberg'
    }
  }
})
// 创建 Profile 实例,并挂载到一个元素上。
new Profile().$mount('#mount-point')
1
2
3
4
5
6
7
8
9
10
11
12
13

结果如下:

<p>Walter White aka Heisenberg</p>
1

可以发现Idmount-point的div ,是直接被替代了,没有在内部填充,在我们 main.js 初始化的时候用发其实也是一样:

new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app');  // 此处都是替换,不是填充
1
2
3
4
5

下面简单的分析下$mount的源码

// src/platforms/web/entry-runtime-with-compiler.js
import config from 'core/config'
import {warn, cached} from 'core/util/index'
import {mark, measure} from 'core/util/perf'

import Vue from './runtime/index'
import {query} from './util/index'
import {compileToFunctions} from './compiler/index'
import {shouldDecodeNewlines, shouldDecodeNewlinesForHref} from './util/compat'

const idToTemplate = cached(id => {
  const el = query(id)
  return el && el.innerHTML
})

const mount = Vue.prototype.$mount;  // 缓存了原型上的 $mount 方法
Vue.prototype.$mount = function (el, hydrating) {
  el = el && query(el)  // 通过 query函数 就是获取真实的DOM元素

  // 判断获取的真实 dom元素是否为 body 或 documentElement 报警告
  if (el === document.body || el === document.documentElement) {
    process.env.NODE_ENV !== 'production' && warn(
      `Do not mount Vue to <html> or <body> - mount to normal elements instead.`
    )
    return this
  }

  const options = this.$options
  // 判断有么有render , 咱们一般都是用 template 写的vue,需要编译
  if (!options.render) {
    let template = options.template
    if (template) {
      if (typeof template === 'string') {
        if (template.charAt(0) === '#') {
          template = idToTemplate(template)
          /* istanbul ignore if */
          if (process.env.NODE_ENV !== 'production' && !template) {
            warn(
              `Template element not found or is empty: ${options.template}`,
              this
            )
          }
        }
      } else if (template.nodeType) {
        template = template.innerHTML
      } else {
        if (process.env.NODE_ENV !== 'production') {
          warn('invalid template option:' + template, this)
        }
        return this
      }
    } else if (el) {
      template = getOuterHTML(el)  // 即获取 el 的 outerHTML
    }
    if (template) {
      // 然后通过compileToFunctions将 template 转化为 render 函数,options.render = render
      // 所有 Vue 的组件的渲染最终都需要 render 方法,无论是用单页面 .vue 方式开发,还是写了 el 或者 template 属性,
      // 最终都要被转成 render 方法,那么这个过程是 Vue 的一个 “在线编译” 的过程, 它是调用 compileToFunctions 方法实现的,最后调用原型上的 $mount 方法挂载。
      const {render, staticRenderFns} = compileToFunctions(template, {
        outputSourceRange: process.env.NODE_ENV !== 'production',
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      options.render = render
      options.staticRenderFns = staticRenderFns
    }
  }
  // 如果没有传el的话,会直接执行这步,会把组件在内存中渲染完毕
  return mount.call(this, el, hydrating)
}
// 及时获取HTML 兼容IE
function getOuterHTML(el): string {
  if (el.outerHTML) {
    return el.outerHTML
  } else {
    const container = document.createElement('div')
    container.appendChild(el.cloneNode(true))
    return container.innerHTML
  }
}
Vue.compile = compileToFunctions

export default Vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
  • $mount方法支持传入 2 个参数,第一个是 el,它表示挂载的元素,可以是字符串,也可以是 DOM 对象,如果是字符串在浏览器环境下会调用 query 方法转换成 DOM 对象的。第二个参数是和服务端渲染相关,在浏览器环境下不需要传第二个参数。

  • $mount方法实际上会调用mountComponent 方法,方法定义在 src/core/instance/lifecycle.js

# 实现一个弹框组件

// message/src/index.vue
<template>
    <div class="wrap">
        <div class="message" :class="item.type" v-for="item in notices" :key="item._name">
            <div class="content">{{item.content}}</div>
        </div>
    </div>
</template>

<script>
  // 默认选项
  const defaultOptions = {
    duration: 1500,
    type: 'info',
    content: '这是一条提示信息!',
  }
  let mid = 0
  export default {
    name:'MyMessage', // 建议添加方便外面直接取值
    data() {
      return {
        notices: []
      }
    },
    methods: {
      addMessage(notice = {}) {
        // name标识 用于移除弹窗
        let _name = this.getName()
        // 合并选项
        notice = Object.assign({
          _name
        }, defaultOptions, notice)

        this.notices.push(notice)

        setTimeout(() => {
          this.removeNotice(_name)
        }, notice.duration) 
      },
      getName() {
        return 'msg_' + (mid++)  //创建一个唯一的值 
      },
      removeNotice(_name) {
        let index = this.notices.findIndex(item => item._name === _name)
        this.notices.splice(index, 1)  // 删除当前超时的dom
      }
    }
  }
</script>

<style scoped>
    .wrap {
        position: fixed;
        top: 50px;
        left: 50%;
        display: flex;
        flex-direction: column;
        align-items: center;
        transform: translateX(-50%);
    }
    .message {
        --borderWidth: 3px;
        min-width: 240px;
        max-width: 500px;
        margin-bottom: 10px;
        border-radius: 3px;
        box-shadow: 0 0 8px #ddd;
        overflow: hidden;
    }
    .content {
        padding: 8px;
        line-height: 1.3;
    }

    // 对应的集中状态 
    .message.info {
        border-left: var(--borderWidth) solid #909399;
        background: #F4F4F5;
    }
    .message.success {
        border-left: var(--borderWidth) solid #67C23A;
        background: #F0F9EB;
    }
    .message.error {
        border-left: var(--borderWidth) solid #F56C6C;
        background: #FEF0F0;
    }
    .message.warning {
        border-left: var(--borderWidth) solid #E6A23C;
        background: #FDF6EC;
    }
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
// message/index.js
import Vue from 'vue'
import Message from './src/index.vue'

let messageInstance = null
let TempMessage = ''

// 模拟异步请求
setTimeout(() => {
  TempMessage = Message
}, 2000)

let init = () => {
  let MessageConstructor = TempMessage && Vue.extend(TempMessage)

  messageInstance = new MessageConstructor({})// 构造函数可以接传值,data、methods.....
  // console.log(messageInstance);

  // $mount()不带参数,会把组件在内存中渲染完毕
  messageInstance.$mount()

  // messageInstance.$el 拿到的就是组件对应的dom元素,可以直接操作dom
  document.body.appendChild(messageInstance.$el)
  messageInstance.$el.style.zIndex = 9999
}


let caller = (options) => {
  if (!messageInstance) {
    // 进页面初始化
    init(options)
  }
  // addMessage 是组件内部声明的方法,也可以通过构造函数传对应的方法
  messageInstance.addMessage(options)
}


export default {
  install(vue) {
    vue.prototype.$mymessage = caller
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// main.js
import Message from '@/components/Message/index.js'
Vue.use(Message)
1
2
3

使用

 this.$mymessage({
   type: 'warning',
   content: '你好坏啊,我好喜欢',
   duration: 6000
 })
1
2
3
4
5

# 参考文章

JavaScript 方式调用的组件 (opens new window)