沃土前端社区Vue系列教程 - event bus 和 vuex

在开发过程中,父子组件传递数据,我们用props和$emit可以解决问题,那么非父子组件之间的数据传递我们要怎么解决呢,一般有两个方案,一个event bus(事件总线)和 vuex。我们先来说event bus。

一、用vue脚手架构建一个简单的项目

# 全局安装 vue-cli
npm install --global vue-cli
# 创建一个基于 webpack 模板的新项目
vue init webpack vuexdemo
# 安装依赖,走你
cd vuexdemo
npm install
npm run dev

二、使用event bus实现跨组件通信

event bus事件总线,组件之间的通信需要使用一个中介来实现,在event bus里面使用一个空的Vue实例来做为通信的桥梁,下面是使用event bus来实现改变登录状态的demo。

  1. 在原有main.js里加上这么一句
// 把bus
Vue.prototype.bus = new Vue();
  1. 修改原有的Index.vue组件,添加如下代码:
 data() {
    return {
      loginStatus: false
    };
  },
  created() {
    // 监听事件
    this.bus.$on("login", (data) => {
      this.loginStatus = data;
    });
  }

修改后的Index.vue代码如下:

<template>
  <div class="hello">
    <h1>登录状态:{{ loginStatus?'已登录':'未登录' }}
      <span v-if="!loginStatus">
        <router-link to="/login">去登录</router-link>
      </span>
    </h1>
  </div>
</template>

<script>
export default {
  name: "Index",
  data() {
    return {
      loginStatus: false
    };
  },
  created() {
    this.bus.$on("login", (data) => {
      this.loginStatus = data;
    });
  }
};
</script>

<style scoped>
h1,
h2 {
  font-weight: normal;
}
ul {
  list-style-type: none;
  padding: 0;
}
li {
  display: inline-block;
  margin: 0 10px;
}
a {
  color: #42b983;
}
</style>
  1. 新建组件Login.vue,在组件Login.vue添加登录操作,在登录成功的时候,触发‘login’事件,Login完整代码如下:
<template>
  <div>
    <input type="text" placeholder="请输入用户名"><br />
    <input type="text" name="" placeholder="请输入密码"/><br/>
    <button @click="login" class="login">立即登录</button>
   <p>还没有账号,<router-link to="/register">立即注册</router-link></p> 
  </div>
</template>

<script>
export default {
  methods: {
    login() {
      this.bus.$emit("login", true);
      this.$router.push('/');
    }
  }
};
</script>
<style>
.login {
  width: 100px;
  height: 30px;
  margin-top: 10px;
}
.psw {
  margin-top: 5px;
}
</style>
  1. 同时在vue-router里面配置路由/login,代码如下:
import Vue from 'vue'
import Router from 'vue-router'
import Index from '@/components/Index'
import Login from '@/components/Login'

Vue.use(Router)

export default new Router({
  routes: [
    {
      path: '/',
      name: 'index',
      
      component: Index
    },
    {
      path: '/login',
      name: 'login',
      component: Login
    }
  ]
})
  1. 测试:Index.vue与Login.vue是两个非父子关系的组件,当我们点击Login组件里面的立即登录的时候,Index组件里面显示的登录状态就会发生改变,这样就实现了非父子组件之间的通信。需要注意的是:负责监听的组件必须先挂载,才能使用$emit来触发事件。简单来讲,类似jQuery一样,事件必须先绑定,你才能取触发事件。

三、使用vuex实现跨组件通信

跨组件通信,使用event bus的做法,对于简单的应用来讲,但是对于复杂的应用来讲,event bus对于事件的管理就可能力不从心了,你不一定记得你有多少个组件监听了该事件,你可能会担心,那个组件到底挂载了没有。所以在复杂的应用中我们推荐使用vuex。

什么是vuex
  1. Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式,它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。vuex包含以下几个元素state(存放数据),action(动作,我把它理解为指令,用户派发action,可以看做是用户发出指令),mutation(改变,用来改变state里面的数据)一个完整的过程是视图(用户)派发 action,action提交修改,mutation负责修改state里的数据,state里的数据被修改了,对应的视图就被刷新,见下图。


    沃土前端社区Vue系列教程 - event bus 和 vuex
  2. 每一个 Vuex 应用的核心就是 store(仓库)。“store”基本上就是一个容器,它包含着你的应用中大部分的状态 (state)。Vuex 和单纯的全局对象有相似的地方,都是把数据存到一个地方,然后取出来,或者进行数据的修改,但有以下两点不同:
    • Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
    • 你不能直接改变 store 中的状态。改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化,从而让我们能够实现一些工具帮助我们更好地了解我们的应用。
