vue使用vue-socket.io踩坑
u、转角 人气:0前言
vue项目实时通信实现常用方式:
一、原生HTML5 WebSocket实现,vue中使用websocket
二、插件socket.io
官网 ,Socket.io是一个WebSocket库,包括了客户端js和服务器端的nodejs,会自动根据浏览器从WebSocket、AJAX长轮询、Iframe流等等各种方式中选择最佳的方式来实现网络实时应用,最低支持IE5.5; 说白了就是如果浏览器支持websocket,那么socket.io就等同于websocket。当然 socket.io还用到了其它的技术来模拟websocket,所以当你使用socket.io的时候,不管浏览器是否支持websocket,你都可以实现异步操作。
使用注意:客户端使用了socket.io 服务端也必须对应使用
三、vue-socket.io是vue对socket.io的封装,使用方式与socket.io大同小异,核心参数还是要参照socket.io官网相关配置;当前在使用中也存在部分坑
当前文章主要说vue-socket.io以下问题:
- 初始化全局挂载使用,事件订阅与销毁(基本无坑,需注意订阅事件要对应销毁,否则存在多次订阅引发的消息重复接收问题)
- 带token验证的全局挂载方式(有坑,有解决方案与解决后使用注意事项)
- 组件内使用
安装
建议直接使用taobao镜像,淘宝镜像地址建议使用新地址,老地域名将于2022年05月31日零时起停止服务nrm use taobao || npm config set registry registry.npmmirror.com/
npm i vue-socket.io@3.0.10 -S //个人使用了当前版本
使用方式一 (官方用法)[全局挂载,不验证]
需注意:事件只有在订阅后才可接接收返回消息(订阅方法与后端约定名称)。
实例创建后,全局挂载实例方法,this.$socket,this.sockets。
其中事件订阅、取消与监听方法在this.sockets封装;
this.$socket是socket.io实例中io的封装。
// 全局挂载 // main.js import VueSocketIO from 'vue-socket.io' Vue.use(new VueSocketIO({ debug: true,// 生产环境关闭,打开可在控制台查看socket连接和事件监听的信息 options: { autoConnect: false //创建时是否自动连接,默认关闭,使用时用open开启链接 }, connection: 'http://127.0.0.1:9527' //链接地址 }))
//1.全局挂载-全局使用 //main.js new Vue({ sockets: { connecting() { console.log('正在连接') }, connect() { console.log('连接成功') }, disconnect() { console.log('断开连接') }, connect_failed() { console.log('连接失败') }, error() { console.log('错误发生,并且无法被其他事件类型所处理') }, reconnecting() { console.log('正在重连') }, reconnect_failed() { console.log('重连失败') }, reconnect() { console.log('重连成功') }, welcome: data => {//全局监听订阅事件,需要与后端约定好 console.log('welcome data', data) } } })
//2. 全局挂载-组件内使用 //demo.vue <template> <div> <button @click="socketOpen">连接Socket</button> <button @click="closeSocket">断开链接</button> <button @click="submsgContent(true)">订阅事件</button> <button @click="submsgContent(false)">取消订阅事件</button> <button @click="socketSendmsg">发送数据</button> <button @click="lockResult">查看链接参数</button> </div> </template> <script> export default { beforeDestroy() { //订阅事件记得要取消---否则多次订阅会引发多次消息返回 if (!this.$socket) return this.sockets.unsubscribe('msgContent') this.$socket.close() }, sockets: { //监听用的是this.sockets 发送消息是this.$socket,不要弄混 connecting() { console.log('正在连接') }, connect() { console.log('连接成功') }, disconnect() { console.log('断开连接') }, connect_failed() { console.log('连接失败') }, error() { console.log('错误发生,并且无法被其他事件类型所处理') }, reconnecting() { console.log('正在重连') }, reconnect_failed() { console.log('重连失败') }, reconnect() { console.log('重连成功') }, welcome: data => {//全局监听订阅事件,需要与后端约定好 console.log('welcome data', data) } }, methods: { socketOpen() { this.$socket.open()// 开始连接 socket }, socketSendmsg() { // 发送消息 this.$socket.emit('hello', '这里是客户端') }, lockResult() { console.log('链接状态', this.$socket.connected) console.log('this.$socket', this.$socket) console.log('this.sockets', this.sockets) }, closeSocket() { this.$socket.close() }, submsgContent(flag) { if (flag) { //事件订阅 this.sockets.subscribe('welcome', data => { //组件内监听 console.log('组件内监听-welcome', data) }) } else {//取消订阅 this.sockets.unsubscribe('welcome') } } } } </script>
使用方式二 (组件挂载使用)[可验证]
因在组件内使用,所以可将逻辑写在登录后加载挂载组件,全局或指定页面使用。
组件内创建注意事项:
因vue-socket.io,在new.use()时挂载方法$socket到全局,在创建实例时挂载sockets监听对象到当前实例,所以,在组件中使用后会导致以下问题:
问题:
1. $socket可正常使用,与$socket也就是io相关的方法都可正常使用(例如:this.$socket.open()、close等等...)
2. sockets相关方法无法使用(例如:this.sockets.unsubscribe、this.sockets.subscribe等等)
处理方式:使用socket.io原生方法,
1. 事件订阅
实例.emitter.addListener
2. 取消订阅
实例.emitter.addListener
//demo.vue <template> <div> <button @click="createSocket">创建socket</button> <div v-if="createSocketIO ? true:false "> <button @click="socketOpen">连接Socket</button> <button @click="closeSocket">断开链接</button> <button @click="submsgContent(true)">订阅事件</button> <button @click="submsgContent(false)">取消订阅事件</button> <button @click="socketSendmsg">发送数据</button> <button @click="lockResult">查看链接参数</button> </div> </div> </template> <script> import VueSocketIO from 'vue-socket.io' export default { data() { return { createSocketIO: null, createSocketEmitter: null } }, beforeDestroy() { //订阅事件记得要取消 if (this.createSocketIO) { this.createSocketEmitter.removeListener('msgContent', this) this.createSocketIO.close() } }, methods: { async createSocket() {//也可在页面初始话调用 let socketUrl /* // 动态ip与token实现,ip+端口可通过接口拿取,token及用户信息可通过vuex拿取 const userId = store.getters.userInfo.userId const ipResult = await getHostIp() if (ipResult.code !== 0) return const { ip, port } = ipResult.data const protocol = window.location.protocol const socketUrl = `${protocol}//${ip}:${port}?userId=${userId}` console.log('socketUrl', socketUrl) */ socketUrl = 'http://127.0.0.1:9527' const createSocketItem = new VueSocketIO({ debug: true, options: { autoConnect: false,//默认关闭,创建后打开,组件内使用可直接打开,就不需要用io.open() transports: ['websocket'], query: { token: 77777777777 //携带的额外参数也可通过url拼接实现 } }, connection:socketUrl }) const { io, emitter } = createSocketItem io.query.ttt = 8888888888 this.createSocketIO = io this.createSocketEmitter = emitter io.open() io.on('connecting', () => { console.log('正在连接---888') }) io.on('connect', () => { console.log('连接成功---888') }) io.on('disconnect', () => { console.log('断开连接---888') }) io.on('connect_failed', () => { console.log('连接失败---888') }) io.on('error', () => { console.log('错误发生,并且无法被其他事件类型所处理') }) io.on('reconnect_attempt', () => {console.log('触发尝试重新连接', 888)}) io.on('reconnecting', () => { console.log('正在重连---888') }) io.on('reconnect_failed', () => { console.log('重连失败---888') }) io.on('reconnect', () => { console.log('重连成功---888') }) emitter.addListener('welcome', (data) => { console.log('data', data) }, this) }, socketOpen() { this.createSocketIO.open() }, socketSendmsg() { // 发送消息 this.createSocketIO.emit('hello', '这里是客户端') }, lockResult() { console.log('链接状态', this.createSocketIO.connected) }, closeSocket() { this.createSocketIO.close() }, submsgContent(flag) { if (flag) { this.createSocketEmitter.addListener('welcome', (data) => { console.log('data', data) }, this) } else { this.createSocketEmitter.removeListener('welcome', this) } } } } </script>
使用方式三 (全局挂载使用)[可验证]
当前方法实现类似直接使用socket.io,基本相当于只是使用了vue-socket.io的库与全局挂载的this.$scoket方法。
注: vue.use( new VueSocketIO({connection:'http://127.0.0.1:9527'})) 只将实例中io模块挂载了this.$scoket
实现思路与遭遇问题:
实现:
通过本地轮询,实时获取用户登录状态,用户登录后挂载全局方法
问题:
同实现方式二问题,vue-socket.io,在new.use()时挂载方法$socket到全局(io模块),在创建实例时挂载sockets监听对象到当前实例;在轮询拿到用户当前挂载相关方法时,当前项目实例已经被创建,能拿到this.$socket,不能拿到this.sockets;需要使用页面后置加载,或重新加载后正常,挂载方法才能正常使用
解决方式:
在创建实例时,将除io,模块的事件(emitter)与监听(listener)也挂载到全局,使用相关定义方法在mounted生命周期中进行;登录前尽量把数据存储好后在进入系统
使用示例:
//main.js 入口文件全局挂载 // import Vue from 'vue' import router from '@/router' import store from '@/store' import App from './App' import '@/util/socket.js' //使用全局挂载 Vue.config.productionTip = false const saasVue = new Vue({ router, store, i18n, render: h => h(App) }).$mount('#app') window.saasVue = saasVue
// socket.js 全局方法实现 import Vue from 'vue' import VueSocketIO from 'vue-socket.io' import store from '@/store' import { getHostIp } from '@/api/admin/message' //获取后端动态ip地址 let socketIo, socketListener, socketEmitter if (!getToken()) { const timer = setInterval(() => { console.count() if (!getToken()) return initSocket(getToken()) window.clearInterval(timer) }, 1000) } else { initSocket(getToken()) } function getToken() {//获取登录标识---请修改为自己项目标识 return store.getters.access_token } async function initSocket(token) { const userId = store.getters.userInfo.userId //获取登录标识---请修改为自己项目用户id const ipResult = await getHostIp() if (ipResult.code !== 0) return const { ip, port } = ipResult.data const protocol = window.location.protocol const socketUrl = `${protocol}//${ip}:${port}?userId=${userId}` console.log('socketUrl', socketUrl) //const socketUrl = 'http://127.0.0.1:9527' //本地测试地址---nodejs服务代码放后面 const socket = new VueSocketIO({ debug: process.env.NODE_ENV !== 'production', options: { autoConnect: false //已通过验证,全局使用可默认打开,组件内使用则默认关闭,使用时在打开 }, connection: socketUrl }) const { emitter, io, listener } = socket socketIo = io socketListener = listener socketEmitter = emitter if (process.env.NODE_ENV !== 'production') {// 与socket链接相关全局处理,与后端预定定义事件在组件内订阅使用 io.on('connecting', () => { console.log('socketjs---正在连接') }) io.on('connect', () => { console.log('socketjs---连接成功') }) io.on('disconnect', () => { console.log('socketjs---断开连接') }) io.on('connect_failed', () => { console.log('socketjs---连接失败') }) io.on('error', () => { console.log('socketjs---错误发生,并且无法被其他事件类型所处理') }) io.on('reconnect_attempt', () => { console.log('socketjs---触发尝试重新连接')}) io.on('reconnecting', () => { console.log('socketjs---正在重连') }) io.on('reconnect_failed', () => { console.log('socketjs---重连失败') }) io.on('reconnect', () => { console.log('socketjs---重连成功') }) } // Vue.use(socket) //只是挂载了io模块,存在已加载页面无法订阅问题,不如不使用,自己挂载方便 Object.defineProperty(Vue.prototype, '$socketIo', { value: socketIo }) Object.defineProperty(Vue.prototype, '$socketListener', { value: socketListener }) Object.defineProperty(Vue.prototype, '$socketEmitter', { value: socketEmitter }) } export default { socketIo, socketListener, socketEmitter }
组件内使用:
//demo.vue 订阅事件与销毁在组件中进行 <template> <div> <button @click="socketOpen">连接Socket</button> <button @click="closeSocket">断开链接</button> <button @click="submsgContent(true)">订阅事件</button> <button @click="submsgContent(false)">取消订阅事件</button> <button @click="socketSendmsg">发送数据</button> <button @click="lockResult">查看链接参数</button> </div> </template> <script> export default { beforeDestroy() { //订阅事件记得要取消 if (this.$socketIo) { this.$socketEmitter.removeListener('msgContent', this) this.$socketIo.close() } }, mounted() { this.$socketIo.open()//初始化打开链接 this.$socketEmitter.addListener('welcome', (data) => {//组件初始化挂载后,订阅后端事件 console.log('data', data) }, this) }, methods: { socketOpen() { this.$socketIo.open() }, socketSendmsg() { // 发送消息 this.$socketIo.emit('hello', '这里是客户端') }, lockResult() { console.log('链接状态', this.$socketIo.connected) }, closeSocket() { this.$socketIo.close() }, submsgContent(flag) { if (flag) { this.$socketEmitter.addListener('welcome', (data) => { console.log('data', data) }, this) } else { this.$socketEmitter.removeListener('welcome', this) } } } } </script>
使用方式推荐
方式一:适用于全局通知类业务类型多,不验证用户登录,一般是门口网站引流类型使用。
方法二:适用于通知类型较少的业务项目
方式三:适用项目通知类型多,业务场景模块较多情况,全局挂载,每个页面只需要做对应方法订阅,与销毁;例如:系统消息+业务消息待办通知,首屏监控、大屏展示下多个模块多订阅场景。
nodejs服务端本地demo代码
相关依赖:
npm install socket.io@2.0.4 -S
// nodeSocket.js var http = require('http') var io = require('socket.io') // 创建server服务 var server = http.createServer(function(req, res) { var headers = {} headers['Access-Control-Allow-Origin'] = '*' headers['Access-Control-Allow-Methods'] = 'POST, GET, PUT, DELETE, OPTIONS' headers['Access-Control-Allow-Credentials'] = true headers['Access-Control-Max-Age'] = '86400' // 24 hours headers['Access-Control-Allow-Headers'] = 'X-Requested-With, Access-Control-Allow-Origin, X-HTTP-Method-Override, Content-Type, Authorization, Accept' res.writeHead(200, headers) res.end() }) // 启动服务器 监听 1024 端口 server.listen(1024, function() { console.log('server runing at 127.0.0.1:1024') }) // 启动socket服务 var socket = io.listen(server, { origins: '*:*' }) socket.use((socket, next) => { const query = socket.handshake.query console.log('token', query.token) if (query.token) { return next() } return next(new Error('authentication error 鉴权失败')) }) // 监听客户端连接 socket.on('connection', function(socket) { console.log('客户端有连接') // 监听客户端断开 socket.on('disconnect', () => { console.log('客户端断开') }) // 给客户端发送消息 setInterval(() => { socket.emit('welcome', '欢迎连接socket') console.count(1) }, 2000) // 监听客户端消息 socket.on('hello', data => { console.log('接收客户端数据---:', data) }) })
启动:
node nodeSocket.js
总结
- vue-socket.io使用版本注意,3.0.8、3.0.9有部分bug,感兴趣的可以去试试;
- nodejs模块使用的socket.io版本注意,socket.io版本大于了2.0.4后使用方法有变化;
- 消息订阅同一方法可多次订阅,订阅几次,后端一次返回了几次订阅消息。所以注意取消订阅方法。
- 同一订阅方法,在不同页面使用,销毁后会取消所有使用当前方法的订阅消息。需要不同模块与后端约定不同方法,根据项目消息架构处理;
加载全部内容