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

news/2024/7/10 0:58:12 标签: vue, javascript

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

前端:Vue+element

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

更新时间:2020-09-10 19:11:42

  • 2020-09-10 19:11:42

    • 拆分 vue 文件支持代码高亮
  • 2020-05-23 22:02:45

    • 更新判断数据的方式在文章更新方案

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

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)
      }
    }
  }
})

vue_88">App.vue

${app}/src/App.vue

<!-- App -->
<div id="app"></div>
javascript">export default {
  name: 'App',
  mounted() {
    // Get socket info
    this.$store.dispatch('GET_SOCKET_INFO')
  }
}

vue_109">foo.vue

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

javascript">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']
    })
  }
}

❓ 问题

问题很显而易见,当我直接访问foo.vue页面的时候,如果我的后台api或者网络请求慢的情况下,我的vuexstore还未更新,也就是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代码变成了如下这样

vue_145">foo.vue

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

javascript">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)
    }
  }
}

❓ 新的问题

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

✅ 第二次解决

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

vue_195">foo.vue

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

javascript">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']
      })
    }
  }
}

这里为啥监听的是$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://segmentfault.com/a/1190000008472264

❓ 思考新的问题

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

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

✅ 最佳方案?

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

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

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

<div>
  <!-- 我的网站名 -->
  <div v-if="$store.state.webConfig.webName">{{ $store.state.webConfig.webName }}</div>
  <!-- 骨架屏 -->
  <skeleton v-else></skeleton>
</div>
javascript">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']
      })
    }
  }
}

✅ 优化代码

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

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

<div>
  <!-- 我的网站名 -->
  <div v-if="webConfig.isUpdated">
    {{ webConfig.webName }}
  </div>
  <!-- 骨架屏 -->
  <skeleton v-else></skeleton>
</div>
javascript">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)
      }
    }
  }
}

${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对象的isUpdatedtrue,可以直接使用,不会去发送新的请求。

${app}/src/App.vue

<!-- App -->
<div id="app"></div>
javascript">export default {
  name: 'App',
}

🤗更新方案

既然是进入页面之后可以判断数据是否加载完毕,我们也可以直接在页面进入之前,通过路由元信息配置该页面需要的全局异步数据,然后通过路由跳转的守卫去拉取异步数据(全局公用的异步数据只需要加载一遍就行,如果加载失败我们可以跳转到服务器错误页面)。完了再显示我们的页面,同时使用Promise.all来进行多个异步数据的读取。nice~!上代码

${app}/src/utils/permission.js

这个文件用来做路由拦截。

javascript">/**
 * @name Global router permission controller
 * @description Do not delete comments
 * @author SunSeekerX
 * @time 2019-08-20 11:14:34
 * @LastEditors: SunSeekerX
 * @LastEditTime: 2020-05-21 15:28:39
 */

import NProgress from 'nprogress' // progress bar
import 'nprogress/nprogress.css' // progress bar style
NProgress.configure({ showSpinner: false }) // NProgress Configuration

import router from '@/router'
import store from '@/store'
import { i18n } from '@/lang/index'
import { NotifyFun, handleApiRequestException } from '@/utils/handle-error'

router.beforeEach(async (to, from, next) => {
  // 启动进度条
  NProgress.start()
  
  // 公用vuex数据
  const {
    siteConfig,
    appConfig,
    socketInfo,
    coinDecimal,
  } = store.state.appSessionStatus

  try {
    // 异步任务列表
    const task = []
    // 站点信息
    if (!siteConfig.isUpdated) {
      task.push(store.dispatch('GET_SITE_CONFIG'))
    }
    // 站点配置
    if (!appConfig.isUpdated) {
      task.push(store.dispatch('GET_APP_CONFIG'))
    }

    /**
     * @name 检查前去的页面需要的公用数据是否加载
     */
    if (to.meta.isUsingCoinDecimal && !coinDecimal.isUpdated) {
      // 需要全局小数点位数
      task.push(store.dispatch('GET_COIN_DECIMAL'))
    }

    if (to.meta.isUsingSocketInfo && !socketInfo.isUpdated) {
      // 需要全局socket链接信息
      task.push(store.dispatch('GET_SOCKET_INFO'))
    }

    // 异步同时执行请求任务
    await Promise.all(task)
  } catch (error) {
    // 提示错误
    handleApiRequestException(error)

    // 显示网络错误白屏图
    store.commit('UPDATE_SERVER_ERROR', true)

    // 请求失败,路由导航终止
    return next(false)
  } finally {
    NProgress.done()
  }

  // Permission
  if (store.state.token) {
    // Has login
    next()
  } else {
    // 判断是否是公开页面
    if (to.meta.isPublic) {
      next()
    } else {
      // Redirect to login
      next('/user/user-login')
    }
  }
})

