前端:Vue
+element
項目爲先後端分離項目,經過
Ajax
交換數據。javascript
今天在檢查代碼的時候發現了一個平時都忽略的問題,就是在組件使用vuex數據時,組件使用都是同步取的vuex
值。關於vuex
的使用能夠查看官網文檔: https://vuex.vuejs.org/zh/ ,若是咱們須要的vuex
裏面的值是異步更新獲取的,在網絡和後臺請求特別快的狀況下不會有什麼問題。可是網絡慢或者後臺數據返回較慢的狀況下問題就來了。
${app}
表明你的項目根目錄,項目目錄結構同大部分Vue
項目。
我須要實現這樣一個效果,我須要在foo.vue
,bar.vue
,兩個不一樣的頁面創建一個使用相同信息的socket
鏈接,當我離開foo.vue
頁面的時候斷開鏈接,在bar.vue
頁面的時候從新鏈接。並且個人socket鏈接信息(鏈接地址,端口等)來自於接口請求。
在App.vue
初始化的時候dispatch
一個action
去獲取socket
的鏈接信息,而後在foo.vue
或者bar.vue
頁面mounted
的時候進行鏈接。
${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}/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>
${app}/src/views/foo/foo.vue
java
<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
代碼變成了以下這樣
${app}/src/views/foo/foo.vue
web
<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
頁面的代碼變成了下面這樣
${app}/src/views/foo/foo.vue
vuex
<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
目前看來完成個人需求是不會有什麼問題了。可是這樣是完美的了嗎?若是個人
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>
記錄下本身平時解決問題的思考方式和解決方案。本文章代碼僅用工具檢查語法錯誤,純手寫,並未實際運行,不保證邏輯合理,若是你有更好的方案,歡迎你和我討論。
有問題纔有更好的解決方案。謝謝你的閱讀。