原文连接: github.com/iglennlee/b…
1. 情景回顾
我们首先来回顾下官方对 vuex 作用的描述:
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
简单来说,vuex 就是用于管理的应用组件状态的,应用状态在vue中主要是指存放于 data 方法的字段(当然也包括 computed 数据)。一般情况下,我们会把组件的状态存放于组件自身的 data(或 computed)中,只有当状态数据需要和其他组件进行通信时,我们才会考虑把它放到 vuex 的 state(或 getters)中。但是,当项目逐渐变大的时候,组件的状态越来越多,组件间的通信方式也变得更加复杂,这时候,我们会发现存放于 state 中的数据越来越多,管理的难度也大大提高。于是,出现module的概念,也就是将state划分成各个modules。本文介绍的 page state 也正是基于module划分的。
在介绍如何划分 page state 之前, 我们先来看看都有哪些数据是适合放到 state 里的。
其实,很多时候我们不会轻易地将组件的状态放到 state 里,特别是对于哪些只涉及父子组件通信的状态,这种状态我们通常可以直接放到 data 中,利用 props 形式进行组件间的通信。但是,利用 props 进行的通信的方式也存在很多限制。一方面是不适合通信跨层级太多(一般控制在父子孙3级以内),另一方面是由于 vue 单向数据流特点,子孙组件向父组件通信方式存在困难;再者,由于 props 的 immutable 特点,不建议我们直接修改 props 的值,所以一般只能以 callback 或者 emit 的方式向父组件通信,这样的通信方式不适合嵌套层数太深的场景,所以这种状态数据我们一般也会放到 state 里。
还有些状态数据是进行兄弟组件之间通信的,对于这一类数据,一种方法是将状态抽到兄弟组件共同父组件中,但是,这种方式一方面要求兄弟组件之间有共同父组件而且共同父组件不宜距离兄弟组件太远,另一方面是,状态数据放到父组件中,很可能在父组件中看不出它的作用,容易被忽略或者不慎删除,所以往往这类状态数据也会被放到 state 里 。
此外,有些组件状态可能还涉及到跨路由通信,这个时候我们要么将数据放到url上要么将数据放到localstorage 或者 sessionstorage 等。但是,有些组件状态不适合放到 url 或者本地存储中,因为这两种存储方式都是不可靠的,之所以不可靠是因为他们都存在被外部修改的可能性。对于一些必须防止被外部修改的状态,我们最好是将他们放到 state 里。
总结一下可以发现,有两类状态数据适合放到 state 里, 一类是用于进行同个路由下的组件间通信(包括父子组件和兄弟组件通信)的状态数据,一类是用于跨路由通信,需要防止被外部修改的状态数据。
一般而言,对于跨路由通信的状态数据,推荐直接放到根 state 中,成为项目的全局状态。这是因为这类数据在两个页面中出现,那么他们后来在3个,4个甚至更多页面出现的可能性也更大。而对于另外一类的数据,一般不推荐直接放到根 state 中,这是因为这类数据往往数据量比较大。特别是随着项目页面增多时,这类数据的增长往往也是很明显的。那么,这类数据要如何处理呢?这里,我们引入一个相对于root state 的概念,我叫它为 page state,即当前页面的全局 state。root state 和 page state 之间的关系可以类比于全局作用域和局部作用域。下面将会介绍如何利用 vuex 创建 page state。
2. 实践演示
项目目录结构如下:
首先我们创建三个路由导航:page1, page2, page3。
// route/base.js
const base = [
{
path: '/',
name: 'index',
redirect: '/page1'
},
{
path: '/page1',
name: 'page1',
component: null
},
{
path: '/page2',
name: 'page2',
component: null
},
{
path: '/page3',
name: 'page3',
component: null
}
]
export default base
PS:这里之所以引入 base.js 是因为防止陷入引用循环,因为在 @/store/module/page/pathMap.js 文件中需要 import routes 而 pathMap.js 有被组件Page1 Page2等import。
// route/routes.js
import base from './base'
import Page1 from '@/components/Page1.vue'
import Page2 from '@/components/Page2.vue'
import Page3 from '@/components/Page3.vue'
/* [<route name>, <component>] */
const components = [
['page1', Page1],
['page2', Page2],
['page3', Page3]
]
const componentsMap = new Map(components)
const routes = base.map((item) => {
if (item.hasOwnProperty('component')) {
item.component = componentsMap.get(item.name)
}
return item
})
export default routes
// route/router.js
import Vue from 'vue'
import Router from 'vue-router'
import routes from './routes'
import { updatePageModuleFn } from '@/store/modules/page/util'
Vue.use(Router)
const router = new Router({
mode: 'history',
base: '/',
routes
})
router.beforeResolve(async (to, from, next) => {
await updatePageModuleFn(to, from)
next()
}) // 我们在路由钩子 beforeResolve 里调用 updatePageModuleFn 方法动态添加各个路由 page state
export default router
接着我们来看下 store 目录下各个文件代码
//store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
import state from '@/store/state'
import mutations from '@/store/mutations'
import actions from '@/store/actions'
import getters from '@/store/getters'
import page from '@/store/modules/page/default'
Vue.use(Vuex)
const store = new Vuex.Store({
namespaced: true,
state,
getters,
mutations,
actions,
modules: {
page: page
}
})
export default store
PS:我们在 modules 中注入了 page module,我们后面动态添加的 page state 都会在 page module 下,这样做的好处是方便管理我们动态注入的 page state。
// store/state.js
const state = {
userInfo: {
firstName: 'glenn',
lastName: 'lee'
}
}
export default state
PS: 我们在root state 中增加了 userIInfo 数据,为了后面演示对比的需要。
// store/modules/page/util.js
import { createNamespacedHelpers } from 'vuex'
import store from '@/store'
import dftModule from './default'
import modulesList from './index'
export async function getModuleFn (name) {
if (!name) return null
if (!modulesList.find(m => m === name)) return dftModule
let res = dftModule
await import(`./${name}/`).then((module) => {
res = module.default
})
return res
} // 获取各个page state module
export async function updatePageModuleFn (to) {
const module = await getModuleFn(to.name)
if (!module) return
if (!store.state.page[to.name]) {
store.registerModule(['page', to.name], module)
}
} // 动态添加 page state
export function generateVuexMapFn (path) {
const {
mapState,
mapActions,
mapMutations,
mapGetters
} = createNamespacedHelpers(path)
return {
mapPageState: mapState,
mapPageGetters: mapGetters,
mapPageMutations: mapMutations,
mapPageActions: mapActions
}
} // 获取 mapPageState, mapPageGetters,mapPageMutations, mapPageActions 方便在组件中操作page state
// store/modules/page/default/index.js
/* use function to create module's state, getters etc is to avoid data contamination */
const module = {
namespaced: true,
state () {
return {}
},
getters () {
return {}
},
mutations () {
return {}
},
actions () {
return {}
}
}
export default module
PS: 这是默认添加的空 page state
// store/modules/page/page1/index.js
import pageTypes from './types'
const module = {
namespaced: true,
state: {
path: 'page/page1/',
pageNum: 1
},
getters: {},
mutations: {
[pageTypes.TEST_PAGE1] (state, payload) {
state.pageNum = payload
}
},
actions: {
}
}
export default module
// store/modules/page/page1/types.js
const TEST_PAGE1 = 'TEST_PAGE1'
const pageTypes = {
TEST_PAGE1
}
export default pageTypes
PS: 这两个文件分别是 page1 路由下的 page state 和 types 定义文件。
以上就是 page state 的定义,可以看出 page state 定义和我们平时定义 root state 并没有任何不同。
下面我们来看看如何在组件中获取和修改 page state。
// Page1.vue
<template>
<div class="page1-container">
this is page 1
<button @click="changePageNumFN">change pageNum</button>
</div>
</template>
<script>
/**
component path
import Page1 from '@/components/Page1.vue'
*/
import { mapState } from 'vuex'
import pageTypes from '@/store/modules/page/page1/types'
import { generateVuexMapFn } from '@/store/modules/page/util'
import pathMap from '@/store/modules/page/pathMap'
const { mapPageState, } = generateVuexMapFn(pathMap.get('page1')) // 获取 mapPageState 等方法,用于操作 page state
export default {
name: 'Page1',
computed: {
...mapState(['userInfo']),
...mapPageState(['path', 'pageNum']) // 获取 page state 下的 `path` 和 `pageNum`
},
created () {
console.log('TCL: created -> this.userInfo', JSON.stringify(this.userInfo))
console.log('TCL: created -> this.path', this.path)
console.log('TCL: created -> this.pageNum', this.pageNum)
},
methods: {
changePageNumFN () {
this.$store.commit(`${this.path}${pageTypes.TEST_PAGE1}`, Math.floor(Math.random() * 10)) // 修改page state 下的 `pageNum`
this.$nextTick(() => {
console.log('TCL: changePageNumFN -> this.pageNum', this.pageNum)
})
}
}
}
</script>
<style scoped>
</style>
从代码可以看出,我们访问 page state 方式和访问 root state 的方式很相似, 不同点在于访问 root state 用的 vuex 提供的 mapState 等方法,而访问 page state 则是调用我们在 store/modules/util.js 文件中定义的 generateVuexMapFn 方法。
下面是运行结果截图:
3. 扩展
如果你稍微注意可能可以发现其实虽然我们注册了各个页面的 page state, 但是其实在 page1 是可以访问到 page2 的 page state 的, 只要 page2 的 page state 已经注册了。但是,这里并不推荐你这么做,因为这么做的话,page state 就不再只是 当前页面的全局状态这么纯粹了,而且如此操作不利于对 page state 进行追踪,因为 page state 可能在不同页面被修改。此外,也不利于我们接下来要说的关于 page state 的功能扩展。
我们回顾上面的代码实现, 可以看出我们是利用 vuex 的 registerModule 方法动态注册 page state 的,既然可以注册也意味着可以卸载。没错, vuex 同时提供了 unregisterModule 方法进行动态注册模块的卸载。一般来说, 对于页面数量不多在20以内的,并不推荐卸载注册的 page state 这是因为这些页面被再次访问的可能性是非常高的。当然,你可以选择性的对某些访问频率不高的页面动态卸载其 page state。此外, 由于足够的灵活性,我们甚至可以自定义卸载 page state 策略以应对复杂的应用需求,提高应用性能。对此,限于篇幅,本文不做具体实现展开。
此外, 由于 page state 的存在更加方便我们对页面数据进行缓存。举个例子说明一下:现在在 page1 有一个表单需要用户填写。用户填写中途需要到 page2 去查看相应的信息再回来填写。对于单页应用来说, 指引用户到 page2 之后,当用户再次回到 page1 时候,之前填写的表单依旧保持用户填写到一半的状态。对于这样的需求,我们或者可以借助 sessionstorage 或者 localstorage 实现。但是有了 page state 之后,我们可以完全不需要利用到本地存储也可以轻松实现这样的需求。此外, 我们还可以在 page state 中做 api 数据的缓存,避免频繁访问 api 提升交互效果的同时也降低了后端接口压力。 对此,限于篇幅,本文同样不做具体实现展开。
4. 最后
行文短促,文章难免存在纰漏,还望不吝指正。欢迎评论交流意见~~~