首页 > 其他分享 >Vue 独立组件开发:不一样的组件通信方式

Vue 独立组件开发:不一样的组件通信方式

时间:2023-04-20 15:03:30浏览次数:47  
标签:vue name parent app 通信 Vue inject 组件

本文是介绍独立组件开发的系列文章的第二篇。

组件的通信

组件一般来说有以下几种关系:

  • 父子关系
  • 隔代关系
  • 兄弟关系

组件间经常会通信,Vue 内置的通信手段一般有两种:

  • ref:给元素或组件注册引用信息;
  • $parent / $children:访问父 / 子实例。

这两种方式都是直接得到组件的实例,然后直接调用组件的方法或访问数据,比如下面的示例中,用 ref 来访问组件):

组件

// component-a
export default {
  data () {
    return {
      title: 'Vue.js'
    }
  },
  methods: {
    sayHello () {
      window.alert('Hello');
    }
  }
}

调用方

<template>
  <component-a ref="comA"></component-a>
</template>
<script>
export default {
  mounted() {
    const comA = this.$refs.comA
    console.log(comA.title) // Vue.js
    comA.sayHello() // 弹窗
  }
}
</script>

$parent 和 $children 跟ref类似,也是基于当前上下文获取父组件或全部子组件的实例,然后调用组件实例上的方法。

这两种方法的弊端是,无法在跨级兄弟间通信,比如下面的结构:

// parent.vue 
<component-a></component-a> 
<component-b></component-b> 
<component-b></component-b>

component-a 组件中,想访问到两个 component-b 组件,那这种情况下,就得配置额外的插件或工具了,比如 VuexBus 的解决方案。

但是,我们开发的是独立组件,不应该依赖任何第三方库,如果你依赖了vuex,但是用户没有安装vuex,那就会直接报错。

因此,有没有不依赖任何三方库,就可以轻松得到任意的组件实例,或在任意组件间进行通信,且适用于任意场景呢?

provide / inject

基本使用

provide / inject 是 Vue.js 2.2.0 版本后新增的 API,在官网中的介绍:

这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在其上下游关系成立的时间里始终生效。如果你熟悉 React,这与 React 的上下文特性很相似。

并且文档中有如下提示:

provide 和 inject 主要为高阶插件/组件库提供用例。并不推荐直接用于应用程序代码中。

我们先来看一下这个 API 怎么用,假设有两个组件: A.vue 和 B.vue,B 是 A 的子组件。

// A.vue
export default {
  provide: {
    name: 'xiaoming'
  }
}

// B.vue
export default {
  inject: ['name'],
  mounted () {
    console.log(this.name);  // xiaoming
  }
}

在 A.vue 里,我们设置了一个 provide: name,值为 xiaoming,它的作用就是将 name 这个变量提供给它的所有子组件。而在 B.vue 中,通过 inject 注入了从 A 组件中提供的 name 变量,那么在组件 B 中,就可以直接通过 this.name 访问这个变量了,它的值也是 xiaoming

这就是 provide / inject API 最核心的用法。

需要注意的是:

provide 和 inject 绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的属性还是可响应的。

provide/inject有什么用呢?

使用场景

1. 替代 Vuex

在稍微大点的vue项目中,我们一般使用 Vuex 做状态管理,它是一个专为 Vuejs 开发的状态管理模式。 使用Vuex目的是跨组件通信、全局数据维护、多人协同开发。比如:用户的登录信息等全局的状态和数据。

当然,我们的目的并不是为了替代 Vuex,而是为了演示如何用provide/inject来实现vuex的功能。

一般在 webpack 中使用 Vuejs,都会有一个入口文件 main.js,里面通常导入了 VueVueRouterElementUI 等库,通常也会导入一个入口组件 app.vue 作为根组件。一个简单的 app.vue 可能只有以下代码:

<template>
  <div>
    <router-view></router-view>
  </div>
</template>
<script>
  export default {

  }
</script>

使用 provide / inject 替代 Vuex,就是在这个 app.vue 文件上做文章。

我们把 app.vue 理解为一个最外层的根组件,用来存储所有需要的全局数据和状态,甚至是计算属性(computed)、方法(methods)等。因为你的项目中所有的组件(包含路由),它的父组件(或根组件)都是 app.vue,所以我们把整个 app.vue 实例通过 provide 对外提供

<template>
  <div>
    <router-view></router-view>
  </div>
</template>
<script>
export default {
  provide() {
    return {
      app: this
    }
  }
}
</script>

