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 谢谢你的阅读 💝