一个vuex demo
  1. 打开上面的例子,在当前文件夹安装vuex
npm i vuex --save
  1. 新建组件Headline.vue,详细代码如下:
    vuex中提供了mapGetters、mapActions、mapMutations用来映射store里面的相应属性,方便我们直接使用我们事先定义的getters、actions和mutations。
<template>
  <h1>组件名称:{{title}}</h1>
</template>

<script>
import { mapGetters } from "vuex";
export default {
  computed: {
    // 映射 `this.title` 为 `this.$store.getters.title`
    ...mapGetters(["title"])
  }
};
</script>
  1. 在src文件夹下新建文件夹store,再新建文件index.js,在index.js里面添加以下内容
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)

export default new Vuex.Store({
    // 存储数据
    state: {
        title: '根组件'
    },
    // 读取数据
    getters: {
        title: state => state.title
    },
    // 动作
    actions: {
        // 修改标题的action
        'UPDATE_TITLE_ACTION'({ commit }, payload) {
            commit('UPDATE_TITLE', payload);
        }
    },
    // 变化;转变
    mutations: {
         // 修改state里的title
        'UPDATE_TITLE'(state, payload) {
            state.title = payload;
        }
    }
})

当涉及的数据比较多的时候,getters、actions、mutations可以用单独的文件导出。

  1. 在main.js里添加下面两行代码
import store from './store'

new Vue({
  el: '#app',
  // 新增代码
  store,
  router,
  template: '<App/>',
  components: { App }
})
  1. 在Login组件里添加如下代码,当我们访问Login组件的时候,就可以看到页面上的组件名称发生了改变
beforeRouteEnter: (to, from, next) => {
    next(vm => {
      vm.$store.dispatch('UPDATE_TITLE_ACTION','登录组件')
    })
  },

同理,我们可以新建一个Register组件,做相关操作,当我们访问/register时改变组件的名称,Register组件代码如下,同时需要在router里面配置添加/register:

<template>
  <div>
      <button>立即注册</button>
      <p>
          已有账号? <router-link to="/login">立即登录</router-link>
      </p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      
    };
  },
  beforeRouteEnter: (to, from, next) => {
    next(vm => {
      vm.$store.dispatch("UPDATE_TITLE_ACTION", "注册组件");
    });
  }
};
</script>

router/index.js里面的代码如下:

import Vue from 'vue'
import Router from 'vue-router'
import Index from '@/components/Index'
import Login from '@/components/Login'
import Register from '@/components/Register'

Vue.use(Router)

export default new Router({
  routes: [
    {
      path: '/',
      name: 'index',
      component: Index
    },
    {
      path: '/login',
      name: 'login',
      component: Login
    },
    {
      path: '/register',
      name: 'register',
      component: Register
    }
  ]
})

7.modules,当我们需要管理很多的状态时,会使用modules来进行分割,具体做法如下:
在store的文件夹新建modules文件夹,里面封装某个具体组件的state、getters、actions和mutations,一下新建一个Foot组件,设置关于该组件的状态信息,其他组件可以修改它的状态,从而改变Foot组件显示的内容(统计浏览过的路由的次数)。

  • 在components下新建组件Foot.vue,代码如下:
<template>
  <div class="foot">
      浏览过的路由个数<span class="num">10</span>
  </div>
</template>

<script>
export default {};
</script>

<style>
.foot {
  background: gray;
  position: fixed;
  bottom: 0px;
  height: 100px;
  width: 100%;
  color: #ffffff;
  line-height: 100px;
  font-size: 20px;
}
.num {
    border-radius: 50%;
    border: 1px solid;
    margin-left: 20px;
    padding: 5px;
}
</style>
  • 在store/modules下新建foot.js,代码如下:
export default {
    state: {
        routeNum: '1'
    },
    getters: {
        routeNum: state => state.routeNum
    },
    actions: {
        'UPDATE_ROUTENUM_ACTION'({ commit }, payload) {
            commit('UPDATE_ROUTENUM',payload);
        }
    },
    mutations: {
        'UPDATE_ROUTENUM'(state, payload) {
            state.routeNum = state.routeNum + payload;
        } 
    }
}
  • 修改store/index.js,修改后代码如下:
import Vue from 'vue'
import Vuex from 'vuex'
import foot from './modules/footer'
Vue.use(Vuex)
// import getters from './getters'
// import mutations from './mutations'
// import actions from './actions';

export default new Vuex.Store({
    // 存储数据
    state: {
        title: '根组件'
    },
    // 读取数据
    getters: {
        title: state => state.title
    },
    actions: {
        // 修改标题
        'UPDATE_TITLE_ACTION'({ commit }, payload) {
            commit('UPDATE_TITLE', payload);
        }
    },
    mutations: {
        'UPDATE_TITLE'(state, payload) {
            state.title = payload;
        }
    },
    modules: {
        foot,
    }
})
  • 在其他组件去派发action来改变Foot组件的状态
    可以在每一个组件都加上这么一句
