前端:Vue
+element
项目为先后端分离项目,经过
Ajax
交换数据。javascript
今天在检查代码的时候发现了一个平时都忽略的问题,就是在组件使用vuex数据时,组件使用都是同步取的vuex
值。关于vuex
的使用能够查看官网文档: https://vuex.vuejs.org/zh/ ,若是咱们须要的vuex
里面的值是异步更新获取的,在网络和后台请求特别快的状况下不会有什么问题。可是网络慢或者后台数据返回较慢的状况下问题就来了。
${app}
表明你的项目根目录,项目目录结构同大部分Vue
项目。
我须要实现这样一个效果,我须要在foo.vue
,bar.vue
,两个不一样的页面创建一个使用相同信息的socket
链接,当我离开foo.vue
页面的时候断开链接,在bar.vue
页面的时候从新链接。并且个人socket链接信息(链接地址,端口等)来自于接口请求。
在App.vue
初始化的时候dispatch
一个action
去获取socket
的链接信息,而后在foo.vue
或者bar.vue
页面mounted
的时候进行链接。
${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: 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}/src/App.vue
vue
<template> <!-- App --> <div id="app"></div> </template> <script> export default { name: 'App', mounted() { // Get socket info this.$store.dispatch('GET_SOCKET_INFO') } } </script>
${app}/src/views/foo/foo.vue
java
<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或者网络请求慢的状况下,个人vuex
的store
还未更新,也就是App.vue
的请求还未回来,这个时候foo.vue
页面的mounted
生命周期函数已经执行,很显然,我须要的socket
链接信息拿不到,这个时候控制台就会飘红。
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
代码变成了以下这样
${app}/src/views/foo/foo.vue
web
<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
页面的代码变成了下面这样
${app}/src/views/foo/foo.vue
vuex
<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
字段(后台开发更新字段很常见)segmentfault{ "serverName":"msgServer1", "host":"192.168.0.2", }我本身的
socketInfo对象
后端{ "serverName":"", "host":"", "port":"8080" }假如我在初始化
state
的时候指定一个默认的端口,Object.assign()
合并的对象,只会合并我没有的,而且更新与我socketInfo
键值对相同的键的值,这样个人socketInfo
对象依然是有一个默认的端口,更新后为api{ "serverName":"msgServer1", "host":"192.168.0.2", "port":"8080" }个人
socket
依然可以链接上。不至于报错。回到以前的问题,若是咱们监听的是$store.state.socketInfo
,这是个引用类型的对象,你会发现watch
不会执行,由于你的对象没有改变。websocket关于
JavaScript
引用数据类型和基础数据类型能够查看:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Grammar_and_types
目前看来完成个人需求是不会有什么问题了。可是这样是完美的了吗?若是个人
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>
在vuex
的socketInfo
对象加一个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
对象的isUpdated
为true
,能够直接使用,不会去发送新的请求。
${app}/src/App.vue
<template> <!-- App --> <div id="app"></div> </template> <script> export default { name: 'App', } </script>
记录下本身平时解决问题的思考方式和解决方案。本文章代码仅用工具检查语法错误,纯手写,并未实际运行,不保证逻辑合理,若是你有更好的方案,欢迎你和我讨论。
有问题才有更好的解决方案。谢谢你的阅读。