前端:
Vue
+element
javascript項目爲先後端分離項目,經過
Ajax
交換數據。前端
今天在檢查代碼的時候發現了一個平時都忽略的問題,就是在組件使用vuex數據時,組件使用都是同步取的
vuex
值。關於vuex
的使用能夠查看官網文檔:vuex.vuejs.org/zh/ ,若是咱們須要的vuex
裏面的值是異步更新獲取的,在網絡和後臺請求特別快的狀況下不會有什麼問題。可是網絡慢或者後臺數據返回較慢的狀況下問題就來了。vue
${app}
表明你的項目根目錄,項目目錄結構同大部分Vue
項目。java
我須要實現這樣一個效果,我須要在
foo.vue
,bar.vue
,兩個不一樣的頁面創建一個使用相同信息的socket
鏈接,當我離開foo.vue
頁面的時候斷開鏈接,在bar.vue
頁面的時候從新鏈接。並且個人socket鏈接信息(鏈接地址,端口等)來自於接口請求。web
在
App.vue
初始化的時候dispatch
一個action
去獲取socket
的鏈接信息,而後在foo.vue
或者bar.vue
頁面mounted
的時候進行鏈接。vuex
${app}/src/store/index.js
segmentfault
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
後端
<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
api
<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
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
<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
<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
字段(後臺開發更新字段很常見){ "serverName":"msgServer1", "host":"192.168.0.2", } 複製代碼
我本身的
socketInfo對象
{ "serverName":"", "host":"", "port":"8080" } 複製代碼
假如我在初始化
state
的時候指定一個默認的端口,Object.assign()
合併的對象,只會合併我沒有的,而且更新與我socketInfo
鍵值對相同的鍵的值,這樣個人socketInfo
對象依然是有一個默認的端口,更新後爲{ "serverName":"msgServer1", "host":"192.168.0.2", "port":"8080" } 複製代碼
個人
socket
依然可以鏈接上。不至於報錯。回到以前的問題,若是咱們監聽的是$store.state.socketInfo
,這是個引用類型的對象,你會發現watch
不會執行,由於你的對象沒有改變。關於
JavaScript
引用數據類型和基礎數據類型能夠查看:developer.mozilla.org/zh-CN/docs/…
目前看來完成個人需求是不會有什麼問題了。可是這樣是完美的了嗎?
若是個人
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>
複製代碼
記錄下本身平時解決問題的思考方式和解決方案。
本文章代碼僅用工具檢查語法錯誤,純手寫,並未實際運行,不保證邏輯合理,若是你有更好的方案,歡迎你和我討論。
有問題纔有更好的解決方案。謝謝你的閱讀。