Vue組件優雅的使用Vuex異步數據

Vue組件優雅的使用Vuex異步數據

前端: Vue+ element

項目爲先後端分離項目,經過Ajax交換數據。javascript

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.vuevue

<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.vuejava

<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或者網絡請求慢的狀況下,個人 vuexstore還未更新,也就是 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.vueweb

<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.vuevuex

<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

簡單易懂的:http://www.javashuo.com/article/p-xurvpinp-v.html

❓ 思考新的問題

目前看來完成個人需求是不會有什麼問題了。可是這樣是完美的了嗎?

若是個人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>

✅ 優化代碼

vuexsocketInfo對象加一個 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對象的 isUpdatedtrue,能夠直接使用,不會去發送新的請求。

${app}/src/App.vue

<template>
  <!-- App -->
  <div id="app"></div>
</template>

<script>
export default {
  name: 'App',
}
</script>

0x3 總結

記錄下本身平時解決問題的思考方式和解決方案。

本文章代碼僅用工具檢查語法錯誤,純手寫,並未實際運行,不保證邏輯合理,若是你有更好的方案,歡迎你和我討論。

有問題纔有更好的解決方案。謝謝你的閱讀。

0x4 謝謝你的閱讀 💝

相關文章
相關標籤/搜索