Vue组件优雅的使用Vuex异步数据

Vue组件优雅的使用Vuex异步数据

前端:Vue+elementjavascript

项目为先后端分离项目,经过Ajax交换数据。前端

0x1 缘起

今天在检查代码的时候发现了一个平时都忽略的问题,就是在组件使用vuex数据时,组件使用都是同步取的vuex值。关于vuex的使用能够查看官网文档:vuex.vuejs.org/zh/ ,若是咱们须要的vuex里面的值是异步更新获取的,在网络和后台请求特别快的状况下不会有什么问题。可是网络慢或者后台数据返回较慢的状况下问题就来了。vue

0x2 案例

${app}表明你的项目根目录,项目目录结构同大部分Vue项目。java

需求

我须要实现这样一个效果,我须要在foo.vue,bar.vue,两个不一样的页面创建一个使用相同信息的socket链接,当我离开foo.vue页面的时候断开链接,在bar.vue页面的时候从新链接。并且个人socket链接信息(链接地址,端口等)来自于接口请求。web

初次实现

App.vue初始化的时候dispatch一个action去获取socket的链接信息,而后在foo.vue或者bar.vue页面mounted的时候进行链接。vuex

Vuex

${app}/src/store/index.jssegmentfault

import Vue from 'vue'
import Vuex from 'vuex'

import api from '@/apis'
import handleError from '@/utils/HandleError'

Vue.use(Vuex)

export default new Vuex.Store({
  strict: process.env.NODE_ENV !== 'production',
  state: {
    socketInfo: {
      serverName: '',
      host: '',
      port: 8080
    }
  },
  mutations: {
    // Update token
    UPDATE_SOCKET_INFO(state, { socketInfo }) {
      // state.socketInfo = socketInfo
      // Update vuex token
      Object.assign(state.socketInfo, socketInfo)
    }
  },
  actions: {
    // Get socket info
    async GET_SOCKET_INFO({ commit }) {
      // Rquest socket info
      try {
        const res = await api.Common.getSocketUrl()
        // Success
        if (res.success) {
          commit('UPDATE_SOCKET_INFO', {
            socketInfo: res.obj
          })
        }
      } catch (e) {
        // Handle api request exception
        handleError.handleApiRequestException(e)
      }
    }
  }
})
复制代码

App.vue

${app}/src/App.vue后端

<template>
  <!-- App -->
  <div id="app"></div>
</template>

<script>
export default {
  name: 'App',
  mounted() {
    // Get socket info
    this.$store.dispatch('GET_SOCKET_INFO')
  }
}
</script>
复制代码

foo.vue

${app}/src/views/foo/foo.vueapi

<template> </template>

<script>
import io from 'socket.io-client'
export default {
  name: 'Foo',
  mounted() {
    const { serverName, host, port } = this.$store.state.socketInfo
    const socket = io(`ws://${host}:${port}`, {
      path: `/${serverName}`,
      transports: ['websocket', 'polling']
    })
  }
}
</script>
复制代码

❓ 问题

问题很显而易见,当我直接访问foo.vue页面的时候,若是个人后台api或者网络请求慢的状况下,个人vuexstore还未更新,也就是App.vue的请求还未回来,这个时候foo.vue页面的mounted生命周期函数已经执行,很显然,我须要的socket链接信息拿不到,这个时候控制台就会飘红。websocket

WebSocket connection to 'ws://%27%27/''/?EIO=3&transport=websocket' failed: Error in connection establishment: net::ERR_NAME_NOT_RESOLVED
复制代码

✅ 第一次解决

既然是须要等到请求回来在链接,那么好办了,我在foo.vue页面也获取一次socket的链接信息获取成功了在进行链接,此时foo.vue代码变成了以下这样

foo.vue

${app}/src/views/foo/foo.vue

<template> </template>

<script>
import io from 'socket.io-client'

import api from '@/apis'
import handleError from '@/utils/HandleError'
export default {
  name: 'Foo',
  async mounted() {
    // Rquest socket info
    try {
      const res = await api.Common.getSocketUrl()
      // Success
      if (res.success) {
        commit('UPDATE_APP_SESSION_STATUS', {
          socketInfo: res.obj
        })

        // Connect to socket
        const { serverName, host, port } = this.$store.state.socketInfo
        const socket = io(`ws://${host}:${port}`, {
          path: `/${serverName}`,
          transports: ['websocket', 'polling']
        })
      }
    } catch (e) {
      // Handle api request exception
      handleError.handleApiRequestException(e)
    }
  }
}
</script>
复制代码

