全手打原创,转载请标明出处:https://www.cnblogs.com/dreamsqin/p/12018866.html,多谢,=。=~
(如果对你有帮助的话请帮我点个赞啦)

请先允许我狠狠吐个槽:vue-socket.io相关中文博客实在太少太少,来来去去就那么几篇,教程也比较零散,版本也比较老,就算我有暴风式搜索还是找不到解决问题的方案,然后我怒了,开始看源码、写测试demo、几乎把相关的issues都看了一遍,折腾1天后终于。。。搞定了,下面总结一下~

考虑到很多小伙伴看完文章还是一头雾水或者无法复现方案,附加demo源码https://github.com/dreamsqin/demo-vue-socket.io一份,耗时一天~满意到话给我个start~感谢

vue-socket.io其实是在socket.io-client基础上做了一层封装,将$socket挂载到vue实例上,同时你可以使用sockets对象轻松实现组件化的事件监听,让你在vue项目中使用起来更方便。我目前用的vue-socket.io:3.0.7,可以在其package.json中看到它依赖于socket.io-client:2.1.1

websocket连接地址是从后端动态获取,所以导致页面加载时VueSocketIO实例还未创建,页面中通过this.$socket.emit发起订阅报错,同时无法找到vue实例的sockets对象(写在内部的事件将无法监听到,就算后面已经连接成功)

如果你的websocket连接地址是静态的(写死的),可以只看使用教程,如果你跟我遇到了同样的问题,那就跳跃到解决方案

console报错如下:

先抛开可能遇到的问题,按照官网的教程我们走一遍:

