从0开始在vue中使用WebSocket,原来WebSocket如此简单

3,490 阅读3分钟

最近做的一个需求涉及到WebSocket,开始以为是个很高大上的东西,做下来才发现很好拿捏。再次证明很多东西不要怕,实际上没有那么唬人。

WebSocket是什么

websocket是一种在单个TCP连接上进行全双工通讯的协议。websocket使得客户端和服务器之间的数据交换变得更加简单,允许服务器主动向客户端推送数据。

在websocket API中,浏览器和服务器只需要完成一次握手,两者间就直接可以创建持续性的连接,并进行双向传输。

websocket最大的特点是除了客户端可以主动向服务器发送信息外,服务器页可以主动向客户端推送信息,是真正的双向通行,属于服务器推送技术的一种。

协议标识符是ws(如果加密,则为wss),服务器网址就是 URL,控制台查看地址如图:

image.png                                                   

WebSocket应用场景

  • 网络电话
  • 即时聊天
  • 多人游戏
  • 在线协同编辑文档
  • 体育/游戏实况
  • 实时地图位置

以网络电话为例,客户端需要知道播出后对方的接听状态、挂断状态,还要获取客户端没操作时对方的来电信息,这种情况下http协议无法满足由服务器向客户端推送,所以适合用websocket协议。

封装WebSocket

wobsocket.js

/*
* socket长连接和公共管理方案
* websocket和 VueX 还有 Redux建立起长连接机制 ,防止在路由切换时候的时候断开连接,需要把socket实例放入公共管理统一处理
* 此方案暴露出一个触发器 $soctket_emit api,方便任何组件调用 , 内部有一个订阅器$soctket_subscribe api ,与 VueX 中 mutations 和 Redux 中 reducers实时连接改变数据
*/

let socketUrl = ''
/**
* @param value  
* @returns {string}  强数据类型校验
*/

function isType (value) {
    return Object.prototype.toString.call(value).slice(8, -1)
}

/**
* @param event 当前事件
*  事件轮询器
*/
function eventPoll (event, outerConditon, time, callback) {
    let timer
    let currentCondition
    timer = setInterval(() => {
        if (currentCondition === outerConditon) {
            clearInterval(timer)
            callback && callback()
        }
        currentCondition = event()
    }, time)
}

function isSocketContent () { 
    // 你的websocket url地址
        socketUrl = 'ws://xxx:xxxx/'
    // }
}

/**
* @constructor 构造函数
* commit 公共管理触发器
* action 处理返回订阅器返回数据
*/

function socket (commit, actions) {
    if (isType(commit) !== 'Function') {
        throw new Error('commit must be a function')
    }  
    this.commit = commit
    this.actions = actions || null
    this.timer = null
    this.errorResetNumber = 0 // 错误重连间隔
    this.closeWs = false
    this.errorFrom = 0 // socket断开来源
    this.errorResetTimer = null // 错误重连轮询
    this.errorDispatchOpen = true // 开启错误调度
    this.heartSocketOpen = false
    isSocketContent()
    this.$soctket_init()
}

/**
* websocket ->初始化
* @param callback 初始化失败回调
* @param value 数据处理
*/
socket.prototype.$soctket_init = function (callback) {
    const _this = this
    if (_this.closeWs) {
        throw new Error('socket is closed ,$socker_init is fail ,  all methods is invalid')
    }
    // const token = window.localStorage.getItem('token') || window.sessionStorage.getItem('token') || null

    // if (!token) {
    //     throw new Error('token  is underfined')
    // } 
    const handerErrorMachine = () => { 
        if (_this.errorResetNumber === 4) {
            _this.errorResetNumber = 0
            _this.errorResetTimer = null
            _this.errorFrom = 0
            _this.errorDispatchOpen = false
            _this.ws = null
            console.log('socket连接失败')
            return
        }
        _this.errorResetTimer = setTimeout(() => {
            _this.$soctket_init()
            _this.errorResetNumber++
        }, _this.errorResetNumber * 2000)
    } 
    
    const errorDispatch = (eventment) => { 
        let event = eventment
        return function () {
            if (_this.errorFrom === 0 && _this.errorDispatchOpen) {
                _this.errorFrom = event
            }
            event === 1 ? console.log('web socket has failed  from closeState ') : console.log('web socket has failed  from errorState ')
            if (_this.errorFrom === event && !_this.closeWs) {
                _this.errorResetTimer && clearTimeout(_this.errorResetTimer)
                handerErrorMachine()
            }   
        }
    }
    if (this.timer) clearTimeout(this.timer)

    _this.ws = new WebSocket(socketUrl)

    _this.ws.onopen = function () {
        callback && callback()
        _this.errorResetNumber = 0
        _this.errorResetTimer = null
        _this.errorFrom = 0
        _this.errorDispatchOpen = true
        _this.$soctket_subscribe()
        _this.$soctket_heartSoctket()
        console.log('web socket has connected ')
    }

    _this.ws.onclose = errorDispatch(1)
    _this.ws.onerror = errorDispatch(2)
}