上面,我们把整个 app.vue 的实例 this 对外提供,命名为 app。接下来任何组件(包括路由),只要通过 inject 注入 app.vueapp,都可以直接通过 this.app.xxx 来访问 app.vue 的 datacomputedmethods 等内容。

app.vue 是整个项目第一个被渲染的组件,而且只会渲染一次,即使切换路由,·app.vue· 也不会被再次渲染,利用这个特性,很适合做一次性全局的状态数据管理,例如,我们将用户的登录信息保存起来:

<script>
export default {
  provide() {
    return {
      app: this
    }
  },
  data() {
    return {
      userInfo: null
    }
  },
  methods: {
    getUserInfo() {
      // 这里通过 ajax 获取用户信息后,赋值给 this.userInfo,以下为伪代码
      $.ajax('/user/info', data => {
        this.userInfo = data
      })
    }
  },
  mounted() {
    this.getUserInfo()
  }
}
</script>

任何页面或组件,只要通过 inject 注入 app 后,就可以直接访问 userInfo 的数据了,比如:

<template>
  <div>
    {{ app.userInfo }}
  </div>
</template>
<script>
export default {
  inject: ['app']
}
</script>

除了直接使用数据,还可以调用方法。比如在某个页面里,修改了个人资料,这时一开始在 app.vue 里获取的 userInfo 已经不是最新的了,需要重新获取。可以这样使用:

<template>
  <div>
    {{ app.userInfo }}
  </div>
</template>
<script>
export default {
  inject: ['app'],
  methods: {
    changeUserInfo() {
      // 这里修改完用户数据后,通知 app.vue 更新,以下为伪代码
      $.ajax('/user/update', () => {
        // 直接通过 this.app 就可以调用 app.vue 里的方法
        this.app.getUserInfo()
      })
    }
  }
}
</script>

只要理解了 this.app 是直接获取整个 app.vue 的实例后,使用起来就得心应手了。如果开发一个不是很复杂的项目,我们完全可以使用 provide / inject 来实现全局数据的管理。而不需要配置复杂的Vuex

2. 独立组件使用 provide / inject

独立组件使用provide / inject,主要是具有联动关系的组件。比如要开发一个具有数据校验功能的表单组件 Form,它其实是两个组件,一个是 Form,一个是 FormItemFormItemForm 的子组件,它需要获取 Form 组件上的一些特性(props),所以就需要得到父组件 Form实例。

这里的难点是FormFormItem 不一定是父子关系,中间很可能间隔了其它组件,所以不能单纯使用 $parent 来向上获取实例。

再没有provide / inject这对api之前,一种比较可行的方案是用计算属性动态获取:

computed: {
  form () {
    let parent = this.$parent;
    while (parent.$options.name !== 'Form') {
      parent = parent.$parent;
    }
    return parent;
  }
}

每个组件都可以设置 name 选项,作为组件名的标识,利用这个特点,通过向上遍历,直到找到需要的组件。

这个方法虽然可行,但是相比一个 provide/inject 来说太不优雅了。如果用 inject,可能只需要一行代码:

export default { 
    inject: ['form'] 
}

派发与广播——自行实现 dispatchbroadcast

provide / inject API 主要解决了跨级组件间的通信问题,不过它的使用场景,主要是子组件获取上级组件的状态,跨级组件间建立了一种主动提供与依赖注入的关系。然后有两种场景它不能很好的解决:

  • 父组件向子组件(支持跨级)传递数据;
  • 子组件向父组件(支持跨级)传递数据;

这种父子(含跨级)传递数据的通信方式,Vue并没有提供原生的 API 来支持,而是推荐使用大型数据状态管理工具 Vuex,但是我们开发独立组件是无法使用的,那有没有另外的方式呢?

$on$emit

$emit 会在当前组件实例上触发自定义事件,并传递一些参数给监听器的回调,一般来说,都是在父级调用这个组件时,使用 @on 的方式来监听自定义事件的,比如在子组件中触发事件:

// child.vue,部分代码省略
export default {
  methods: {
    handleEmitEvent () {
      this.$emit('test', 'Hello Vue.js');
    }
  }
}

在父组件中监听由 child.vue 触发的自定义事件 test

<!-- parent.vue,部分代码省略-->
<template>
  <child-component @test="handleEvent">
</template>
<script>
  export default {
    methods: {
      handleEvent (text) {
      	console.log(text);  // Hello Vue.js
      }
    }
  }
</script>

这里看似是在父组件 parent.vue 中绑定的自定义事件 test 的处理句柄,然而事件 test 并不是在父组件上触发的,而是在子组件 child.vue 里触发的,只是通过 v-on 在父组件中监听。

