百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术分类 > 正文

浅析vue封装自定义插件(vue自己封装组件 vue项目中)

ztj100 2024-10-27 18:36 17 浏览 0 评论

前言

在使用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,响应式的声明属性的getset。当对属性进行读写时,会调用对应的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>

相关推荐

如何将数据仓库迁移到阿里云 AnalyticDB for PostgreSQL

阿里云AnalyticDBforPostgreSQL(以下简称ADBPG,即原HybridDBforPostgreSQL)为基于PostgreSQL内核的MPP架构的实时数据仓库服务,可以...

Python数据分析:探索性分析

写在前面如果你忘记了前面的文章,可以看看加深印象:Python数据处理...

CSP-J/S冲奖第21天:插入排序

...

C++基础语法梳理:算法丨十大排序算法(二)

本期是C++基础语法分享的第十六节,今天给大家来梳理一下十大排序算法后五个!归并排序...

C 语言的标准库有哪些

C语言的标准库并不是一个单一的实体,而是由一系列头文件(headerfiles)组成的集合。每个头文件声明了一组相关的函数、宏、类型和常量。程序员通过在代码中使用#include<...

[深度学习] ncnn安装和调用基础教程

1介绍ncnn是腾讯开发的一个为手机端极致优化的高性能神经网络前向计算框架,无第三方依赖,跨平台,但是通常都需要protobuf和opencv。ncnn目前已在腾讯多款应用中使用,如QQ,Qzon...

用rust实现经典的冒泡排序和快速排序

1.假设待排序数组如下letmutarr=[5,3,8,4,2,7,1];...

ncnn+PPYOLOv2首次结合!全网最详细代码解读来了

编辑:好困LRS【新智元导读】今天给大家安利一个宝藏仓库miemiedetection,该仓库集合了PPYOLO、PPYOLOv2、PPYOLOE三个算法pytorch实现三合一,其中的PPYOL...

C++特性使用建议

1.引用参数使用引用替代指针且所有不变的引用参数必须加上const。在C语言中,如果函数需要修改变量的值,参数必须为指针,如...

Qt4/5升级到Qt6吐血经验总结V202308

00:直观总结增加了很多轮子,同时原有模块拆分的也更细致,估计为了方便拓展个管理。把一些过度封装的东西移除了(比如同样的功能有多个函数),保证了只有一个函数执行该功能。把一些Qt5中兼容Qt4的方法废...

到底什么是C++11新特性,请看下文

C++11是一个比较大的更新,引入了很多新特性,以下是对这些特性的详细解释,帮助您快速理解C++11的内容1.自动类型推导(auto和decltype)...

掌握C++11这些特性,代码简洁性、安全性和性能轻松跃升!

C++11(又称C++0x)是C++编程语言的一次重大更新,引入了许多新特性,显著提升了代码简洁性、安全性和性能。以下是主要特性的分类介绍及示例:一、核心语言特性1.自动类型推导(auto)编译器自...

经典算法——凸包算法

凸包算法(ConvexHull)一、概念与问题描述凸包是指在平面上给定一组点,找到包含这些点的最小面积或最小周长的凸多边形。这个多边形没有任何内凹部分,即从一个多边形内的任意一点画一条线到多边形边界...

一起学习c++11——c++11中的新增的容器

c++11新增的容器1:array当时的初衷是希望提供一个在栈上分配的,定长数组,而且可以使用stl中的模板算法。array的用法如下:#include<string>#includ...

C++ 编程中的一些最佳实践

1.遵循代码简洁原则尽量避免冗余代码,通过模块化设计、清晰的命名和良好的结构,让代码更易于阅读和维护...

取消回复欢迎 发表评论: