Vue 中的组件通信技术详解

1. 组件通信介绍

VUE.js 是基于组件化思想搭建的前端框架,但组件之间的通信是一件棘手的事。因为组件通信的方法不应该影响其它组件甚至对全局变量的操作。让组件在 VUE.js 的上下结构中能够更好地进行通信,有各种机制可供选择。

1.1 父子组件通信

由于VUE.js是组件化构建页面的,组件之间的父子关系是最普遍的,父控件通过props向子控件传值或通过方法调用实现与子控件通信。

下面是一个父组件向子组件传值的例子:

// 父组件

<template>

<div>

<Child :message="msg"/>

</div>

</template>

<script>

import Child from './Child.vue'

export default {

components: {

Child

},

data () {

return {

msg: '父组件的消息'

}

}

}

</script>

// 子组件

<template>

<div>

{{message}}

</div>

</template>

<script>

export default {

props: {

message: String

}

}

</script>

props是通过VUE.js的props接收和传递数据的,props最好使用短横线来命名,使用props传递数据时,子组件无法更改父组件中的值。如果需要改变父组件中的值,可以通过$emit()方法传递自定义事件。

下面是一个父组件与子组件相互调用的例子:

// 父组件

<template>

<div>

<Child ref="child"/>

<button @click="callChildMethod">调用子组件方法</button>

</div>

</template>

<script>

import Child from './Child.vue'

export default {

components: {

Child

},

methods: {

callChildMethod () {

this.$refs.child.childMethod()

}

}

}

</script>

// 子组件

<template>

<div>

</div>

</template>

<script>

export default {

methods: {

childMethod () {

console.log('子组件的方法')

}

}

}

</script>

这里使用了ref identify父组件里的$refs属性。它只在渲染后建立,并且它为组件提供一个实例引用,用$refs.child.childMethod()这种方式才能调用子组件的方法。

1.2 兄弟组件通信

兄弟组件之间的通信是通过其共同的父组件作为中转站进行数据传递的,会有一些复杂,但不会让整个应用变得一团糟。

下面是一个兄弟组件通过事件总线进行通信的例子:

// 事件总线

import Vue from 'vue'

export default new Vue()

// 发出事件并传递数据

import EventBus from './eventBus'

EventBus.$emit('dataChanged', data)

// 监听事件并获取数据

import EventBus from './eventBus'

EventBus.$on('dataChanged', data => {

// 处理数据

})

1.3 跨级组件通信

如果组件层级相对较深,光靠父子传递显然是不可能完成的。要实现跨级组件通信,可以使用provide和inject的方式。

provide和inject让我们可以在父组件中定义一个数据源,并在任何子组件中调用。它不会像props和事件emit那样,要求父组件要向子组件上传递数据或者调用子组件的方法,而是直接在子组件中调用。

下面是一个provide和inject的例子:

// 父组件

<template>

<div>

<Grandson />

</div>

</template>

<script>

import EventBus from '../eventBus'

export default {

provide: {

eventBus: EventBus

},

mounted () {

setTimeout(() => {

EventBus.$emit('message', '父组件的消息')

}, 2000)

}

}

</script>

// 子组件

<template>

<div>

{{message}}

</div>

</template>

<script>

export default {

inject: ['eventBus'],

data () {

return {

message: ''

}

},

mounted () {

this.eventBus.$on('message', message => {

this.message = message

})

}

}

</script>

通过provide向整个应用提供一个共享数据源,让inject在任何组件中调用,深层次组件可以方便地获取并渲染数据。

2. Vuex 状态管理

Vuex是一个专为Vue.js应用程序开发的状态管理模式。它使用集中式存储管理应用所有组件的状态,并提供规则保证状态只能有可预测的方式进行操作。使用Vuex可以使组件之间更好地进行通信,同时保证各个组件取得的状态准确无误。

2.1 Vuex Store

Vuex Store是一个状态管理工具,它具有五个特点:

单一状态树:用一个对象存储全部组件的状态,作为一个"唯一数据源",易于状态管理;

State:也即store提供的数据源,我们定义的全局变量都在这里存放;

Mutations:Mutations是Vuex存储状态数据的方法;

Actions:Action类似Mutations,不同之处在于Actions可以处理异步操作,并修改State;

Getters:类似Vue组件中的computed计算属性,用户从Store中的State派生出一些状态。

下面是一个Vuex Store的例子:

// store.js

import Vue from 'vue'

import Vuex from 'vuex'

Vue.use(Vuex)

const store = new Vuex.Store({

state: {

count: 0

},

mutations: {

increment (state) {

state.count++

}

},

actions: {

incrementAsync ({ commit }) {

setTimeout(() => {

commit('increment')

}, 1000)

}

},

getters: {

count: state => state.count

}

})

export default store

State是Vuex存储状态数据的方法,Mutations也是类似的方法,不同之处在于Mutations不能处理异步操作,只能同步更新state的值。

下面是一个Vuex中使用Mutations:

// 定义Mutation

mutations: {

changeStatus (state, status) {

state.status = status

}

}

// 调用Mutation

this.$store.commit('changeStatus', '登录成功')

Action类似Mutations,不同之处在于Action可以处理异步操作,并修改State的值,例如:

// 定义Action

actions: {

getUserName ({ commit, state }, id) {

// 存在缓存中则直接读取

let user = state.users.find(user => user.id === id)

if (user !== undefined) {

commit('SET_USER_NAME', user.name)

return

}

// 不存在则发起请求

axios.get('/api/user/' + id)

.then(res => res.data)

.catch(() => {

let userName = '未知用户'

commit('SET_USER_NAME', userName)

})

}

}

// 调用Action

this.$store.dispatch('getUserName', '1001')

Getter是由State派生出来的,类似Vue组件中的computed计算属性,除了作为对State的一个扩展外,Getter还可以和其他Getter进行组合。下面是一个Getter的例子:

// 定义Getter

getters: {

doneTodos: state => {

return state.todos.filter(todo => todo.done)

}

}

// 调用Getter

this.$store.getters.doneTodos

2.2 使用Vuex

Vuex到底是怎么用呢?使用Vuex分为以下几步:

安装Vuex,导入Vuex的状态Store;

定义状态管理mutations;

如果需要异步操作,定义action;

定义getters。

下面是一个简单的使用Vuex的例子:

先定义一个状态,可以在使用的时候方便的调用和修改:

// 创建一个使用的状态

// locations,savedMap,travelMode都是状态初始值

const state = {

locations: [],

savedMap: null,

travelMode: 'DRIVING'

}

接下来,我们需要在Vuex中定义mutations,这里是直接修改state的地方:

// 定义mutations

const mutations = {

SET_LOCATIONS: (state, payload) => { state.locations = payload },

SET_SAVED_MAP: (state, payload) => { state.savedMap = payload },

SET_TRAVEL_MODE: (state, payload) => { state.travelMode = payload }

}

之后,我们需要把一个mutation绑定到一个实例的方法中。这里是一个设置位置的例子:

// 需要使用dispatch('setLocations', ...)来执行该方法

// setLocations方法调用了mutations的SET_LOCATIONS来修改State

const actions = {

setLocations: ({ commit }, payload) => {

commit('SET_LOCATIONS', payload)

}

}

在mutations之后,通过actions来实现我们需要的功能:

// 获取当前位置的信息

// 使用Promise方便异步加载

getCurrentPosition ({ state, dispatch }) {

const options = { enableHighAccuracy: true, timeout: 60000 }

return new Promise((resolve, reject) => {

if (!('geolocation' in navigator)) {

// 没有获取到坐标,报错

reject(new Error('Geolocation is not supported by this browser.'))

return

}

function successCallback (position) {

const lat = position.coords.latitude

const lng = position.coords.longitude

// 调用setLocations报告当前位置

dispatch('setLocations', { lat, lng })

resolve({ lat, lng })

}

function errorCallback (error) {

console.error(`Unable to retrieve position: ${error.message}`)

reject(error)

}

navigator.geolocation.getCurrentPosition(successCallback, errorCallback, options)

})

}

最后,我们还需要定义getters来返回状态的一些计算值:

// 声明getter

const getters = {

// 返回两个坐标点之间的距离

getLatLngDistance: state => (lat1, lng1, lat2, lng2) => {

// 两点之间的距离

const R = 6371e3; // metres

const φ1 = lat1 * Math.PI / 180; // φ, λ in radians

const φ2 = lat2 * Math.PI / 180;

const Δφ = (lat2 - lat1) * Math.PI / 180;

const Δλ = (lng2 - lng1) * Math.PI / 180;

const a = Math.sin(Δφ / 2) * Math.sin(Δφ / 2) +

Math.cos(φ1) * Math.cos(φ2) *

Math.sin(Δλ / 2) * Math.sin(Δλ / 2);

const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));

const d = R * c; // in metres

return d

}

}

3. 插件

除了直接在Vuex中定义状态变量和事件,VUE.js还提供了插件机制来扩展其功能。插件可以给某个组件或整个应用增加业务逻辑,并提供一种封装和复用代码的方式。

3.1 定义插件

Vue插件的机制其实很简单,只需要定义一个供Vue.use方法调用的对象或者方法即可,该对象或方法必须有一个install方法用来安装扩展逻辑。

下面是一个写入localStorage的插件:

let MyPlugin = {}

MyPlugin.install = function (Vue, options) {

Vue.prototype.$myMethod = function () {

alert('custom method: $myMethod')

}

Vue.prototype.$localStorage = {

setItem (key, value) {

localStorage.setItem(key, value)

},

getItem (key) {

return localStorage.getItem(key)

}

}

}

export default MyPlugin

这个插件将一个方法和一个对象添加到Vue实例上,对应的方法和属性可以在组件内直接使用。

3.2 使用插件

使用插件非常简单,只要在Vue.use方法里注册该插件即可:

// main.js注册整个项目的插件

import Vue from 'vue'

import App from './App.vue'

import router from './router'

import store from './store'

import MyPlugin from './plugins/my-plugin'

Vue.use(MyPlugin)

Vue.config.productionTip = false

new Vue({

router,

store,

render: h => h(App)

}).$mount('#app')

然后,在组件中即可直接使用插件方法或对象了:

// 在Vue组件内使用

methods: {

myClickHandler () {

// 调用自定义方法

this.$myMethod()

// 调用localStorage对象的方法

this.$localStorage.setItem('key', 'value')

}

}

这里调用了向localStorage写入值的方法,该操作比直接访问localStorage对象更方便,因为这样的话,在其他人员遇到类似的问题时,他们也可以使用同一个方法。

4. 结语

VUE.js实现组件通信的