/**
* 触发器->发布信息
* @param callback 状态处理
* @param value 数据处理
*/
socket.prototype.$soctket_emit = function (value, callback) {
    const _this = this
    const poll = function () {
        return _this.ws.readyState
    }
    if (callback && isType(callback) !== 'Function') {
        throw new Error('$socket_emit arugment[1] must be a function')
    }
    if (!_this.ws) {
        throw new Error('$socket dispatch is fail please use $socket_open method')
    }
    console.log('look at this1,',_this.ws.readyState)
    
    if (_this.ws.readyState == 1) { // 连接成功状态
        console.log('look at this2,',value)
        _this.ws.send(JSON.stringify(value))
        _this.$soctket_heartSoctket()
        callback && callback()
    }
    else if (_this.ws.readyState === 0) { // 连接中状态 ,轮询查询连接
        console.log('look at this3,',value)
        eventPoll(poll, 1, 500, () => {
            _this.ws.send(JSON.stringify(value))
            _this.$soctket_heartSoctket()
            callback && callback()
        })
    }
    else { // 失败重新连接
        _this.$soctket_init(() => {
            _this.$soctket_emit(value, callback)
        })
    }
}

/**
* 订阅器->接受广播
*/

socket.prototype.$soctket_subscribe = function () {
    const _this = this

    _this.ws.onmessage = function (res) {
        if (_this.actions) {
            if (isType(_this.actions) !== 'Function') {
                throw new Error('actions')
            } else {
                _this.commit(..._this.actions(res.data))
            }
        } else {
            _this.commit(JSON.parse(res.data).cmd,JSON.parse(res.data))
            
        }    
        _this.$soctket_heartSoctket()
    }
}
/**
* 心脏搏动机制->防止断开连接
*/

socket.prototype.$soctket_heartSoctket = function () {  
    if (this.timer) clearTimeout(this.timer)
    console.log(this.timer)
    this.timer = setTimeout(() => {
        if (this.ws.readyState === 1 || this.ws.readyState === 0) {
            this.ws.send(JSON.stringify({"cmd":"keepalive","timestamp":"<timestamp>"}))
            this.$soctket_heartSoctket()
        } else {
            this.$soctket_init()
        }
    }, 59000)
}
/**
* 关闭socket连接
*/
socket.prototype.$soctket_close = function () {
    if (this.timer) clearTimeout(this.timer)
    if (this.errorResetTimer)clearTimeout(this.errorResetTimer)
    this.closeWs = true
    console.log('this is closing')
    // this.ws.send(JSON.stringify(value))
    this.ws.close()
}
/**
* 重启socket连接
*/
socket.prototype.$soctket_open = function () {
    if (!this.closeWs) {
        throw new Error('socket is connected')
    }
    this.timer = null
    this.errorResetNumber = 0
    this.closeWs = false
    this.errorFrom = 0
    this.errorResetTimer = null
    this.errorDispatchOpen = true
    this.heartSocketOpen = false
    this.closeWs = false
    this.$soctket_init()
}
export default socket

WebSocket结合状态管理器(Vuex)使用

header.vue

  <div>
    <div class="header">
              <span @click="handlerClick" class="statusFlag">{{
                statusFlag ? "【退出】" : "【登录】"
              }}</span>
              <span style="margin-left: 0">呼叫系统|</span
              ><span :class="statusFlag ? `online` : `outline`"
                >● {{ statusFlag ? "在线" : "离线" }}</span
              >
    </div>
  </div>
</template>

<script>
export default {
  name: "Header",
  data() {
    return {
      status: "离线",
      statusFlag: false,
    };
  },
  created() {
    this.$store.dispatch("socketInit");

  },
  mounted() {
  },
  methods: {
    callLogin(callback) {
      const { ws } = this.$store.state.socket;
      ws.$soctket_emit(
        {
          cmd: "seatlogin",
          seatname: "0",
          seatnum: "",
          password: "8888",
          timestamp: "<timestamp>",
        },
        () => {                                                                     
            console.log("登录成功");
        }
      );
    },
    callLogout() {
      const { ws } = this.$store.state.socket;
      const param = {
        cmd: "seatlogout",
        seatname: "0",
        seatnum: "",
        timestamp: "<timestamp>",
      };
      ws.$soctket_emit({
        cmd: "seatlogout",
        seatname: "0",
        seatnum: "",
        timestamp: "<timestamp>",
      });
    },
    handlerClick() {
      if (this.statusFlag) {
        this.callLogout();
        this.statusFlag = !this.statusFlag;
      } else {
        //当前退出 准备登入
        this.callLogin();
        this.statusFlag = !this.statusFlag;
      }
    },
    },
  },
};
</script>

<style lang="scss" scoped>
</style>

floatBtn.vue


      <!-- // 0:空闲
    // 1:摘机
    // 2:拨号中
    // 3:通话 4:振铃 -->
      <div
        class="info"
        v-if="statusList.length ? statusList[channel].phonestate == 0 : false"
      >
        <img src="@/assets/image/call-1.png" alt="空闲" />
        <p>空闲</p>
      </div>
      <div
        class="info"
        v-else-if="
          statusList.length ? statusList[channel].phonestate == 1 : false
        "
      >
        <img src="@/assets/image/call-2.png" alt="摘机" />
        <p>摘机</p>
      </div>
      <div
        class="info"
        v-else-if="
          statusList.length ? statusList[channel].phonestate == 2 : false
        "
      >
        <img src="@/assets/image/call-3.png" alt="拨号中" />
        <p>呼叫中...</p>
      </div>
      <div
        class="info"
        v-else-if="
          statusList.length ? statusList[channel].phonestate == 3 : false
        "
      >
        <img src="@/assets/image/call-4.png" alt="通话" />
        <p>{{ one }}:{{ two }}:{{ three }}</p>
      </div>
      <div
        class="info"
        v-else-if="
          statusList.length ? statusList[channel].phonestate == 4 : false
        "
      >
        <img src="@/assets/image/call-5.png" alt="呼入" />
        <p>呼入</p>
      </div>
      <div class="info" v-else>
        <p>请稍等</p>
      </div>
</template>
<script>
import { mapGetters } from "vuex";
import callInfo from "./callInfo.vue";
export default {
  name: "floating-window",
  components: {
    callInfo,
  },
  computed: {
    ...mapGetters(["channelStatusList"]),
    channelStatusList() {
      return this.$store.state.socket.channelStatusList;
    },
  },
  props: {
    form: Object,
  },
  data() {
    return {
      statusList: [],
      flag: null,
      channel: "",
      phonestate: "",                                                           
    };
  },
  mounted() {
    const { ws, channel } = this.$store.state.socket;
    this.channel = channel;
    ws.$soctket_emit(
      {
        cmd: "channelstatusopen",
        seatname: "0",
        seatnum: "",
        timestamp: "<timestamp>",
      },
      () => {
        console.log("打开坐席通道:", this.$store.state.socket.channel);
      }
    );
  },
  watch: {
    channelStatusList(item1, item2) {
      this.statusList = item1;
      if (this.statusList[this.channel].phonestate == 0) {
        this.end()
      }
      if (this.statusList[this.channel].phonestate == 3) {
        this.timer();
      }
      if (this.statusList[this.channel].phonestate == 4) {
        this.openCallInfo();
      }
    },
  },
  methods: {
    handleClickMenuAction() {
      this.$confirm("确定要挂断吗?", "提示", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning",
      }).then(() => {
        this.$message({
          message: "已挂断",
        });
        this.closed = false;
        this.$emit("close-call", closed);
      });
      return;
    },
    openCallInfo() {
      this.$emit("openCallInfo", this.statusList[this.channel].phonestate == 4,this.statusList[this.channel].dtmfa);
    },
  },
};
</script>

sokect.js


const sokect = {
    state: {
        ws: null,//websocket实例
        callLogin:'',//是否登录坐席
        channel:'',//通道号
        channelStatusList:'',//所有通道的状态
    },

    mutations: {
        contentSocket(state, { commit }) {
            state.ws = new Socket(commit)
        },
        seatlogin(state,data){
            state.callLogin = data.result
            state.channel = data.channel
        },
        channelstatus(state,result){
            state.channelStatusList = result.channelStatusList
            console.log('channelstatus',state.channelStatusList)
        },
        seatlogout(state,data){
            data.result == 'success' && (state.callLogin = 'closed')

        }
    },

    actions: {
        socketInit({ commit, state }) {
            commit('contentSocket', { commit })
        },
        
        // seatlogin({ commit, state }) {
        //     commit('seatlogin', { commit })
        // }

    }
}

export default sokect

getters.js

  ws:state => state.sokect.ws,
  callLogin:state => state.sokect.callLogin,
  channelStatusList:state => state.sokect.channelStatusList
}
export default getters

index.js

import Vuex from 'vuex'
import app from './modules/app'
import socket from './modules/sokect'

Vue.use(Vuex)

const store = new Vuex.Store({
  modules: {
    app,
    socket,
  },
  getters
})

export default store