Vuex4 进阶

项目结构

一些规则:

  • 应用层级的状态应该集中到单个 store 对象中。
  • 提交 mutation 是更改状态的唯一方法,并且这个过程是同步的。
  • 异步逻辑都应该封装到 action 里面。

对于大型应用,我们会希望把 Vuex 相关代码分割到模块中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
├── index.html
├── main.js
├── api
│ └── ... # 抽取出API请求
├── components
│ ├── App.vue
│ └── ...
└── store
├── index.js # 我们组装模块并导出 store 的地方
├── actions.js # 根级别的 action
├── mutations.js # 根级别的 mutation
└── modules
├── cart.js # 购物车模块
└── products.js # 产品模块

组合式API

可以通过调用 useStore 函数,来在 setup 钩子函数中访问 store。

这与在组件中使用选项式 API 访问 this.$store 是等效的。

1
2
3
4
5
6
7
import { useStore } from 'vuex'

export default {
setup () {
const store = useStore()
}
}

访问 State 和 Getter

为了访问 state 和 getter,需要创建 computed 引用以保留响应性,这与在选项式 API 中创建计算属性等效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { computed } from 'vue'
import { useStore } from 'vuex'

export default {
setup () {
const store = useStore()

return {
// 在 computed 函数中访问 state
count: computed(() => store.state.count),

// 在 computed 函数中访问 getter
double: computed(() => store.getters.double)
}
}
}

访问 Mutation 和 Action

只需要在 setup 钩子函数中调用 commitdispatch 函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { useStore } from 'vuex'

export default {
setup () {
const store = useStore()

return {
// 使用 mutation
increment: () => store.commit('increment'),

// 使用 action
asyncIncrement: () => store.dispatch('asyncIncrement')
}
}
}

表单处理

用“Vuex 的思维”去解决这个问题的方法是:给 <input> 中绑定 value,然后侦听 input 或者 change 事件,在事件回调中调用一个方法:

1
<input :value="message" @input="updateMessage">
1
2
3
4
5
6
7
8
9
10
11
// ...
computed: {
...mapState({
message: state => state.obj.message
})
},
methods: {
updateMessage (e) {
this.$store.commit('updateMessage', e.target.value)
}
}
1
2
3
4
5
6
// ...
mutations: {
updateMessage (state, message) {
state.obj.message = message
}
}

双向绑定的计算属性

必须承认,这样做比简单地使用“v-model + 局部状态”要啰嗦得多,并且也损失了一些 v-model 中很有用的特性。另一个方法是使用带有 setter 的双向绑定计算属性:

1
<input v-model="message">
1
2
3
4
5
6
7
8
9
10
11
// ...
computed: {
message: {
get () {
return this.$store.state.obj.message
},
set (value) {
this.$store.commit('updateMessage', value)
}
}
}

TypeScript 支持

Vuex 提供了类型声明,因此可以使用 TypeScript 定义 store,并且不需要任何特殊的 TypeScript 配置。

请遵循 Vue 的基本 TypeScript 配置来配置项目。

Vue 组件中 $store 属性的类型声明

Vuex 没有为 this.$store 属性提供开箱即用的类型声明。如果你要使用 TypeScript,首先需要声明自定义的模块补充(module augmentation)

为此,需要在项目文件夹中添加一个声明文件来声明 Vue 的自定义类型 ComponentCustomProperties

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// vuex.d.ts
import { ComponentCustomProperties } from 'vue'
import { Store } from 'vuex'

declare module '@vue/runtime-core' {
// 声明自己的 store state
interface State {
count: number
}

// 为 `this.$store` 提供类型声明
interface ComponentCustomProperties {
$store: Store<State>
}
}

useStore 组合式函数类型声明

为了 useStore 能正确返回类型化的 store,必须执行以下步骤:

  • 定义类型化的 InjectionKey
  • 将 store 安装到 Vue 应用时提供类型化的 InjectionKey
  • 将类型化的 InjectionKey 传给 useStore 方法。

首先,使用 Vue 的 InjectionKey 接口和自己的 store 类型定义来定义 key :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// store.ts
import { InjectionKey } from 'vue'
import { createStore, Store } from 'vuex'

// 为 store state 声明类型
export interface State {
count: number
}

// 定义 injection key
export const key: InjectionKey<Store<State>> = Symbol()

export const store = createStore<State>({
state: {
count: 0
}
})

然后,将 store 安装到 Vue 应用时传入定义好的 injection key。

1
2
3
4
5
6
7
8
9
10
// main.ts
import { createApp } from 'vue'
import { store, key } from './store'

const app = createApp({ ... })

// 传入 injection key
app.use(store, key)

app.mount('#app')

最后,将上述 injection key 传入 useStore 方法可以获取类型化的 store。

1
2
3
4
5
6
7
8
9
10
11
// vue 组件
import { useStore } from 'vuex'
import { key } from './store'

export default {
setup () {
const store = useStore(key)

store.state.count // 类型为 number
}
}

本质上,Vuex 将store 安装到 Vue 应用中使用了 Vue 的 Provide/Inject 特性,这就是 injection key 是很重要的因素的原因。

简化 useStore 用法

引入 InjectionKey 并将其传入 useStore 使用过的任何地方,很快就会成为一项重复性的工作。为了简化问题,可以定义自己的组合式函数来检索类型化的 store :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// store.ts
import { InjectionKey } from 'vue'
import { createStore, useStore as baseUseStore, Store } from 'vuex'

export interface State {
count: number
}

export const key: InjectionKey<Store<State>> = Symbol()

export const store = createStore<State>({
state: {
count: 0
}
})

// 定义自己的 `useStore` 组合式函数
export function useStore () {
return baseUseStore(key)
}

通过引入自定义的组合式函数,不用提供 injection key 和类型声明就可以直接得到类型化的 store:

1
2
3
4
5
6
7
8
9
10
// vue 组件
import { useStore } from './store'

export default {
setup () {
const store = useStore()

store.state.count // 类型为 number
}
}