用Vuex管理websocket实时推送的数据,实现全局提示

state

0x00 写在前面

  最近的项目中用到了websocket实现实时通信。遇到了一个问题:如何将websocket服务器端实时推送过来的数据全局实时推送。由于websocket通信的实时性和vue中的渲染机制。在查了很多资料之后,发现vuex是实现这个功能的最佳选择。

  上图中展示了一个状态自管理应用中的单向数据流。由 state,驱动应用的数据源;view,以声明方式将 state 映射到视图;actions,响应在 view 上的用户输入导致的状态变化 三部分组成。当遇到多个组件共享状态时,单向数据流的简洁性很容易被破坏:

  • 多个视图依赖于同一状态。
  • 来自不同视图的行为需要变更同一状态。

  这时,将组件的共享状态抽取出来,以一个全局单例模式管理。在这种模式下,组件树构成了一个巨大的“视图”,不管在树的哪个位置,任何组件都能获取状态或者触发行为!这就是vuex的设计思想。

0x01 什么是vuex?

  vue官方说法中,Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
vuex
  如上图所示,Vuex 应用的核心就是 store(仓库)。“store”基本上就是一个容器,它包含着你的应用中大部分的状态 (state)。Vuex 和单纯的全局对象有以下两点不同:

  • Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。

  • 不能直接改变 store 中的状态。改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样可以方便跟踪每一个状态的变化。

0x02 关于State

  Vuex 使用单一状态树:用一个对象包含全部的应用层级状态。由于 Vuex 的状态存储是响应式的,从 store 实例中读取状态最简单的方法就是在计算属性中返回某个状态:

1
2
3
4
5
6
7
8
9
// 创建一个 Counter 组件
const Counter = {
template: `<div>{{ count }}</div>`,
computed: {
count () {
return store.state.count
}
}
}

  每当 store.state.count 变化的时候,都会重新求取计算属性,并且触发更新相关联的 DOM。这种模式导致组件依赖全局状态单例。在模块化的构建系统中,在每个需要使用 state 的组件中需要频繁地导入,并且在测试组件时需要模拟状态。

  Vuex 通过 store 选项,提供了一种机制将状态从根组件“注入”到每一个子组件中(需调用 Vue.use(Vuex))。通过在根实例中注册 store 选项,该 store 实例会注入到根组件下的所有子组件中,且子组件能通过 this.$store 访问到。

  当一个组件需要获取多个状态时候,将这些状态都声明为计算属性会有些重复和冗余。这时可以使用 mapState 辅助函数帮助我们生成计算属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 在单独构建的版本中辅助函数为 Vuex.mapState
import { mapState } from 'vuex'
export default {
// ...
computed: mapState({
// 箭头函数可使代码更简练
count: state => state.count,
// 传字符串参数 'count' 等同于 `state => state.count`
countAlias: 'count',
// 为了能够使用 `this` 获取局部状态,必须使用常规函数
countPlusLocalState (state) {
return state.count + this.localCount
}
})
}
0x03 关于Mutations

  提交 mutation是更改Vuex 的 store 中的状态的唯一方法。mutation 非常类似于事件:每个 mutation 都有一个字符串的 事件类型 (type) 和 一个 回调函数 (handler)。这个回调函数就是我们实际进行状态更改的地方,并且它会接受 state 作为第一个参数:

1
2
3
4
5
6
7
8
9
10
11
const store = new Vuex.Store({
state: {
count: 1
},
mutations: {
increment (state) {
// 变更状态
state.count++
}
}
})

  mutation handler不能直接被调用,Mutations以相应的type通过调用store.commit(‘increment’)触发 mutation handler

1
store.commit('increment')
0x04 关于Actions

  Action 类似于 mutation,不同在于:

  • Action 提交的是 mutation,而不是直接变更状态。
  • Action 可以包含任意异步操作。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    const store = new Vuex.Store({
    state: {
    count: 0
    },
    mutations: {
    increment (state) {
    state.count++
    }
    },
    actions: {
    increment (context) {
    context.commit('increment')
    }
    }
    })

  Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象,因此可以通过调用 context.commit 提交一个 mutation,或者通过 context.state 和 context.getters 来获取 state 和 getters。实践中,经常会用到 ES2015 的 参数解构 来简化代码(特别是需要多次调用 commit 的时候):

