Vue組件優(yōu)雅的使用Vuex異步數(shù)據(jù)
前端:
Vue
+element
項(xiàng)目為前后端分離項(xiàng)目匆背,通過(guò)
Ajax
交換數(shù)據(jù)励饵。更新時(shí)間:2020-09-10 19:11:42
2020-09-10 19:11:42
- 拆分 vue 文件支持代碼高亮
2020-05-23 22:02:45
0x1 緣起
今天在檢查代碼的時(shí)候發(fā)現(xiàn)了一個(gè)平時(shí)都忽略的問(wèn)題捏顺,就是在組件使用vuex數(shù)據(jù)時(shí)涣脚,組件使用都是同步取的
vuex
值思杯。關(guān)于vuex
的使用可以查看官網(wǎng)文檔:https://vuex.vuejs.org/zh/ 贞铣,如果我們需要的vuex
里面的值是異步更新獲取的闹啦,在網(wǎng)絡(luò)和后臺(tái)請(qǐng)求特別快的情況下不會(huì)有什么問(wèn)題。但是網(wǎng)絡(luò)慢或者后臺(tái)數(shù)據(jù)返回較慢的情況下問(wèn)題就來(lái)了辕坝。
0x2 案例
${app}
代表你的項(xiàng)目根目錄窍奋,項(xiàng)目目錄結(jié)構(gòu)同大部分Vue
項(xiàng)目。
需求
我需要實(shí)現(xiàn)這樣一個(gè)效果酱畅,我需要在
foo.vue
,bar.vue
费变,兩個(gè)不同的頁(yè)面建立一個(gè)使用相同信息的socket
連接,當(dāng)我離開(kāi)foo.vue
頁(yè)面的時(shí)候斷開(kāi)連接圣贸,在bar.vue
頁(yè)面的時(shí)候重新連接挚歧。而且我的socket連接信息(連接地址,端口等)來(lái)自于接口請(qǐng)求吁峻。
初次實(shí)現(xiàn)
在
App.vue
初始化的時(shí)候dispatch
一個(gè)action
去獲取socket
的連接信息滑负,然后在foo.vue
或者bar.vue
頁(yè)面mounted
的時(shí)候進(jìn)行連接。
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.vue
<!-- App -->
<div id="app"></div>
export default {
name: 'App',
mounted() {
// Get socket info
this.$store.dispatch('GET_SOCKET_INFO')
}
}
foo.vue
${app}/src/views/foo/foo.vue
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']
})
}
}
? 問(wèn)題
問(wèn)題很顯而易見(jiàn)用含,當(dāng)我直接訪問(wèn)
foo.vue
頁(yè)面的時(shí)候矮慕,如果我的后臺(tái)api或者網(wǎng)絡(luò)請(qǐng)求慢的情況下,我的vuex
的store
還未更新啄骇,也就是App.vue
的請(qǐng)求還未回來(lái)痴鳄,這個(gè)時(shí)候foo.vue
頁(yè)面的mounted
生命周期函數(shù)已經(jīng)執(zhí)行,很顯然缸夹,我需要的socket
連接信息拿不到痪寻,這個(gè)時(shí)候控制臺(tái)就會(huì)飄紅。
WebSocket connection to 'ws://%27%27/''/?EIO=3&transport=websocket' failed: Error in connection establishment: net::ERR_NAME_NOT_RESOLVED
? 第一次解決
既然是需要等到請(qǐng)求回來(lái)在連接虽惭,那么好辦了橡类,我在
foo.vue
頁(yè)面也獲取一次socket
的連接信息獲取成功了在進(jìn)行連接,此時(shí)foo.vue
代碼變成了如下這樣
foo.vue
${app}/src/views/foo/foo.vue
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)
}
}
}
? 新的問(wèn)題
上一個(gè)辦法確實(shí)解決了問(wèn)題芽唇,但是新的問(wèn)題又來(lái)了顾画,我發(fā)了兩次請(qǐng)求,每個(gè)頁(yè)面都要寫(xiě)一個(gè)請(qǐng)求。仔細(xì)想想這要是個(gè)十幾二十個(gè)頁(yè)面都要用的方法研侣,那不得累死谱邪?有沒(méi)有更好的解決辦法呢?答案是有的庶诡。
? 第二次解決
既然我在
foo.vue
頁(yè)面需要等待vuex
的更新虾标,那我監(jiān)聽(tīng)一下socketInfo
的更新,有更新我在連接灌砖,然后在mounted
里面判斷socketInfo
是否有值再連接不就可以了嗎璧函。這個(gè)時(shí)候foo.vue
頁(yè)面的代碼變成了下面這樣
foo.vue
${app}/src/views/foo/foo.vue
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']
})
}
}
}
這里為啥監(jiān)聽(tīng)的是
$store.state.socketInfo.host
呢,因?yàn)槲覀兊?code>mutations里面的UPDATE_SOCKET_INFO
更新socketInfo
的方式是Object.assign()
基显,這種更新方式的好處是蘸吓,如果api
請(qǐng)求返回的字段是這樣的一個(gè)對(duì)象,少了port
字段(后臺(tái)開(kāi)發(fā)更新字段很常見(jiàn)){ "serverName":"msgServer1", "host":"192.168.0.2", }
我自己的
socketInfo對(duì)象
{ "serverName":"", "host":"", "port":"8080" }
假如我在初始化
state
的時(shí)候指定一個(gè)默認(rèn)的端口撩幽,Object.assign()
合并的對(duì)象库继,只會(huì)合并我沒(méi)有的,并且更新與我socketInfo
鍵值對(duì)相同的鍵的值窜醉,這樣我的socketInfo
對(duì)象依然是有一個(gè)默認(rèn)的端口宪萄,更新后為{ "serverName":"msgServer1", "host":"192.168.0.2", "port":"8080" }
我的
socket
依然能夠連接上。不至于報(bào)錯(cuò)榨惰“萦ⅲ回到之前的問(wèn)題,如果我們監(jiān)聽(tīng)的是$store.state.socketInfo
琅催,這是個(gè)引用類型的對(duì)象居凶,你會(huì)發(fā)現(xiàn)watch
不會(huì)執(zhí)行,因?yàn)槟愕膶?duì)象沒(méi)有改變藤抡。關(guān)于
JavaScript
引用數(shù)據(jù)類型和基礎(chǔ)數(shù)據(jù)類型可以查看:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Grammar_and_types簡(jiǎn)單易懂的:https://segmentfault.com/a/1190000008472264
? 思考新的問(wèn)題
目前看來(lái)完成我的需求是不會(huì)有什么問(wèn)題了侠碧。但是這樣是完美的了嗎?
如果我的
foo.vue
頁(yè)面不只是創(chuàng)建連接的時(shí)候需要取vuex
的數(shù)據(jù)缠黍,我在頁(yè)面渲染的時(shí)候弄兜,也需要vuex
里面的數(shù)據(jù)。比如我的foo.vue
瓷式,和bar.vue
都需要顯示我的網(wǎng)站名替饿,網(wǎng)站名是通過(guò)接口拉取存在vuex
的。這個(gè)時(shí)候怎么辦呢蒿往?盛垦,剛剛解決上面問(wèn)題的辦法就無(wú)能為力了湿弦。畢竟mounted
不能阻止頁(yè)面渲染瓤漏。
? 最佳方案?
借用
watch
的方案,我在頁(yè)面判斷一下vuex
的值是否更新蔬充,然后再渲染不就ok了嘛蝶俱?這也是很多網(wǎng)站骨架屏渲染的使用場(chǎng)景。很多網(wǎng)站在剛剛打開(kāi)的一刻饥漫,數(shù)據(jù)未準(zhǔn)備好的時(shí)候是會(huì)顯示一個(gè)骨架加載的動(dòng)畫(huà)榨呆,等到加載完畢再把內(nèi)容呈現(xiàn)給用戶∮苟樱看代碼
${app}/src/views/foo/foo.vue
<div>
<!-- 我的網(wǎng)站名 -->
<div v-if="$store.state.webConfig.webName">{{ $store.state.webConfig.webName }}</div>
<!-- 骨架屏 -->
<skeleton v-else></skeleton>
</div>
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']
})
}
}
}
? 優(yōu)化代碼
在
vuex
的socketInfo
對(duì)象加一個(gè)isUpdated
字段积蜻,如果更新了,直接取值進(jìn)行我需要的操作彻消,沒(méi)更新的話就行請(qǐng)求api
更新竿拆。這是目前能想到的比較優(yōu)雅的方案了。
${app}/src/views/foo/foo.vue
<div>
<!-- 我的網(wǎng)站名 -->
<div v-if="webConfig.isUpdated">
{{ webConfig.webName }}
</div>
<!-- 骨架屏 -->
<skeleton v-else></skeleton>
</div>
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
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
頁(yè)面需要使用數(shù)據(jù)的時(shí)候我們才去請(qǐng)求數(shù)據(jù)宾尚,因此App.vue
的請(qǐng)求可以取消丙笋,這樣一來(lái)用戶只是打開(kāi)我們的網(wǎng)站,并不會(huì)去請(qǐng)求無(wú)意義的數(shù)據(jù)煌贴。優(yōu)化了后臺(tái)的接口請(qǐng)求壓力御板。同時(shí)在第一次進(jìn)入foo.vue
頁(yè)面的時(shí)候已經(jīng)請(qǐng)求了數(shù)據(jù),如果用戶沒(méi)有刷新頁(yè)面牛郑,再次訪問(wèn)該頁(yè)面我們的socketInfo
對(duì)象的isUpdated
為true
怠肋,可以直接使用,不會(huì)去發(fā)送新的請(qǐng)求淹朋。
${app}/src/App.vue
<!-- App -->
<div id="app"></div>
export default {
name: 'App',
}
??更新方案
既然是進(jìn)入頁(yè)面之后可以判斷數(shù)據(jù)是否加載完畢灶似,我們也可以直接在頁(yè)面進(jìn)入之前,通過(guò)路由元信息配置該頁(yè)面需要的全局異步數(shù)據(jù)瑞你,然后通過(guò)路由跳轉(zhuǎn)的守衛(wèi)去拉取異步數(shù)據(jù)(全局公用的異步數(shù)據(jù)只需要加載一遍就行酪惭,如果加載失敗我們可以跳轉(zhuǎn)到服務(wù)器錯(cuò)誤頁(yè)面)。完了再顯示我們的頁(yè)面者甲,同時(shí)使用Promise.all
來(lái)進(jìn)行多個(gè)異步數(shù)據(jù)的讀取春感。nice~!上代碼
${app}/src/utils/permission.js
這個(gè)文件用來(lái)做路由攔截虏缸。
/**
* @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) => {
// 啟動(dòng)進(jìn)度條
NProgress.start()
// 公用vuex數(shù)據(jù)
const {
siteConfig,
appConfig,
socketInfo,
coinDecimal,
} = store.state.appSessionStatus
try {
// 異步任務(wù)列表
const task = []
// 站點(diǎn)信息
if (!siteConfig.isUpdated) {
task.push(store.dispatch('GET_SITE_CONFIG'))
}
// 站點(diǎn)配置
if (!appConfig.isUpdated) {
task.push(store.dispatch('GET_APP_CONFIG'))
}
/**
* @name 檢查前去的頁(yè)面需要的公用數(shù)據(jù)是否加載
*/
if (to.meta.isUsingCoinDecimal && !coinDecimal.isUpdated) {
// 需要全局小數(shù)點(diǎn)位數(shù)
task.push(store.dispatch('GET_COIN_DECIMAL'))
}
if (to.meta.isUsingSocketInfo && !socketInfo.isUpdated) {
// 需要全局socket鏈接信息
task.push(store.dispatch('GET_SOCKET_INFO'))
}
// 異步同時(shí)執(zhí)行請(qǐng)求任務(wù)
await Promise.all(task)
} catch (error) {
// 提示錯(cuò)誤
handleApiRequestException(error)
// 顯示網(wǎng)絡(luò)錯(cuò)誤白屏圖
store.commit('UPDATE_SERVER_ERROR', true)
// 請(qǐng)求失敗鲫懒,路由導(dǎo)航終止
return next(false)
} finally {
NProgress.done()
}
// Permission
if (store.state.token) {
// Has login
next()
} else {
// 判斷是否是公開(kāi)頁(yè)面
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
/**
* @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 [
// 幣幣交易》首頁(yè)
{
path: '/exchange',
name: 'ExchangeIndex',
component: () => import('@/views/exchange/index/index'),
meta: {
title: i18n.t('Title_Exchange'),
// 代表路由權(quán)限公開(kāi)
isPublic: true,
// 需要socketInfo信息
isUsingSocketInfo: true,
// 需要coinDecimal信息
isUsingCoinDecimal: true,
},
},
]
0x3 總結(jié)
記錄下自己平時(shí)解決問(wèn)題的思考方式和解決方案。
本文章代碼僅用工具檢查語(yǔ)法錯(cuò)誤刽辙,純手寫(xiě)窥岩,并未實(shí)際運(yùn)行,不保證邏輯合理宰缤,如果你有更好的方案颂翼,歡迎你和我討論晃洒。
有問(wèn)題才有更好的解決方案。謝謝你的閱讀朦乏。
0x4 謝謝你的閱讀 ??
關(guān)于我
SunSeekerX球及,前端開(kāi)發(fā)、Nodejs開(kāi)發(fā)呻疹、小程序吃引、uni-app
開(kāi)發(fā)、等等
喜歡探討技術(shù)實(shí)現(xiàn)方案和細(xì)節(jié)刽锤,完美主義者镊尺,見(jiàn)不得bug
。
Github:https://github.com/SunSeekerX
個(gè)人博客:https://yoouu.cn/
個(gè)人在線筆記:https://sunseekerx.yoouu.cn/