Vuex + TypeScript
前言
4.x
🚨 在 Vuex和 Vue.js3.x
完全发布之前,不建议在生产环境中使用本文描述的方法。Vuex4.x
和 Vue.js3.x
API 仍然不稳定。本文仅演示了我对 Vuex store 进行静态类型化的尝试,因为 Vuex storeVuex@v4.0.0-beta.1
已经移除了它的全局类型。
⚠️ 项目配置部分被有意省略。所有源代码都位于此仓库中。
介绍
Vuex@v4.0.0-beta.1
正式发布。其中一项重大变化是,该库不再附带this.$store
Vue 组件内的全局类型。
this.$store
Vuex 4 删除了Vue 组件中的全局类型
您可以在本期找到有关其背后的原因和动机的更多信息。
由于全局类型已被移除,开发者需要自行定义。如发行说明中所述:
使用 TypeScript 时,您必须提供自己的增强声明。
在本文中,我想分享我扩充 store 类型的经验。我将通过一个简单 store 的示例来演示这一点。为了简单起见,我们的 store 尽可能地简单。
让我们进行一些编码。
状态
商店的定义始于状态的定义。
state.ts
:
export const state = {
counter: 0,
}
export type State = typeof state
我们需要导出状态类型,因为它将用于 getter、mutations 和 action 的定义。
到目前为止一切顺利。让我们继续讨论突变。
突变
正如Vuex 文档中所述:
在各种 Flux 实现中,使用常量作为突变类型是一种常见的模式。
因此,我们所有可能的突变名称都将存储在MutationTypes
枚举中。
mutation-types.ts
:
export enum MutationTypes {
SET_COUNTER = 'SET_COUNTER',
}
现在我们已经定义了突变的名称,我们可以为每个突变(其实际类型)声明一个契约。突变只是一个简单的函数,它接受状态作为第一个参数,有效载荷作为第二个参数,并最终改变前者。State
类型在执行时用作第一个参数的类型。第二个参数特定于特定的突变。我们已经知道突变的存在SET_COUNTER
,所以让我们为它声明类型。
mutations.ts
:
import { MutationTypes } from './mutation-types'
import { State } from './state'
export type Mutations<S = State> = {
[MutationTypes.SET_COUNTER](state: S, payload: number): void
}
太棒了!现在是时候实现它了。
import { MutationTree } from 'vuex'
import { MutationTypes } from './mutation-types'
import { State } from './state'
export type Mutations<S = State> = {
[MutationTypes.SET_COUNTER](state: S, payload: number): void
}
export const mutations: MutationTree<State> & Mutations = {
[MutationTypes.SET_COUNTER](state, payload: number) {
state.counter = payload
},
}
该mutations
变量负责存储所有已实现的突变,并最终将用于构建存储。
MutationTree<State> & Mutations
类型交集保证了契约的正确实现。如果没有正确实现,TypeScript 会报错,并出现以下错误:
Type '{ SET_COUNTER(state: { counter: number; }, payload: number): void; }' is not assignable to type 'MutationTree<{ counter: number; }> & Mutations<{ counter: number; }>'.
Property '[MutationTypes.RESET_COUNTER]' is missing in type '{ SET_COUNTER(state: { counter: number; }, payload: number): void; }' but required in type 'Mutations<{ counter: number; }>'
MutationTree
关于类型,简单说几句。MutationTree
它是一个泛型类型,随包一起提供vuex
。从名字就可以看出,它用于声明一个类型的变异树。
vuex/types/index.d.ts
:
export interface MutationTree<S> {
[key: string]: Mutation<S>;
}
但它不够具体,无法满足我们的需求,因为它假设突变的名称可以是任意的string
,但在我们的例子中,我们知道突变的名称只能是typeof MutationTypes
。我们保留此类型只是为了与Store
选项兼容。
行动
对于这种简单的存储,不需要任何操作,但是为了说明操作的类型,让我们想象一下我们可以从某个地方获取计数器。
我们存储突变名称的方式与存储动作名称的方式相同。
action-types.ts
:
export enum ActionTypes {
GET_COUTNER = 'GET_COUTNER',
}
actions.ts
:
import { ActionTypes } from './action-types'
export const actions = {
[ActionTypes.GET_COUTNER]({ commit }) {
return new Promise((resolve) => {
setTimeout(() => {
const data = 256
commit(MutationTypes.SET_COUNTER, data)
resolve(data)
}, 500)
})
},
}
我们有一个简单的GET_COUNTER
操作,它返回Promise
,并在 500 毫秒内完成解析。它提交了之前定义的变更(SET_COUNTER
)。一切看起来都正常,但commit
它允许提交任何变更,这很不合适,因为我们知道 只能提交已定义的变更。让我们来修复这个问题。
import { ActionTree, ActionContext } from 'vuex'
import { State } from './state'
import { Mutations } from './mutations'
import { ActionTypes } from './action-types'
import { MutationTypes } from './mutation-types'
type AugmentedActionContext = {
commit<K extends keyof Mutations>(
key: K,
payload: Parameters<Mutations[K]>[1]
): ReturnType<Mutations[K]>
} & Omit<ActionContext<State, State>, 'commit'>
export interface Actions {
[ActionTypes.GET_COUTNER](
{ commit }: AugmentedActionContext,
payload: number
): Promise<number>
}
export const actions: ActionTree<State, State> & Actions = {
[ActionTypes.GET_COUTNER]({ commit }) {
return new Promise((resolve) => {
setTimeout(() => {
const data = 256
commit(MutationTypes.SET_COUNTER, data)
resolve(data)
}, 500)
})
},
}
就像我们声明突变契约一样,我们也声明了动作契约(Actions
)。我们还必须扩充包ActionContext
中附带的类型vuex
,因为它假设我们可以提交任何突变。AugmentedActionContext
完成这项工作,限制仅提交已声明的突变(它还会检查有效载荷类型)。
在操作中输入commit
:
实施不当的行动:
吸气剂
Getter 也可以是静态类型的。Getter 类似于 Mutation,本质上是一个接收状态作为第一个参数的函数。Getter 的声明与 Mutation 的声明没有太大区别。
getters.ts
:
import { GetterTree } from 'vuex'
import { State } from './state'
export type Getters = {
doubledCounter(state: State): number
}
export const getters: GetterTree<State, State> & Getters = {
doubledCounter: (state) => {
return state.counter * 2
},
}
全局$store
类型
商店的核心模块已经定义完毕,现在我们可以实际构建商店了。 中的商店创建流程Vuex@v4.0.0-beta.1
与 略有不同Vuex@3.x
。更多信息请参阅发行说明。Store
应声明 类型以便在组件中安全地访问定义的商店。请注意,默认的 Vuex 类型:getters
、commit
和dispatch
应替换为我们之前定义的类型。进行这种替换的原因是默认的 Vuex 商店类型过于通用。只需查看默认的 getter 类型:
export declare class Store<S> {
// ...
readonly getters: any;
// ...
}
毫无疑问,如果您想安全地使用类型化商店,这些类型并不合适。
store.ts
:
import {
createStore,
Store as VuexStore,
CommitOptions,
DispatchOptions,
} from 'vuex'
import { State, state } from './state'
import { Getters, getters } from './getters'
import { Mutations, mutations } from './mutations'
import { Actions, actions } from './actions'
export const store = createStore({
state,
getters,
mutations,
actions,
})
export type Store = Omit<
VuexStore<State>,
'getters' | 'commit' | 'dispatch'
> & {
commit<K extends keyof Mutations, P extends Parameters<Mutations[K]>[1]>(
key: K,
payload: P,
options?: CommitOptions
): ReturnType<Mutations[K]>
} & {
dispatch<K extends keyof Actions>(
key: K,
payload: Parameters<Actions[K]>[1],
options?: DispatchOptions
): ReturnType<Actions[K]>
} & {
getters: {
[K in keyof Getters]: ReturnType<Getters[K]>
}
}
我不会关注 TypeScript 的实用类型。
我们已经到达终点线。剩下的就是扩充全局 Vue 类型了。
types/index.d.ts
:
import { Store } from '../store'
declare module '@vue/runtime-core' {
interface ComponentCustomProperties {
$store: Store
}
}
太棒了!我们准备好享受完全打字的商店访问了。
在组件中的使用
现在我们的 store 已正确声明并静态类型化,我们可以在组件中使用它了。我们将介绍如何在使用 Options API 和 Composition API 语法定义的组件中使用 store,因为 Vue.js 3.0 两者都支持。
选项 API
<template>
<section>
<h2>Options API Component</h2>
<p>Counter: {{ counter }}, doubled counter: {{ counter }}</p>
<input v-model.number="counter" type="text" />
<button type="button" @click="resetCounter">Reset counter</button>
</section>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { MutationTypes } from '../store/mutation-types'
import { ActionTypes } from '../store/action-types'
export default defineComponent({
name: 'OptionsAPIComponent',
computed: {
counter: {
get() {
return this.$store.state.counter
},
set(value: number) {
this.$store.commit(MutationTypes.SET_COUNTER, value)
},
},
doubledCounter() {
return this.$store.getters.doubledCounter
}
},
methods: {
resetCounter() {
this.$store.commit(MutationTypes.SET_COUNTER, 0)
},
async getCounter() {
const result = await this.$store.dispatch(ActionTypes.GET_COUTNER, 256)
},
},
})
</script>
组合 API
要在使用 Composition API 定义的组件中使用存储,我们必须通过useStore
钩子访问它,它只返回我们的存储:
export function useStore() {
return store as Store
}
<script lang="ts">
import { defineComponent, computed, h } from 'vue'
import { useStore } from '../store'
import { MutationTypes } from '../store/mutation-types'
import { ActionTypes } from '../store/action-types'
export default defineComponent({
name: 'CompositionAPIComponent',
setup(props, context) {
const store = useStore()
const counter = computed(() => store.state.counter)
const doubledCounter = computed(() => store.getters.doubledCounter)
function resetCounter() {
store.commit(MutationTypes.SET_COUNTER, 0)
}
async function getCounter() {
const result = await store.dispatch(ActionTypes.GET_COUTNER, 256)
}
return () =>
h('section', undefined, [
h('h2', undefined, 'Composition API Component'),
h('p', undefined, counter.value.toString()),
h('button', { type: 'button', onClick: resetCounter }, 'Reset coutner'),
])
},
})
</script>
结论
我们努力的成果是完全静态类型的存储。我们只允许提交/分派已声明的、带有适当负载的变更/操作,否则会报错。
目前 Vuex 尚未提供正确的辅助函数来简化存储类型处理,因此我们必须手动操作。希望 Vuex 的后续版本能够支持灵活的存储类型。
文章来源:https://dev.to/3vilarthas/vuex-typescript-m4j