Vue组件优雅的使用Vuex异步数据
# Vue组件优雅的使用Vuex异步数据
> 前端:`Vue`+`element`
>
> 项目为前后端分离项目,通过`Ajax`交换数据。
## 0x1 缘起
> 今天在检查代码的时候发现了一个平时都忽略的问题,就是在组件使用vuex数据时,组件使用都是同步取的`vuex`值。关于`vuex`的使用可以查看官网文档:[https://vuex.vuejs.org/zh/](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`
```javascript
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`
```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`
```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`连接信息拿不到,这个时候控制台就会飘红。
```javascript
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`
```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`
```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`字段(后台开发更新字段很常见)
>
> ```javascript
> {
> "serverName":"msgServer1",
> "host":"192.168.0.2",
> }
> ```
>
> 我自己的`socketInfo对象`
>
> ```javascript
> {
> "serverName":"",
> "host":"",
> "port":"8080"
> }
> ```
>
> 假如我在初始化`state`的时候指定一个默认的端口,`Object.assign()`合并的对象,只会合并我没有的,并且更新与我`socketInfo`键值对相同的键的值,这样我的`socketInfo`对象依然是有一个默认的端口,更新后为
>
> ```javascript
> {
> "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](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Grammar_and_types)
>
> 简单易懂的:[https://segmentfault.com/a/1190000008472264](https://segmentfault.com/a/1190000008472264)
### ❓ 思考新的问题
> 目前看来完成我的需求是不会有什么问题了。但是这样是完美的了吗?
>
> 如果我的`foo.vue`页面不只是创建连接的时候需要取`vuex`的数据,我在页面渲染的时候,也需要`vuex`里面的数据。比如我的`foo.vue`,和`bar.vue`都需要显示我的网站名,网站名是通过接口拉取存在`vuex`的。这个时候怎么办呢?,刚刚解决上面问题的办法就无能为力了。毕竟`mounted`不能阻止页面渲染。
### ✅ 最佳方案?
> 借用`watch`的方案,我在页面判断一下`vuex`的值是否更新,然后再渲染不就ok了嘛?这也是很多网站骨架屏渲染的使用场景。
>
> 很多网站在刚刚打开的一刻,数据未准备好的时候是会显示一个骨架加载的动画,等到加载完毕再把内容呈现给用户。看代码
`${app}/src/views/foo/foo.vue`
```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`
```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`
```javascript
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`
```vue
<template>
<!-- App -->
<div id="app"></div>
</template>
<script>
export default {
name: 'App',
}
</script>
```
## 0x3 总结
> 记录下自己平时解决问题的思考方式和解决方案。
>
> 本文章代码仅用工具检查语法错误,纯手写,并未实际运行,不保证逻辑合理,如果你有更好的方案,欢迎你和我讨论。
>
> 有问题才有更好的解决方案。谢谢你的阅读。
## 0x4 谢谢你的阅读 💝