❓ 新的问题

上一个办法确实解决了问题,可是新的问题又来了,我发了两次请求,每一个页面都要写一个请求。仔细想一想这要是个十几二十个页面都要用的方法,那不得累死?有没有更好的解决办法呢?答案是有的。

✅ 第二次解决

既然我在foo.vue页面须要等待vuex的更新,那我监听一下socketInfo的更新,有更新我在链接,而后在mounted里面判断socketInfo是否有值再链接不就能够了吗。这个时候foo.vue页面的代码变成了下面这样

foo.vue

${app}/src/views/foo/foo.vue

<template> </template>

<script>
import io from 'socket.io-client'

import api from '@/apis'
import handleError from '@/utils/HandleError'
export default {
  name: 'Foo',
  async mounted() {
    if (this.$store.state.socketInfo.host) {
      // Handle create socket
      this.handleCreateSocket()
    }
  },
  watch: {
    '$store.state.socketInfo.host'() {
      if (this.$store.state.socketInfo.host) {
        // Handle create socket
        this.handleCreateSocket()
      }
    }
  },
  methods: {
    // Handle create socket
    handleCreateSocket() {
      // Connect to socket
      const { serverName, host, port } = this.$store.state.socketInfo
      const socket = io(`ws://${host}:${port}`, {
        path: `/${serverName}`,
        transports: ['websocket', 'polling']
      })
    }
  }
}
</script>
复制代码

这里为啥监听的是$store.state.socketInfo.host呢,由于咱们的mutations里面的UPDATE_SOCKET_INFO更新socketInfo的方式是Object.assign(),这种更新方式的好处是,若是api请求返回的字段是这样的一个对象,少了port字段(后台开发更新字段很常见)

{
    "serverName":"msgServer1",
    "host":"192.168.0.2",
}
复制代码

我本身的socketInfo对象

{
    "serverName":"",
    "host":"",
    "port":"8080"
}
复制代码

假如我在初始化state的时候指定一个默认的端口,Object.assign()合并的对象,只会合并我没有的,而且更新与我socketInfo键值对相同的键的值,这样个人socketInfo对象依然是有一个默认的端口,更新后为

{
    "serverName":"msgServer1",
    "host":"192.168.0.2",
    "port":"8080"
}
复制代码

个人socket依然可以链接上。不至于报错。回到以前的问题,若是咱们监听的是$store.state.socketInfo,这是个引用类型的对象,你会发现watch不会执行,由于你的对象没有改变。

关于JavaScript引用数据类型和基础数据类型能够查看:developer.mozilla.org/zh-CN/docs/…

简单易懂的:segmentfault.com/a/119000000…

❓ 思考新的问题

目前看来完成个人需求是不会有什么问题了。可是这样是完美的了吗?

若是个人foo.vue页面不仅是建立链接的时候须要取vuex的数据,我在页面渲染的时候,也须要vuex里面的数据。好比个人foo.vue,和bar.vue都须要显示个人网站名,网站名是经过接口拉取存在vuex的。这个时候怎么办呢?,刚刚解决上面问题的办法就无能为力了。毕竟mounted不能阻止页面渲染。

✅ 最佳方案?

借用watch的方案,我在页面判断一下vuex的值是否更新,而后再渲染不就ok了嘛?这也是不少网站骨架屏渲染的使用场景。

不少网站在刚刚打开的一刻,数据未准备好的时候是会显示一个骨架加载的动画,等到加载完毕再把内容呈现给用户。看代码

${app}/src/views/foo/foo.vue

<template>
  <div>
    <!-- 个人网站名 -->
    <div v-if="$store.state.webConfig.webName">{{ $store.state.webConfig.webName }}</div>
    <!-- 骨架屏 -->
    <skeleton v-else></skeleton>
  </div>
</template>

<script>
import io from 'socket.io-client'