既然是子组件自己触发的,那它自己也可以监听到,这就要使用 $on 来监听实例上的事件。也就是,组件在自己内部使用 $emit触发事件,并在内部用 $on 监听它。

<template>
  <div>
    <button @click="handleEmitEvent">触发自定义事件</button>
  </div>
</template>
<script>
export default {
  methods: {
    handleEmitEvent() {
      // 在当前组件上触发自定义事件 test,并传值
      this.$emit('test', 'Hello Vue.js')
    }
  },
  mounted() {
    // 监听自定义事件 test
    this.$on('test', text => {
      window.alert(text)
    })
  }
}
</script>

$on 监听了自己触发的自定义事件 test,因为有时不确定何时会触发事件,一般会在 mounted 或 created 钩子中来监听。

从上面的示例中,这绝对是多此一举的,因为大可在 handleEmitEvent 里直接写 window.alert(text),没必要绕一圈。

之所以显得多此一举,是因为 handleEmitEvent 是当前组件内的 <button> 调用的,如果这个方法不是它自己调用,而是其它组件调用的,那这个用法就大有可为了

$on 和 $emitVue3中弃用了,但是官方推荐了一个库:mitt,可以达到同样的效果。

实现 dispatch 和 broadcast 方法

尽管我们可以通过新建一个vue实例,并通过vue实例上的$on$emit来实现数据的传递,类似于eventBus

但是在独立组件开发中是不推荐这样做的,因为它又引入了一个新的vue实例来作为数据中转,我们想要的是直接操作组件实例来达到数据通信的效果。

Vue 1 中,提供了两个方法:$dispatch 和 $broadcast ,前者用于向上级派发事件,只要是它的父级(一级或多级以上),都可以在组件内通过 $on监听到。后者相反,是由上级向下级广播事件的。

这两个方法虽然看起来很好用,但是在 Vue 2 中都废弃了,官方给出的解释是:

因为基于组件树结构的事件流方式有时让人难以理解,并且在组件结构扩展的过程中会变得越来越脆弱。

虽然在业务开发中,它没有 Vuex 这样专门管理状态的插件清晰好用,但对独立组件(库)的开发,绝对是福音。因为独立组件一般层级并不会很复杂,并且剥离了业务,不会变的难以维护。

我们要实现的 dispatchbroadcast 方法,将具有以下功能:

  • 在子组件调用 dispatch 方法,向上级指定的组件实例上触发自定义事件,并传递数据,且该上级组件已预先通过 $on 监听了这个事件;
  • 相反,在父组件调用 broadcast 方法,向下级指定的组件实例上触发自定义事件,并传递数据,且该下级组件已预先通过 $on 监听了这个事件。

实现这对方法的关键点在于,如何正确地向上或向下找到对应的组件实例,并在它上面触发方法

在寻找组件实例上,我们的惯用伎俩就是通过遍历来匹配组件的 name 选项,在独立组件里,每个组件的 name 值应当是唯一的。

function broadcast(componentName, eventName, params) {
  this.$children.forEach(child => {
    const name = child.$options.name;

    if (name === componentName) {
      child.$emit.apply(child, [eventName].concat(params));
    } else {
      broadcast.apply(child, [componentName, eventName].concat([params]));
    }
  });
}
export default {
  methods: {
    dispatch(componentName, eventName, params) {
      let parent = this.$parent || this.$root;
      let name = parent.$options.name;

      while (parent && (!name || name !== componentName)) {
        parent = parent.$parent;

        if (parent) {
          name = parent.$options.name;
        }
      }
      if (parent) {
        parent.$emit.apply(parent, [eventName].concat(params));
      }
    },
    broadcast(componentName, eventName, params) {
      broadcast.call(this, componentName, eventName, params);
    }
  }
};

该方法可能在很多组件中都会使用,复用起见,我们封装在混合mixins里。那它就这么使用:

import Emitter from '../mixins/emitter.js'

export default {
  mixins: [ Emitter ],
  methods: {
    handleDispatch () {
      this.dispatch();
    },
    handleBroadcast () {
      this.broadcast();
    }
  }
}

这两个方法都接收了三个参数,第一个是组件的 name 值,用于向上或向下递归遍历来寻找对应的组件,第二个和第三个就是上文分析的自定义事件名称和要传递的数据。

现在有 A.vue 和 B.vue 两个组件,其中 BA 的子组件,中间可能跨多级,在 A 中向 B 通信:

<!-- A.vue -->
<template>
  <button @click="handleClick">触发事件</button>
</template>
<script>
import Emitter from '../mixins/emitter.js'

