2023.03.27 更新
近期笔者复习了一下本文内容,发现文章内有一个细节没有提到,就是手动挂载的组件的销毁问题,比如本文介绍到的手动挂载登录组件,当我们调用 this.$loginPopup() 方法生成登录组件后,若用户点击关闭按钮关闭了该组件,此时应该将该组件销毁(不能将样式设置为 display: none,应该将 dom 结构移除。具体实现上,vue3 的版本实现可以参考这篇文章 JS 单例设计模式解读与实践(Vue 中的单例登录弹窗)_vue 单例_流光D的博客-CSDN博客 ,vue2 版本的实现可以参考 iview 的 notice 组件的 vue2 版本,iview/index.js at 2.0 · iview/iview · GitHub),否则重复调用 this.$loginPopup() 方法会在 dom 树中重复生成登录弹窗的 dom 结构。
——————————————————————————————————
前言
之前一段时间笔者因为换城市发展换工作等一系列不愉快的原因博客停更了==|,原本的计划是每个月至少写一篇原创的文章出来了,现在也是断了几个月;当然在过去的几个月时间里笔者也没闲着,主要学习了React框架的一些基础知识,不过现在学习的还不是很熟练,所以暂时也是出不了成体系的文章出来。反倒是一直在使用的vue.js(2.x)框架因为业务需求再加上看之前同事写的项目研究了一下全局手动挂载组件的开发(当然了,最近 vue 3.0 也是发布了 Beta 版本,也不知道以后这一块的写法是否会发生变化)。
1. 定义及相关实例
挂载,这个词汇我们在查看 vue 官方文档是总是会看到,但是概念上还是比较模糊的,是属于那种我们大概知道意思但是又不是很清晰的了解的概念。笔者在知乎上找到了一个比较靠谱回答,是关于 React 中挂载的概念:
我们把 React.js 将组件渲染,并且构造 DOM 元素然后塞入页面的过程称为组件的挂载。
而在HTML中文网上有关于 vue 中挂载的概念:
将组件渲染,并且构造 DOM 元素然后塞入页面的过程称为组件的挂载。
虽然个人觉得这个表述还不是很书面,但是已经比较易懂了。所谓的全局手动挂载组件,是区别于一般我们在 vue 中写的模板用于框架自动挂载展示页面,具体我们可以看 vue 的生命周期的经典图:
![](https://i-blog.csdnimg.cn/blog_migrate/18467059ac27551ffc367f0cb3518492.png)
如上,详见绿框部分,有两个黄色背景的判断逻辑:Has "el" option? , Has "template" option? 这两个意思也比较好懂,第一个意为在初始化一个 vue 实例的时候,传入的 option 参数是否含有 el 属性,如果有,则会直接进入下一步判断(此时说明 vue 中的模板内容将会被自动挂载到 el 属性所在的 div 上),而如果没有 el 属性,则 vue 中模板的内容将会在 vm.$mount(el) 函数被调用的时候挂载到参数 el 上。之后会判断是否有 render 函数,若有则会渲染 render 中的虚拟DOM,若没有 render 则判断是否有 template 选项,若有则挂载 template 中的模板,若没有则 Compile el's outerHTML as template(获取 el 及其子内容作为模板)。
举例:比如在我们每个 vue 项目都会存在的 @/main.js 入口文件中,就有相关实例。
import Vue from 'vue'
import App from './App'
import router from './router'
new Vue({
el: '#app',
router,
components: { App },
template: '<App/>'
})
如上,我们在项目中创建的 vue 实例(一般是脚手架生成),传入 el 选项,template 选项,那么最终 vue 会把 App 中的模板内容挂载到 id 为 app 的dom节点上,也就是一般我们项目中的 index.html 文件中:
<body>
<noscript>
<strong>Please enable JavaScript to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
而在没有传入 el 选项的时候呢,如下:
import Vue from 'vue'
import router from '@/router.js'
import App from './App'
import store from './store'
const app = new Vue({
router,
store,
render: h => h(App)
})
app.$mount("#app")
最终将会在调用 vm.$mount 方法的时候将 render 函数中的内容挂载到 app dom上,其作用和刚才第一种写法是一样的。
因此,手动挂载即是我们在 vue 项目开发时调用 $mount 函数进行组件的挂载过程,一般我们将这样的组件用于全局注册,因而也叫全局手动挂载。比如 element/iview/antD vue 等第三方组件库中的 Message , Spin, Loading 等公共组件都有手动挂载的设计。
这是 iview 官方文档在介绍 Message 组件的说明,在使用 JS 直接调用相关方法使用组件,它隐式的创建和维护了 vue 组件,也就是调用时手动挂载了 vue 组件。这么写的好处是不用提前在需要使用该组件的地方写 HTML 到<template>,我们可以很方便的在 JS 逻辑代码里调用,甚至在一些公共 JS文件中调用,比如在封装了 api 请求的js文件中调用。
2.需求背景
由于我们的项目在很多页面都需要判断用户是否登录,若用户未登录则弹出登录弹窗供用户登录,如果按一般的做法就需要在所有需要展示登录的页面加入登录弹窗组件:
显然这样会麻烦一些,如果我们能手动挂载该组件,在需要显示弹窗的地方直接用一句话搞定肯定更方便很多:this.$loginPopup(). 那么具体应该怎么处理呢?
3. 组件开发
首先我们已经写好了这个登录组件:login.vue,那么相当于这个组件的模板内容已经不需要重新修改了,只需要在该组件所在文件夹创建一个 index.js 文件,在该文件中将 login 组件用 Vue.install 方法暴露出来。
<!-- login.vue 文件的部分结构 -->
<template>
<div class="loginView">
<Modal
v-model="innerShowPopup"
:footer-hide="true"
:scrollable="false"
:closable="false"
:z-index="900"
>
<login-area :return-url="returnUrl"></login-area>
</Modal>
</div>
</template>
import Vue from 'vue'
import loginPopupComponent from './login.vue'
import i18n from '@/i18n/index.js'
const Login = {
install (Vue, options) {
Vue.prototype.$loginPopup = function (options) {
const LoginConstructor = Vue.extend(loginPopupComponent)
const div = document.createElement('div')
document.body.appendChild(div)
const vm = new LoginConstructor({
propsData: options,
i18n
}).$mount(div)
vm.innerShowPopup = true
// console.log('vm', vm)
// return vm.login()
}
}
}
export default Login
关于 Vue.install ,官方文档(https://cn.vuejs.org/v2/guide/plugins.html#%E5%BC%80%E5%8F%91%E6%8F%92%E4%BB%B6)有比较详细的解释:
Vue.js 的插件应该暴露一个
install
方法。这个方法的第一个参数是Vue
构造器,第二个参数是一个可选的选项对象。
说白了就是我们开发 vue 插件的时候得调用这个方法,这样组件才能被 Vue.use() 方法调用。关于 Vue.use 方法
安装 Vue.js 插件。如果插件是一个对象,必须提供
install
方法。如果插件是一个函数,它会被作为 install 方法。install 方法调用时,会将 Vue 作为参数传入。
在上述 index.js 代码中 ,loginPopupComponent 就是我们已经写好的 login.vue 组件,它需要被传入 Vue.extend 方法生成对应的构造函数,Vue.extend来自官方的解释及示例:
使用基础 Vue 构造器,创建一个“子类”。参数是一个包含组件选项的对象。
// 创建构造器
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')
在生成 LoginConstructor 的登录组件构造函数后,我们通过 new LoginConstructor() 的方式创建一个 vue 实例,其中的 propsData 参数就是我们可以传入组件的 props 对象合集。
这里需要特别注意的一个坑是 i18n 国际化的问题。这个问题可能在很大业务场景下遇不到,因为我们做的项目是需要支持多语言显示的,故在 login.vue 组件中写的 label 标签名都是从 i18n 中取的变量名,在使用手动挂载时如果没有显式将 i18n 配置传入构造函数中,那么最终在手动挂载函数时会报错,因为找不到变量对应的值。
最后是挂载点的问题,一般作为全局组件来说,我们可以利用 JS 的 createElement 函数生成一个空的 div 标签,将对应vue 实例挂载到该div标签上。之后需要将该组件在 main.js 文件中进行全局注册:
/**
* 全局注册一个登录弹窗组件,在之后需要调用登录弹窗时可直接调用 this.$loginPopup()
*/
Vue.use(Login)
这样在之后的 vue 页面中我们就可以随时调用登录弹窗组件了,这样会方便不少。
![](https://i-blog.csdnimg.cn/blog_migrate/c09ab6a8f2fdbb9fc98e6ecf08a265f3.png)
4. 总结
总的来说,Vue组件的手动挂载就是 vue 插件开发的一种方式,平时虽然这类组件比较少但是在特定场景和业务下是非常有用的,大部分是为了方便为项目添加一个全局类的功能同时又不想影响已有页面的很多结构。另外,手动挂载全局组件也更加方便相关组件的修改维护。
参考:
https://cn.vuejs.org/v2/guide/plugins.html#ad