在使用vue的过程中,经常会用到Vue.use,但是大部分对它一知半解,不了解在调用的时候具体做了什么,因此,本文简要概述下在vue中,如何封装自定义插件。

  在开始之前,先补充一句,其实利用vue封装自定义插件的本质就是组件实例化的过程或者指令等公共属性方法的定义过程,比较大的区别在于封装插件需要手动干预,就是一些实例化方法需要手动调用,而Vue的实例化,很多逻辑内部已经帮忙处理掉了。插件相对于组件的优势就是插件封装好了之后,可以开箱即用,而组件是依赖于项目的。对组件初始化过程不是很熟悉的可以参考这篇博文

    我们从vue源码中,可以看到Vue.use的方法定义如下: 

Vue.use = function (plugin: Function | Object) {
const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
// 已经存在插件,则直接返回插件对象
if (installedPlugins.indexOf(plugin) > -1) {
return this
} // additional parameters
const args = toArray(arguments, 1)
args.unshift(this)
// vue插件形式可以是对象,也可以是方法,默认会传递一个Vue的构造方法作为参数
if (typeof plugin.install === 'function') {
plugin.install.apply(plugin, args)
} else if (typeof plugin === 'function') {
plugin.apply(null, args)
}
installedPlugins.push(plugin)
return this
}

  从上述代码中,我们可以看出,Vue.use代码比较简洁,处理逻辑不是很多。我们常用的Vue.use(xxx),xxx可以是方法,也可以是对象。在Vue.use中,通过apply调用插件方法,传入一个参数,Vue的构造方法。举个栗子,最简单的Vue插件封装如下:

// 方法
function vuePlugins (Vue) {
Vue.directive('test', {
bind (el) {
el.addEventListener('click', function (e) {
alert('hello world')
})
}
})
} // 对象
const vuePlugins = {
install (Vue) {
Vue.directive('test', {
bind (el) {
el.addEventListener('click', function (e) {
alert('hello world')
})
}
})
}
}

  以上两种封装方法都可以,说白了,就是将全局注册的指令封装到一个方法中,在Vue.use时调用。这个比较显然易懂。现在举一个稍微复杂点的例子,tooltip在前端开发中经常会用到,直接通过方法能够调用显示,防止不必要的组件注册引入,如果我们单独封装一个tooltip组件,应该如何封装呢?这种封装方式需要了解组件的初始化过程。区别在于将组件封装成插件时,不能通过template将组件实例化挂载到真实DOM中,这一步需要手动去调用对应组件实例化生命周期中的方法。具体实现代码如下:  

// component
let toast = {
props: {
show: {
type: Boolean,
default: false
},
msg: {
type: String
}
},
template: '<div v-show="show" class="toast">{{msg}}</div>'
}

  组件初始化过程:

// JavaScript初始化逻辑
// 获取toast构造实例
const TempConstructor = Vue.extend(toast)
// 实例化toast
let instance = new TempConstructor()
// 手动创建toast的挂载容器
let div = document.createElement('div')
// 解析挂载toast
instance.$mount(div)
// 将toast挂载到body中
document.body.append(instance.$el)
// 将toast的调用封装成一个方法,挂载到Vue的原型上
Vue.prototype.$toast = function (msg) {
instance.show = true
instance.msg = msg
setTimeout(() => {
instance.show = false
}, 5000)
}

  组件的定义,和普通的组件声明一致。组件的插件化过程,和普通的组件实例化一致,区别在于插件化时组件部分初始化方法需要手动调用。比如:

    1、Vue.extend作用是组装组件的构造方法VueComponent

    2、new TempConstructor()是实例化组件实例。实例化构造方法,只是对组件的状态数据进行了初始化,并没有解析组件的template,也没有后续的生成vnode和解析vnode

    3、instance.$mount(div)的作用是解析模板文件,生成render函数,进而调用createElement生成vnode,最后生成真实DOM,将生成的真实DOM挂载到实例instance的$el属性上,也就是说,实例instance.$el即为组件实例化最终的结果。

    4、组件中,props属性最终会声明在组件实例上,所以直接通过实例的属性,也可以响应式的更改属性的传参。组件的属性初始化方法如下:

function initProps (Comp) {
const props = Comp.options.props
for (const key in props) {
proxy(Comp.prototype, `_props`, key)
}
}
// 属性代理,从一个原对象中拿数据
export function proxy (target: Object, sourceKey: string, key: string) {
// 设置对象属性的get/set,将data中的数据代理到组件对象vm上
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key]
}
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}

  从上述可以看出,最终会在构造方法中,给所有的属性声明一个变量,本质上是读取_props中的内容,_props中的属性,会在实例化组件,initState中的InitProps中进行响应式的声明,具体代码如下:

function initProps (vm: Component, propsOptions: Object) {
const propsData = vm.$options.propsData || {}
const props = vm._props = {}
// cache prop keys so that future props updates can iterate using Array
// instead of dynamic object key enumeration.
const keys = vm.$options._propKeys = []
const isRoot = !vm.$parent
// root instance props should be converted
if (!isRoot) {
toggleObserving(false)
}
for (const key in propsOptions) {
keys.push(key)
const value = validateProp(key, propsOptions, propsData, vm)
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
const hyphenatedKey = hyphenate(key)
if (isReservedAttribute(hyphenatedKey) ||
config.isReservedAttr(hyphenatedKey)) {
warn(
`"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`,
vm
)
}
defineReactive(props, key, value, () => {
if (!isRoot && !isUpdatingChildComponent) {
warn(
`Avoid mutating a prop directly since the value will be ` +
`overwritten whenever the parent component re-renders. ` +
`Instead, use a data or computed property based on the prop's ` +
`value. Prop being mutated: "${key}"`,
vm
)
}
})
} else {
defineReactive(props, key, value)
}
// static props are already proxied on the component's prototype
// during Vue.extend(). We only need to proxy props defined at
// instantiation here.
if (!(key in vm)) {
proxy(vm, `_props`, key)
}
}
toggleObserving(true)
}

  这里会遍历所有订单props,响应式的声明属性的get和set。当对属性进行读写时,会调用对应的get/set,进而会触发视图的更新,vue的响应式原理在后面的篇章中会进行介绍。这样,我们可以通过方法参数的传递,来动态的去修改组件的props,进而能够将组件插件化。

  有些人可能会有疑问,到最后挂载到body上的元素是通过document.createElement('div')创建的div,还是模板的template解析后的结果。其实,最终挂载只是组件解析后的结果。在调用__patch__的过程中,执行流程是,首先,记录老旧的节点,也就是$mount(div)中的div;然后,根据模板解析后的render生成的vnode的节点,去创建DOM节点,创建后的DOM节点会放到instance.$el中;最后一步,会将老旧节点给移除掉。所以,在我们封装一个插件的过程中,实际上手动创建的元素只是一个中间变量,并不会保留在最后。可能大家还会注意到,插件实例化完成后的DOM挂载也是我们手动挂载的,执行的代码是document.body.append(instance.$el)。

  附:test.html 测试代码

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<style>
.toast{
position: absolute;
left: 45%;
top: 10%;
width: 10%;
height: 5%;
background: #ccc;
border-radius: 5px;
}
</style>
<title>Hello World</title>
<script src="https://cdn.bootcss.com/vue/2.6.10/vue.js"></script>
</head>
<body>
<div id='app' v-test>
<button @click="handleClick">我是按钮</button>
</div>
<script>
function vuePlugins (Vue) {
Vue.directive('test', {
bind (el) {
el.addEventListener('click', function (e) {
alert('hello world')
})
}
})
}
// const vuePlugins = {
// install (Vue) {
// Vue.directive('test', {
// bind (el) {
// el.addEventListener('click', function (e) {
// alert('hello world')
// })
// }
// })
// }
// }
Vue.use(vuePlugins)
let toast = {
props: {
show: {
type: Boolean,
default: false
},
msg: {
type: String
}
},
template: '<div v-show="show" class="toast">{{msg}}</div>'
}
// 获取toast构造实例
const TempConstructor = Vue.extend(toast)
// 实例化toast
let instance = new TempConstructor()
// 手动创建toast的挂载容器
let div = document.createElement('div')
// 解析挂载toast
instance.$mount(div)
// 将toast挂载到body中
document.body.append(instance.$el)
// 将toast的调用封装成一个方法,挂载到Vue的原型上
Vue.prototype.$toast = function (msg) {
instance.show = true
instance.msg = msg
setTimeout(() => {
instance.show = false
}, 5000)
}
var vm = new Vue({
el: '#app',
data: {
msg: 'Hello World',
a: 11
},
methods: {
test () {
console.log('这是一个主方法')
},
handleClick () {
this.$toast('hello world')
}
},
created() {
console.log('执行了主组件上的created方法')
},
})
</script>
</body>
</html>