1
2
3
4
5
actions: {
increment ({ commit }) {
commit('increment')
}
}

  Action 通过 store.dispatch 方法触发:

1
store.dispatch('increment')

  这个方式就体现了action与mutation 的不同之处:mutation 必须同步执行,而Action 就不受约束!可以在 action 内部执行异步操作:

1
2
3
4
5
6
7
actions: {
incrementAsync ({ commit }) {
setTimeout(() => {
commit('increment')
}, 1000)
}
}

0x05 项目中的思路

  项目结构如下所示:其中Web.vue是整个页面的根组件,于是我将websocket的实时通信写在了这里。
  在建立连接之后,当有用户上线,后端就向前端实时推送数据,并使用this.$store.dispatch将后端推送的数据保存为store里的一个state。

1、实时通信
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
methods:{
initWebSocket(){
// 打开一个 web socket
this.ws = new WebSocket("ws://ip:port");
this.ws.onopen = this.websocketsend;
this.ws.onmessage = this.websocketonmessage;
this.ws.onclose = this.websocketclose;
},
websocketsend(agentData){// Web Socket 已连接上,使用 send() 方法发送数据
this.ws.send(agentData);
},
websocketonmessage(evt) {
var data = eval("("+evt.data+")");
var type =data.type || '';
var action = data.action;
var id = data.client_id;
// 后端php文件中中返回的init类型的消息,将client_id发给后台进行uid绑定
// 利用jquery发起ajax请求,将client_id发给后端进行uid绑定
// console.log(evt);
if(type == 'init'){
$.ajax({ //这里使用了jQuery的同步通信来提交用户id,是根据后端websocket框架使用的websocket通信连接验证及建立的方式来的。
type: 'POST',
url: "./index.php/bind", //?client_id="+data.client_id+"&token="+sessionStorage.token
async: false,
dataType:'json',
data:{client_id: id,token:sessionStorage.token},
success: function(data){
// console.log(data);
//第一次连接建立之后,就启动一个dispatch将当前返回的cookie存到vuex的全局变量中去。
this.$store.dispatch('dataPush',
data).then(() => {
})
}
});
// console.log("请求后");
}else if(action == 'ping'){
this.websocketsend("pong");
}else{
// console.log(evt.data);
this.$store.dispatch('dataPush', evt.data).then(() => {
})
}
},
websocketclose(e){ // 关闭 websocket
console.log(e);
},
}

  然后,在src文件夹下新建一个/store/index.js文件,引入vuex来进行全局状态管理。在state里面定义一个全局变量data。在mutations里面定义一个dataPush函数,更改state状态。当接收到新用户上线的数据推送时,就会触发store里面的dataPush函数,修改store里面的数据状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const store = new Vuex.Store({
// 全局变量
state: {
data: undefined
},
// 修改全局变量必须通过mutations中的方法
// mutations只能采用同步方法
mutations: {
dataPush (state, payload) {
state.data = payload;
}
},
// 异步方法用actions
// actions不能直接修改全局变量,需要调用commit方法来触发mutation中的方法
actions: {
dataPush (context, payload) {
context.commit('dataPush', payload)
}
}
})
export default store

  最后,在web.vue页面建立一个监听事件,监听$store.state.data这个状态是否有更新,如果有更新,就使用一个提示组件,展示新上线的用户信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
watch:{//监听vuex中更新的data数据
"$store.state.data":function () {
var data = JSON.parse(this.$store.state.data);
// "uid":"xxxxxxx","eventid":"xxxxxxxxxxxxxxxxxxx",
// "sessdate":"","ip":""
this.$Notice.warning({
title: '客户端上线',
desc: "ip:"+data["ip"]+"<br>uid:"+data["uid"]+"<br>eventid:"+data["eventid"]+"<br>sessdate:"+data["sessdate"],
duration: 0
});
}
},
Miss Me wechat
light