export default {
  name: 'componentA',
  mixins: [Emitter],
  methods: {
    handleClick() {
      this.broadcast('componentB', 'on-message', 'Hello Vue.js')
    }
  }
}
</script>
// B.vue
export default {
  name: 'componentB',
  created () {
    this.$on('on-message', this.showMessage);
  },
  methods: {
    showMessage (text) {
      window.alert(text);
    }
  }
}

找到任意组件实例——findComponents 系列方法

上面已经介绍了两种组件间通信的方法:provide / injectdispatch / broadcast

它们都有各自的使用场景和局限,比如前者多用于子组件获取父组件的状态,后者常用于父子组件间通过自定义事件通信。

现在介绍第三种通信方式:findComponents 系列方法,它并非 Vue 内置,而是需要自行实现,以工具函数的形式来使用,它是一系列的函数,可以说是组件通信的终极方案。

findComponents 系列方法最终都是返回组件的实例,进而可以读取或调用该组件的数据和方法。

它适用于以下场景:

  • 由一个组件,向上找到最近的指定组件;
  • 由一个组件,向上找到所有的指定组件;
  • 由一个组件,向下找到最近的指定组件;
  • 由一个组件,向下找到所有指定的组件;
  • 由一个组件,找到指定组件的兄弟组件。

5 个不同的场景,对应 5 个不同的函数,实现原理也大同小异。都是通过递归、遍历,找到指定组件的 name 选项匹配的组件实例并返回。

向上找到最近的指定组件——findComponentUpward

function findComponentUpward (context, componentName) {
  let parent = context.$parent;
  let name = parent.$options.name;

  while (parent && (!name || [componentName].indexOf(name) < 0)) {
    parent = parent.$parent;
    if (parent) name = parent.$options.name;
  }
  return parent;
}
export { findComponentUpward };

findComponentUpward 接收两个参数,第一个是当前上下文,比如你要基于哪个组件来向上寻找,一般都是基于当前的组件,也就是传入 this;第二个参数是要找的组件的 name

比如,有组件 A 和组件 BAB 的父组件,在 B 中获取和调用 A 中的数据和方法:

<!-- component-a.vue -->
<template>
  <div>
    组件 A
    <component-b></component-b>
  </div>
</template>
<script>
import componentB from './component-b.vue'

export default {
  name: 'componentA',
  components: { componentB },
  data() {
    return {
      name: 'xiaoming'
    }
  },
  methods: {
    sayHello() {
      console.log('Hello, Vue.js')
    }
  }
}
</script>
<!-- component-b.vue -->
<template>
  <div>组件 B</div>
</template>
<script>
import { findComponentUpward } from '../utils/index'

export default {
  name: 'componentB',
  mounted() {
    const comA = findComponentUpward(this, 'componentA')

    if (comA) {
      console.log(comA.name) // xiaoming
      comA.sayHello() // Hello, Vue.js
    }
  }
}
</script>

使用起来很简单,只要在需要的地方调用 findComponentUpward 方法就行,第一个参数一般都是传入 this,即当前组件的上下文(实例)。

向下找到最近的指定组件——findComponentDownward

function findComponentDownward (context, componentName) {
  const childrens = context.$children;
  let children = null;

  if (childrens.length) {
    for (const child of childrens) {
      const name = child.$options.name;

      if (name === componentName) {
        children = child;
        break;
      } else {
        children = findComponentDownward(child, componentName);
        if (children) break;
      }
    }
  }
  return children;
}
export { findComponentDownward };

context.$children 得到的是当前组件的全部子组件,所以需要遍历一遍,找到有没有匹配到的组件 name,如果没找到,继续递归找每个 $children 的 $children,直到找到最近的一个为止。

找到指定组件的兄弟组件——findBrothersComponents

function findBrothersComponents (context, componentName, exceptMe = true) {
  let res = context.$parent.$children.filter(item => {
    return item.$options.name === componentName;
  });
  let index = res.findIndex(item => item._uid === context._uid);
  if (exceptMe) res.splice(index, 1);
  return res;
}
export { findBrothersComponents };

findBrothersComponents 多了一个参数 exceptMe,是否把本身除外,默认是 true。

寻找兄弟组件的方法,是先获取 context.$parent.$children,也就是父组件的全部子组件,这里面当前包含了本身,所有也会有第三个参数 exceptMe

Vue 在渲染组件时,都会给每个组件加一个内置的属性 _uid,这个 _uid 是不会重复的,借此可以从一系列兄弟组件中把自己排除掉。

