Vue组件优雅的使用Vuex异步数据
前端:
Vue
+element
项目为前后端分离项目,通过
Ajax
交换数据。
0x1 缘起
今天在检查代码的时候发现了一个平时都忽略的问题,就是在组件使用vuex数据时,组件使用都是同步取的
vuex
值。关于vuex
的使用可以查看官网文档:https://vuex.vuejs.org/zh/ ,如果我们需要的vuex
里面的值是异步更新获取的,在网络和后台请求特别快的情况下不会有什么问题。但是网络慢或者后台数据返回较慢的情况下问题就来了。
0x2 案例
${app}
代表你的项目根目录,项目目录结构同大部分Vue
项目。
需求
我需要实现这样一个效果,我需要在
foo.vue
,bar.vue
,两个不同的页面建立一个使用相同信息的socket
连接,当我离开foo.vue
页面的时候断开连接,在bar.vue
页面的时候重新连接。而且我的socket连接信息(连接地址,端口等)来自于接口请求。
初次实现
在
App.vue
初始化的时候dispatch
一个action
去获取socket
的连接信息,然后在foo.vue
或者bar.vue
页面mounted
的时候进行连接。
Vuex
${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.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.vue
<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
代码变成了如下这样
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
引用数据类型和基础数据类型可以查看: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>
0x3 总结
记录下自己平时解决问题的思考方式和解决方案。
本文章代码仅用工具检查语法错误,纯手写,并未实际运行,不保证逻辑合理,如果你有更好的方案,欢迎你和我讨论。
有问题才有更好的解决方案。谢谢你的阅读。
0x4 谢谢你的阅读 ?
- 文章来源:思否
- 版权声明:文章来源于网络采集,版权归原创者所有,均已注明来源,如有侵权请联系管理员删除。