npm install vue-socket.io --save

  1. import Vue from \'vue\'
  2. import store from \'./store\'
  3. import App from \'./App.vue\'
  4. import VueSocketIO from \'vue-socket.io\'
  5. Vue.use(new VueSocketIO({
  6. debug: true,
  7. connection: \'http://metinseylan.com:1992\',
  8. vuex: {
  9. store,
  10. actionPrefix: \'SOCKET_\',
  11. mutationPrefix: \'SOCKET_\'
  12. },
  13. options: { path: "/my-app/" } //Optional options
  14. }))
  15. new Vue({
  16. router,
  17. store,
  18. render: h => h(App)
  19. }).$mount(\'#app\')
  • debug生产环境建议关闭,开发环境可以打开,这样你就可以在控制台看到socket连接和事件监听的一些信息,例如下面这样:

  • connection连接地址前缀,注意!这里只有前缀,我之前被坑过,因为明明后端有给我返回上下文,但莫名其妙的被去除了,vue-socket.io这里用到的是socket.io-clientManager api,关键源码如下(只看我写中文备注的部分就好):

vue-socket.io(index.js)

  1. import SocketIO from "socket.io-client";
  2. export default class VueSocketIO {
  3. /**
  4. * lets take all resource
  5. * @param io
  6. * @param vuex
  7. * @param debug
  8. * @param options
  9. */
  10. constructor({connection, vuex, debug, options}){
  11. Logger.debug = debug;
  12. this.io = this.connect(connection, options); // 获取到你设定的参数后就调用了connect方法
  13. this.useConnectionNamespace = (options && options.useConnectionNamespace);
  14. this.namespaceName = (options && options.namespaceName);
  15. this.emitter = new Emitter(vuex);
  16. this.listener = new Listener(this.io, this.emitter);
  17. }
  18. /**
  19. * registering SocketIO instance
  20. * @param connection
  21. * @param options
  22. */
  23. connect(connection, options) {
  24. if (connection && typeof connection === "object") {
  25. Logger.info(`Received socket.io-client instance`);
  26. return connection;
  27. } else if (typeof connection === "string") {
  28. const io = SocketIO(connection, options);// 其实用的是socket.io-client的Manager API
  29. Logger.info(`Received connection string`);
  30. return (this.io = io);
  31. } else {
  32. throw new Error("Unsupported connection type");
  33. }
  34. }

socket.io-client(index.js)

  1. var url = require(\'./url\');
  2. function lookup (uri, opts) {
  3. if (typeof uri === \'object\') {
  4. opts = uri;
  5. uri = undefined;
  6. }
  7. opts = opts || {};
  8. var parsed = url(uri); // 通过url.js对connection前缀进行截取
  9. var source = parsed.source;
  10. var id = parsed.id;
  11. var path = parsed.path;
  12. var sameNamespace = cache[id] && path in cache[id].nsps;
  13. var newConnection = opts.forceNew || opts[\'force new connection\'] ||
  14. false === opts.multiplex || sameNamespace;
  15. var io;
  16. if (newConnection) {
  17. debug(\'ignoring socket cache for %s\', source);
  18. io = Manager(source, opts);
  19. } else {
  20. if (!cache[id]) {
  21. debug(\'new io instance for %s\', source);
  22. cache[id] = Manager(source, opts);
  23. }
  24. io = cache[id];
  25. }
  26. if (parsed.query && !opts.query) {
  27. opts.query = parsed.query;
  28. }
  29. return io.socket(parsed.path, opts);// 实际调用的是解析后的前缀地址
  30. }
  • options.path: 这里就可以填websocket连接地址的后缀,如果不填会被默认添加/socket.io,关键源码如下(只看我写中文备注的部分就好):
    其他的options配置可以参见https://socket.io/docs/client-api/#Manager

socket.io-client(manager.js)

  1. function Manager (uri, opts) {
  2. if (!(this instanceof Manager)) return new Manager(uri, opts);
  3. if (uri && (\'object\' === typeof uri)) {
  4. opts = uri;
  5. uri = undefined;
  6. }
  7. opts = opts || {};
  8. opts.path = opts.path || \'/socket.io\'; // 看到没有,如果你不传递options.path参数的话会被默认安一个尾巴"/socket.io"
  9. this.nsps = {};
  10. this.subs = [];
  11. this.opts = opts;
  12. this.reconnection(opts.reconnection !== false);
  13. this.reconnectionAttempts(opts.reconnectionAttempts || Infinity);
  14. this.reconnectionDelay(opts.reconnectionDelay || 1000);
  15. this.reconnectionDelayMax(opts.reconnectionDelayMax || 5000);
  16. this.randomizationFactor(opts.randomizationFactor || 0.5);
  17. this.backoff = new Backoff({
  18. min: this.reconnectionDelay(),
  19. max: this.reconnectionDelayMax(),
  20. jitter: this.randomizationFactor()
  21. });
  22. this.timeout(null == opts.timeout ? 20000 : opts.timeout);
  23. this.readyState = \'closed\';
  24. this.uri = uri;
  25. this.connecting = [];
  26. this.lastPing = null;
  27. this.encoding = false;
  28. this.packetBuffer = [];
  29. var _parser = opts.parser || parser;
  30. this.encoder = new _parser.Encoder();
  31. this.decoder = new _parser.Decoder();
  32. this.autoConnect = opts.autoConnect !== false;
  33. if (this.autoConnect) this.open();
  34. }
  • vuex: 配置后可以在store.jsmutations或者actions监听到Vue-Socket.io事件(例如:connect、disconnect、reconnect等),这部分目前用得比较少,也挺简单,如果有疑问可以给我留言我再单独提供教程。

注意:熟悉socket.io-client的应该知道,默认情况下,websocket在创建实例的时候就会自动发起连接了,所以切记不要在组件中重复发起连接。如果你想自己控制发起连接的时机可以将options.autoConnect设置为false

  1. export default {
  2. name: \'Page\',
  3. sockets: {// 通过vue实例对象sockets实现组件中的事件监听
  4. connect: function () {// socket的connect事件
  5. console.log(\'socket connected from Page\')
  6. },
  7. STREAM_STATUS(data) {// 后端按主题名推送的消息数据
  8. console.log(\'Page\' + data)
  9. }
  10. },
  11. mounted() {
  12. console.log(\'page mounted\')
  13. this.$socket.emit(\'STREAM_STATUS\', { subscribe: true })// 在页面加载时发起订阅,“STREAM_STATUS”是你跟后端约定好的主题名
  14. }
  15. }

事件除了在sockets对象中默认监听,你还可以在外部单独注册事件监听或取消注册:

  1. this.sockets.subscribe(\'EVENT_NAME\', (data) => {
  2. this.msg = data.message;
  3. });
  4. this.sockets.unsubscribe(\'EVENT_NAME\');

但这种方式从源码上看是不支持参数传递的,只支持传递事件名及回调函数(部分源码如下):

vue-Socket.io(mixin.js)

  1. beforeCreate(){
  2. if(!this.sockets) this.sockets = {};
  3. if (typeof this.$vueSocketIo === \'object\') {
  4. for (const namespace of Object.keys(this.$vueSocketIo)) {
  5. this.sockets[namespace] = {
  6. subscribe: (event, callback) => {
  7. this.$vueSocketIo[namespace].emitter.addListener(event, callback, this);
  8. },
  9. unsubscribe: (event) => {
  10. this.$vueSocketIo[namespace].emitter.removeListener(event, this);
  11. }
  12. }
  13. }
  14. } else {
  15. this.$vueSocketIo.emitter.addListener(event, callback, this);
  16. this.$vueSocketIo.emitter.removeListener(event, this);
  17. }
  18. }

针对我上面描述的问题,最大原因就在于获取socket连接地址是异步请求,如文章开头的截图,page mounted打印时,this.$socket还是undefined。所以我们要做的就是怎么样让页面加载在VueSocketIO实例创建之后。
我提供两种解决方案,具体怎么选择看你们的需求~

缺点:如果你获取socket地址的请求失败了,整个项目的页面都加载不出来(一般服务器出现问题才会有这种情况产生)
优点:实现简单,一小段代码挪个位置就好

  1. import Vue from \'vue\'
  2. import App from \'./App.vue\'
  3. import router from \'./router\'
  4. import store from \'./store\'
  5. import ParentApi from \'@/api/Parent\'
  6. import VueSocketIO from \'vue-socket.io\'
  7. /* 使用vue-socket.io */
  8. ParentApi.getSocketUrl().then((res) => {
  9. Vue.use(new VueSocketIO({
  10. debug: false,
  11. connection: res.data.path,
  12. options: { path: \'/my-project/socket.io\' }
  13. }))
  14. new Vue({
  15. router,
  16. store,
  17. render: h => h(App)
  18. }).$mount(\'#app\')
  19. })

控制台打印如下图:

原理:异步请求回调中创建VueSocketIO实例并监听connect事件,监听回调中修改isSuccessConnect参数的值,在Page页面路由中增加beforeEnter守卫,利用setInterval周期性判断isSuccessConnect的值,满足条件则取消定时执行并路由跳转。
缺点:实现起来稍微复杂一点
优点:不会影响其他页面的加载

main.js
  1. import Vue from \'vue\'
  2. import App from \'./App.vue\'
  3. import router from \'./router\'
  4. import store from \'./store\'
  5. import ParentApi from \'@/api/Parent\'
  6. import VueSocketIO from \'vue-socket.io\'
  7. ParentApi.getSocketUrl().then((res) => {
  8. let vueSocketIo = new VueSocketIO({
  9. debug: false,
  10. connection: res.data.path,
  11. options: { path: \'/my-project/socket.io\' }
  12. })
  13. // 监听connect事件,设置isSuccessConnect为true
  14. vueSocketIo.io.on(\'connect\', () => {
  15. console.log(\'socket connect from main.js\')
  16. store.commit(\'newIsSuccessConnect\', true)
  17. })
  18. Vue.use(vueSocketIo)
  19. })
  20. new Vue({
  21. router,
  22. store,
  23. render: h => h(App)
  24. }).$mount(\'#app\')
  1. import Vue from \'vue\'
  2. import Vuex from \'vuex\'
  3. Vue.use(Vuex)
  4. export default new Vuex.Store({
  5. state: {
  6. // socket连接状态
  7. isSuccessConnect: false
  8. },
  9. mutations: {
  10. newIsSuccessConnect(state, value) {
  11. state.isSuccessConnect = value
  12. }
  13. },
  14. getters: {
  15. getIsSuccessConnect: state => {
  16. return state.isSuccessConnect
  17. }
  18. },
  19. actions: {
  20. }
  21. })
  1. import Vue from \'vue\'
  2. import Router from \'vue-router\'
  3. import store from \'./store\'
  4. Vue.use(Router)
  5. export default new Router({
  6. mode: \'history\',
  7. base: process.env.BASE_URL,
  8. routes: [
  9. {
  10. path: \'/page\',
  11. name: \'Page\',
  12. component: () => import(/* webpackChunkName: "Page" */ \'./pages/Page.vue\'),
  13. beforeEnter: (to, from, next) => {
  14. let intervalId = setInterval(() => {
  15. // 直到store中isSuccessConnect为true时才能进入/page
  16. if (store.getters.getIsSuccessConnect) {
  17. clearInterval(intervalId)
  18. next()
  19. }
  20. }, 500)
  21. }
  22. }
  23. ]
  24. })

控制台打印如下图:

1、vue-socket.io:https://github.com/MetinSeylan/Vue-Socket.io
2、socket.io-client:https://github.com/socketio/socket.io-client
3、vue-router守卫:https://router.vuejs.org/zh/guide/advanced/navigation-guards.html#%E8%B7%AF%E7%94%B1%E7%8B%AC%E4%BA%AB%E7%9A%84%E5%AE%88%E5%8D%AB

版权声明:本文为dreamsqin原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/dreamsqin/p/12018866.html