beforeRouteEnter: (to, from, next) => {
    next(vm => {
      vm.$store.dispatch("UPDATE_TITLE_ACTION", "登录组件");
      vm.$store.dispatch("UPDATE_ROUTENUM_ACTION",1);
    });
  },

当然这个是比较笨的方法,更好的办法是,在main.js里面加上以下这段代码,那么当每一次变换路由的时候我们给底部加圆圈的小数字加1了。

router.beforeEach((to, from, next) => {
  store.dispatch('UPDATE_ROUTENUM_ACTION',1);
  next();
})
在开始的event bus的例子里我们通过event bus改变组件的登录状态,在这里我们也可以使用vuex轻松实现,我们用state用loginState来保存登录的状态,当我们登录成功的时候,就修改loginState的值,具体实现过程如下。
  1. 在store/modules下新建文件login.js,具体代码如下:
import { mapActions } from "vuex";

export default {
    state: {
        loginState: false
    },
    getters: {
        loginState: state => state.loginState
    },
    actions: {
        'UPDATE_LOGINSTATE_ACTION'({ commit }, payload) {
            // UPDATE_LOGINSTATE为对应的mutation
            commit('UPDATE_LOGINSTATE', payload);
        }
    },
    mutations: {
        'UPDATE_LOGINSTATE'(state, payload) {
            state.loginState = payload;
        }
    }
}
  1. 修改store/index.js,在modules里添加,修改后的代码如下:
import Vue from 'vue'
import Vuex from 'vuex'
import foot from './modules/footer'
import login from './modules/login'
Vue.use(Vuex)

export default new Vuex.Store({
    // 存储数据
    state: {
        title: '根组件'
    },
    // 读取数据
    getters: {
        title: state => state.title
    },
    actions: {
        // 修改标题
        'UPDATE_TITLE_ACTION'({ commit }, payload) {
            commit('UPDATE_TITLE', payload);
        }
    },
    mutations: {
        'UPDATE_TITLE'(state, payload) {
            state.title = payload;
        }
    },
    modules: {
        foot,
        login
    }
})
  1. 修改login.vue,代码如下:
<template>
  <div>
    <input type="text" placeholder="请输入用户名"><br />
    <input class="psw" type="text" name="" placeholder="请输入密码"/><br/>
    <button @click="login" class="login">立即登录</button>
    <p>还没有账号,<router-link to="/register">立即注册</router-link></p> 
  </div>
</template>

<script>
export default {
  beforeRouteEnter: (to, from, next) => {
    next(vm => {
      vm.$store.dispatch("UPDATE_TITLE_ACTION", "登录组件");
    });
  },
  methods: {
    login() {
      this.$store.dispatch('UPDATE_LOGINSTATE_ACTION', true);
      this.$router.push("/");
    }
  }
};
</script>
<style>
.login {
  width: 100px;
  height: 30px;
  margin-top: 10px;
}
.psw {
  margin-top: 5px;
}
</style>
  1. 修改components下的index.vue,代码如下:
<template>
  <div class="hello">
    <h1>登录状态:{{ loginState?'已登录':'未登录' }}
      <span v-if="!loginState">
        <router-link to="/login">去登录</router-link>
      </span>
      <button v-else @click="logout">退出登录</button>
    </h1>
  </div>
</template>

<script>
import { mapGetters,mapActions } from 'vuex'
export default {
  name: "HelloWorld",
  data() {
    return {
      
    };
  },
  computed: {
    ...mapGetters(['loginState'])
  },
  beforeRouteEnter: (to, from, next) => {
    next(vm => {
      vm.$store.dispatch("UPDATE_TITLE_ACTION", "根组件");
    });
  },
  methods: {
    ...mapActions(['UPDATE_LOGINSTATE_ACTION']),
    logout() {
      // this.$store.dispatch('UPDATE_LOGINSTATE_ACTION',false);
      this.UPDATE_LOGINSTATE_ACTION(false);
    }
  }
};
</script>

<style scoped>
h1,
h2 {
  font-weight: normal;
}
ul {
  list-style-type: none;
  padding: 0;
}
li {
  display: inline-block;
  margin: 0 10px;
}
a {
  color: #42b983;
}
</style>
探讨问题:
  1. devtool
  2. vue路由的history模式
  3. 刷新后的store里面的数据清空,怎么处理
上一篇:Code First开发系列之数据库迁移


下一篇:沃土前端社区教程 - es6(7)常用技能点