import api from '@/apis'
import handleError from '@/utils/HandleError'
export default {
  name: 'Foo',
  async mounted() {
    if (this.$store.state.socketInfo.host) {
      // Handle create socket
      this.handleCreateSocket()
    }
  },
  watch: {
    '$store.state.socketInfo.host'() {
      if (this.$store.state.socketInfo.host) {
        // Handle create socket
        this.handleCreateSocket()
      }
    }
  },
  methods: {
    // Handle create socket
    handleCreateSocket() {
      // Connect to socket
      const { serverName, host, port } = this.$store.state.socketInfo
      const socket = io(`ws://${host}:${port}`, {
        path: `/${serverName}`,
        transports: ['websocket', 'polling']
      })
    }
  }
}
</script>
复制代码

✅ 优化代码

vuexsocketInfo对象加一个isUpdated字段,若是更新了,直接取值进行我须要的操做,没更新的话就行请求api更新。这是目前能想到的比较优雅的方案了。

${app}/src/views/foo/foo.vue

<template>
  <div>
    <!-- 个人网站名 -->
    <div v-if="webConfig.isUpdated">
      {{ webConfig.webName }}
    </div>
    <!-- 骨架屏 -->
    <skeleton v-else></skeleton>
  </div>
</template>

<script>
import io from 'socket.io-client'
import { mapState } from 'vuex'

import api from '@/apis'
import handleError from '@/utils/HandleError'
export default {
  name: 'Foo',
  computed: {
    ...mapState(['webConfig', 'socketInfo'])
  },
  async mounted() {
    // Handle get socket info
    this.handleGetSocketInfo()
  },
  methods: {
    // Handle create socket
    handleCreateSocket() {
      // Connect to socket
      const { serverName, host, port } = this.$store.state.socketInfo
      const socket = io(`ws://${host}:${port}`, {
        path: `/${serverName}`,
        transports: ['websocket', 'polling']
      })
    },
    // Handle get socket info
    handleGetSocketInfo() {
      if (this.socketInfo.isUpdated) {
        // Handle create socket
        this.handleCreateSocket()
      } else {
        this.$store.dispatch('GET_SOCKET_INFO', this.handleCreateSocket)
      }
    }
  }
}
</script>
复制代码

${app}/src/store/index.js

import Vue from 'vue'
import Vuex from 'vuex'

import api from '@/apis'
import handleError from '@/utils/HandleError'

Vue.use(Vuex)

export default new Vuex.Store({
  strict: process.env.NODE_ENV !== 'production',
  state: {
    socketInfo: {
      serverName: '',
      host: '',
      port: '',
      isUpdated: false
    },
    webConfig:{
      webName: '',
      isUpdated: false
    }
  },
  mutations: {
    // Update token
    UPDATE_SOCKET_INFO(state, { socketInfo }) {
      // state.socketInfo = socketInfo
      // Update vuex token
      Object.assign(
        state.socketInfo,
        {
          isUpdated: true
        },
        socketInfo
      )
    }
  },
  actions: {
    // Get socket info
    async GET_SOCKET_INFO({ commit }, callback) {
      // Rquest socket info
      try {
        const res = await api.Common.getSocketUrl()
        // Success
        if (res.success) {
          commit('UPDATE_SOCKET_INFO', {
            socketInfo: res.obj
          })
          // Call back you custom function
          if (callback) {
            callback()
          }
        }
      } catch (e) {
        // Handle api request exception
        handleError.handleApiRequestException(e)
      }
    }
  }
})
复制代码

因为在foo.vue页面须要使用数据的时候咱们才去请求数据,所以App.vue的请求能够取消,这样一来用户只是打开咱们的网站,并不会去请求无心义的数据。优化了后台的接口请求压力。同时在第一次进入foo.vue页面的时候已经请求了数据,若是用户没有刷新页面,再次访问该页面咱们的socketInfo对象的isUpdatedtrue,能够直接使用,不会去发送新的请求。

${app}/src/App.vue

<template>
  <!-- App -->
  <div id="app"></div>
</template>

<script>
export default {
  name: 'App',
}
</script>
复制代码

0x3 总结

记录下本身平时解决问题的思考方式和解决方案。

本文章代码仅用工具检查语法错误,纯手写,并未实际运行,不保证逻辑合理,若是你有更好的方案,欢迎你和我讨论。

有问题才有更好的解决方案。谢谢你的阅读。

0x4 谢谢你的阅读 💝

相关文章
相关标签/搜索