其他函数就不做介绍,只有你开发过 Vue 独立组件,才会明白这 5 个函数的强大之处。

总结

本文介绍了在开发独立组件时常用的三种通信方式:

1、provide / inject:通常是子组件用来获取父组件的状态数据。所有的子组件,不管跨多少层级,都能获取到父组件的数据。但是这种方式子组件是被动的,它不能主动的把数据传递给父组件;

2、dispatch / broadcast:这种传递方式可以让父子组件都能主动的传递数据,解决了provide / inject比较被动的局限, 这样就数据的传递就更加灵活;

3、findComponents:这是一种终结方式,其实所有通信方式都是找到对应的组件实例,然后调用组件的某个方法实现传参。即使是vue内置的ref,它也是组件实例的引用。

标签:vue,name,parent,app,通信,Vue,inject,组件
From: https://blog.51cto.com/u_16078169/6209575

相关文章

  • 无界微前端(wujie):element-ui 弹框内使用select组件,弹出框位置异常解决方案 (主程序加载
    https://wujie-micro.github.io/doc/guide/ element-ui弹框内使用select组件,弹出框位置异常解决方案第一步:在子应用中: 以上3步就好啦!!!是不是很简单这个框架坑很多,希望对大家有帮助!!! ......
  • Vue3+TS+Node打造个人博客(后端架构)
    在使用Express搭建后端服务时,主要关注的几个点是:路由中间件和控制器SQL处理响应返回体数据结构错误码Web安全环境变量/配置路由和控制器路由基本上是按模块或功能去划分的。首先是按模块去划分一级路由,各个模块的子功能相当于是用二级路由处理。简单举个例子,/article......
  • vue全家桶进阶之路43:Vue3 Element Plus el-form表单组件
    在ElementPlus中,el-form是一个表单组件,用于创建表单以便用户填写和提交数据。它提供了许多内置的验证规则和验证方法,使表单验证更加容易。使用el-form组件,您可以将表单控件组织在一起,并对表单进行验证,以确保提交的数据符合预期的格式和要求。该组件具有以下特性:支持内置......
  • Android UI组件
    1.TextView知识点:autoLink:文本自动识别为web超链接、email地址等——第五章AcitonBar用法:自定义ActionBar——第四章创建Acticity选项菜单:让返回键在ActionBar中显示后,重写onOptionsItemSelected——第四章创建,注册监听器,实现按钮功能——第四章布局:<?xmlversion="1.0......
  • C++ - UDP通信
    UDPUDP就比较简单了,步骤比tcp要少一些。连接过程图:  1).服务器1.初始化套接字库WORDwVersion;WSADATAwsaData;interr;​wVersion=MAKEWORD(1,1);2.创建套接字SOCKETsockSrv=socket(AF_INET,SOCK_DGRAM,0);3.绑定//SOCKADDR_INaddrSrv;省略了定......
  • C++ - TCP通信
    前言socket编程分为TCP和UDP两个模块,其中TCP是可靠的、安全的,常用于发送文件等,而UDP是不可靠的、不安全的,常用作视频通话等。如下图:头文件与库:#include<WinSock2.h>​#pragmacomment(lib,"ws2_32.lib")准备工作:创建工程后,首先右键工程,选择属性然后选择C/C++-预......
  • vue3微信公众号商城项目实战系列(12)项目发布到服务器上
    本篇介绍如何将vue3项目打包发布到服务器上,然后在微信公众号上打开。vue3发布之前需要对项目进行编译,编译时会在项目根目录下创建dist文件夹,编译后的文件会存放在这里。 在编译之前,我们在public目录下建一个config.js的文件,里面放如下的代码:constconfig={baseUr......
  • Vue3 toRef与toRefs
    视频直接用ref是创建新的对象10.toRef作用:创建一个ref对象,其value值指向另一个对象中的某个属性。语法:constname=toRef(person,'name')应用:要将响应式对象中的某个属性单独提供给外部使用时。扩展:toRefs与toRef功能一致,但可以批量创建多个ref对象,语法......
  • 如何在html页面引入Element组件
    相关步骤1、引入相关链接<linkrel="stylesheet"href="https://unpkg.com/[email protected]/lib/theme-chalk/index.css"><scriptsrc="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script><scriptsrc="https://un......
  • Vue3 自定义hook
    视频9.自定义hook函数什么是hook?——本质是一个函数,把setup函数中使用的CompositionAPI进行了封装。类似于vue2.x中的mixin。自定义hook的优势:复用代码,让setup中的逻辑更清楚易懂。componentsDemo.vue<template> <h2>当前求和为:{{sum}}</h2> <button@clic......