浅析vue封装自定义插件的更多相关文章

  1. webpack + vue 项目 自定义 插件 解决 前端 JS 版本 更新 问题

    Webpack 是一个前端资源加载/打包工具.它将根据模块的依赖关系进行静态分析,然后将这些模块按照指定的规则生成对应的静态资源. 它的异步加载原理是,事先将编译好后的静态文件,通过js对象映射,硬编 ...

  2. vue之自定义插件

    1.插件的作用 插件通常会为 Vue 添加全局功能,一般是添加全局方法/全局指令/过滤器等 Vue 插件有一个公开方法 install ,通过 install 方法给 Vue 添加全局功能 通过全局方 ...

  3. vue封装第三方插件并发布到npm

    前言 写此文前特意google了一下,因为有较详细的开发教程我再写意义不大,有把插件封装成组件的教程,有把自己的组件封住成插件的教程,本文主要说明如何把第三方的插件封装成vue插件,简化配置,一键安装 ...

  4. Vue学习之--------Vue中自定义插件(2022/8/1)

    文章目录 1.插件的基本介绍 2.实际应用 2.1 目录结构 2.2 代码实例 2.2.1 学校组件(School.vue) 2.2.2 学生组件(Student.vue) 2.2.3 定义的插件 2 ...

  5. vue 封装自定义组件

    组件结构 sjld >index.js >sjid.vue 最好单独放一个文件夹,有依赖的话装依赖 Sjld.vue 内容 <template id="sjld" ...

  6. jquery自定义插件结合baiduTemplate.js实现异步刷新(附源码)

    上一篇记录了BaiduTemplate模板引擎使用示例附源码,在此基础上对使用方法进行了封装 自定义插件jajaxrefresh.js 代码如下: //闭包限定命名空间 (function ($) { ...

  7. vue自定义插件封装,实现简易的elementUi的Message和MessageBox

    vue自定义插件封装示例 1.实现message插件封装(类似简易版的elementUi的message) message组件 <template>     <transition  ...

  8. 浅析MyBatis(三):聊一聊MyBatis的实用插件与自定义插件

    在前面的文章中,笔者详细介绍了 MyBatis 框架的底层框架与运行流程,并且在理解运行流程的基础上手写了一个自己的 MyBatis 框架.看完前两篇文章后,相信读者对 MyBatis 的偏底层原理和 ...

  9. 【vue3】封装自定义全局插件

    [vue3]封装自定义全局插件 原vue2方法 main.js import Vue from 'vue' import App from './App.vue' import router from ...

随机推荐

  1. MySQL_连表查询

    连表查询 连表查询通常分为内连接和外连接.内连接就是使用INNER JOIN进行连表查询:而外连接又分为三种连接方式,分别是左连接(LEFT JOIN).右连接(RIGHT JOIN).全连接(FUL ...

  2. iptables 伪装(Masquerading)

    「偽装」是一种特殊的SNAT操作:将来自其它电脑的包的来源位址改成自己的位址:请注意,由於入替的来源位址是自动決定的(执行SNAT的主机的IP位址).所以,如果它改变了,仍在持续中的旧连線将会失效.「 ...

  3. 2019-11-13-如何在国内发布-UWP-应用

    title author date CreateTime categories 如何在国内发布 UWP 应用 lindexi 2019-11-13 08:46:44 +0800 2019-02-17 ...

  4. ccf-201403-3有趣的命令行

    傻逼题,要是考试只能得0分.. 提供几组傻逼数据,这要是在真实的生活中一定是错的... 还是要好好读题吧,全凭自己的感觉就得0分 albw:x 4 ls -a docu Case 1: -a ls - ...

  5. UVa11400 - Lighting System Design——[动态规划]

    题干略. 题意分析: 很容易理解一类灯泡要么全部换要么全不换,其实费用节省的主要原因是由于替换灯泡类型而排除了低压电压源,于是我们就可以推断出灯泡类型替换的原则: 对于两类灯泡a1和a2,a1可以被a ...

  6. springboot2多数据源完整示例

    springboot2 + mybatis + mysql + oracle + sqlserver多数据源的配置 相信很多朋友在开发的时候,可能会碰到需要一个项目,配置多个数据源的需求,可能是同一种 ...

  7. Node.js 安装及环境配置 以及google浏览器安装插件并使用

    一.安装环境 1.本机系统:Windows 10 企业版(64位)2.Node.js:node-v10.16.0-x64.msi(64位) 二.安装Node.js步骤 1.下载对应自己系统对应的 No ...

  8. ImportError: DLL load failed: 找不到指定的模块。 TensorFlow 1.13

    版权声明:本文为博主原创文章,未经博主允许不得转载. https://blog.csdn.net/zhenlingcn/article/details/88647288问题描述 问题环境TensorF ...

  9. Python--day40--datetime,socketserver,Threading模块分别是哪些模块的高层模块

  10. UVA 11922 Permutation Transformer —— splay伸展树

    题意:根据m条指令改变排列1 2 3 4 … n ,每条指令(a, b)表示取出第a~b个元素,反转后添加到排列尾部 分析:用一个可分裂合并的序列来表示整个序列,截取一段可以用两次分裂一次合并实现,粘 ...