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 { count: computed(() => store.state.count),
double: computed(() => store.getters.double) } } }
|
访问 Mutation 和 Action
只需要在 setup
钩子函数中调用 commit
和 dispatch
函数。
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 { increment: () => store.commit('increment'),
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
| import { ComponentCustomProperties } from 'vue' import { Store } from 'vuex'
declare module '@vue/runtime-core' { interface State { count: number }
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
| import { InjectionKey } from 'vue' import { createStore, Store } from 'vuex'
export interface State { count: number }
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
| import { createApp } from 'vue' import { store, key } from './store'
const app = createApp({ ... })
app.use(store, key)
app.mount('#app')
|
最后,将上述 injection key 传入 useStore
方法可以获取类型化的 store。
1 2 3 4 5 6 7 8 9 10 11
| import { useStore } from 'vuex' import { key } from './store'
export default { setup () { const store = useStore(key)
store.state.count } }
|
本质上,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
| 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 } })
export function useStore () { return baseUseStore(key) }
|
通过引入自定义的组合式函数,不用提供 injection key 和类型声明就可以直接得到类型化的 store:
1 2 3 4 5 6 7 8 9 10
| import { useStore } from './store'
export default { setup () { const store = useStore()
store.state.count } }
|