router.afterEach(() => {
  // finish progress bar
  NProgress.done()
})

路由配置

${app}/src/router/Exchange.js

javascript">/**
 * @name Exchange.js
 * @author SunSeekerX
 * @time 2019-09-21 10:51:25
 * @LastEditors: SunSeekerX
 * @LastEditTime: 2020-05-21 14:59:37
 */

import { i18n } from '@/lang/index'

export default [
  // 币币交易》首页
  {
    path: '/exchange',
    name: 'ExchangeIndex',
    component: () => import('@/views/exchange/index/index'),
    meta: {
      title: i18n.t('Title_Exchange'),
      // 代表路由权限公开
      isPublic: true,
      // 需要socketInfo信息
      isUsingSocketInfo: true,
      // 需要coinDecimal信息
      isUsingCoinDecimal: true,
    },
  },
]

0x3 总结

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

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

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

0x4 谢谢你的阅读 💝

关于我

SunSeekerX,前端开发、Nodejs开发、小程序、uni-app开发、等等

喜欢探讨技术实现方案和细节,完美主义者,见不得bug

Github:https://github.com/SunSeekerX

个人博客:https://yoouu.cn/

个人在线笔记:https://sunseekerx.yoouu.cn/


http://www.niftyadmin.cn/n/1332123.html

相关文章

SQL查找是否存在,别再count了,很耗费时间的

点击上方 果汁简历 &#xff0c;选择“置顶公众号”优质文章&#xff0c;第一时间送达作者&#xff1a;程序猿囧途链接&#xff1a;http://suo.im/5T1tGv根据某一条件从数据库表中查询 『有』与『没有』&#xff0c;只有两种状态&#xff0c;那为什么在写 SQL 的时候&#xff0…

ECMAScript 2020(ES2020)的新增语法

ECMAScript 2020&#xff08;ES2020&#xff09;的新增语法 是时候对不断变化的艺术作品(即JavaScript)进行另一个更新了。在本文中&#xff0c;我们将回顾ES2020的一些最新和最强大的功能。 安装 自从很多人不认为更新他们的浏览器是为了让他们的开发工作更简单&#xff0c;我…

Jenkins+vue项目自动化构建部署

Jenkins 更新地址&#xff1a;https://sunseekerx.yoouu.cn/common/jenkins.html 更新时间&#xff1a;2020-05-05 20:39:33 Vue项目自动化构建 Jenkins安装查看Docker章节。 Jenkins第一次安装完成最好重启一下&#xff0c;因为如果安装了语言包有些地方还是英文的。应该是个…

同事写了一个update,误用一个双引号,生产数据全变0了!

来源&#xff1a;fordba.com/mysql-double-quotation-marks-accident.html一、前言最近经常碰到开发误删除误更新数据&#xff0c;这不&#xff0c;他们又给我找了个麻烦&#xff0c;我们来看下整个过程。二、过程由于开发需要在生产环节中修复数据&#xff0c;需要执行120条SQ…

如何自动加载网站的深色模式?

如何自动加载网站的深色模式&#xff1f;&#x1f319; 本文翻译自&#xff1a;Going dark (Web Edition) &#x1f319; 来源&#xff1a;科技爱好者周刊&#xff1a;第 107 期 - 阮一峰 时间&#xff1a;2020-05-15 23:54:03 深色模式时代已经来临 现在有一种新的趋势 - 把东…

还在用 Guava Cache?它才是 Java 本地缓存之王!

点击上方 果汁简历 &#xff0c;选择“置顶公众号”优质文章&#xff0c;第一时间送达Guava Cache 的优点是封装了get&#xff0c;put操作&#xff1b;提供线程安全的缓存操作&#xff1b;提供过期策略&#xff1b;提供回收策略&#xff1b;缓存监控。当缓存的数据超过最大值时…

uni-app 配置 eslint + prettier + stylelint + lint-staged + husky 代码格式校验

0x1 问题 更新时间&#xff1a;2021-01-13 19:10:25 2021-01-13 19:10:25 stylelint --fix --allow-empty-input 增加 --allow-empty-input 参数防止意外提交卡顿。 2020-11-13 14:30:02 增加 stylelint css 代码校验&#xff0c;增加 package.json 校验命令 改变配置文件名符…

关于Vue Loading chunk {n} failed的一些思考

关于Vue Loading chunk {n} failed的一些思考 更新时间&#xff1a;2020-06-02 14:52:43 0x1 问题 测试偶尔会出现这个问题&#xff0c;遂提出一个bug&#xff0c;从bug字面意思来看&#xff0c;有一个js的块加载失败了。丢失了一个${domain}/js/chunk-${hash}.js。 为什么会丢…