Vuex и Typescript
В последнее время Typescript становится наиболее популярным в экосистеме Javascript и в данном посте я не хочу глубоко погружаться в Typescript, но мне бы хотелось показать базовые принципы интеграции Vuex во Vue приложении с кодовой базой Typescript.
На данном этапе, я предполагаю, что вы знакомы с базовыми понятиями Typescript и тем как использовать язык во Vue приложении. Если вы хотите взглянуть на базовый пример с TS, я предлагаю посмотреть этот репозиторий: GitHub - microsoft/TypeScript-Vue-Starter: A starter template for TypeScript and Vue with a detailed README describing how to use the two together.
Согласно официальной документации, Vuex определяется следующим образом:
Vuex - это шаблон управления состоянием + библиотека для Vue.js приложения. Он служит центральным хранилищем для всех компонентов в приложении, с набором правил, гарантирующих, что состояние может быть изменено только предсказуемым способом.
Так как у меня был большой опыт работы с Flux и Redux, данная концепция не открыла для меня чего-то нового, так что если вы знакомы с этой схемой, это не должно вызвать у вас больших трудностей при использовании Vuex.
По моему мнению, данный паттерн реально полезен когда вы имеете дело с приложением, в котором необходимо масштабирование и повышение общей производительности.
Но вернемся к теме, как нам объединить Vuex с Typescript?
// index.ts
import Vue from 'vue';
import Vuex, { StoreOptions } from 'vuex';
import { RootState } from './types';
import { profile } from './profile/index';
Vue.use(Vuex);
const store: StoreOptions<RootState> = {
state: {
version: '1.0.0' // a simple property
},
modules: {
profile
}
};
export default new Vuex.Store<RootState>(store);
И types.ts:
// types.ts
export interface RootState {
version: string;
}
Этот код очень похож на стандартный подход к созданию хранилища Vuex, но вы можете заметить здесь несколько различий:
store
создается с помощью типаStoreOptions
и определяетcя общим типомRootState
(который в свою очередь определяет тип корневого стейта)new Vuex.State
так же используетRootState
Благодаря этому, мы явно определяем типы для корневого экземпляра Vue.
Как обычно, я предлагаю и рекомендую использовать модульный подход, потому что появляется много преимуществ при подключении Vuex к нескольким компонентам, поэтому я создал хранилище с простым базовым модулем: Profile.
// profile/index.ts
import { Module } from 'vuex';
import { getters } from './getters';
import { actions } from './actions';
import { mutations } from './mutations';
import { ProfileState } from './types';
import { RootState } from '../types';
export const state: ProfileState = {
user: undefined,
error: false
};
const namespaced: boolean = true;
export const profile: Module<ProfileState, RootState> = {
namespaced,
state,
getters,
actions,
mutations
};
И types.ts:
// types.ts
export interface User {
firstName: string;
lastName: string;
email: string;
phone?: string;
}
export interface ProfileState {
user?: User;
error: boolean;
}
Глядя на файл index.ts, вы можете заметить несколько вещей:
- Стейт начинает инициализироваться с типом
ProfileState
- Созданный и экспортированный на данном этапе модуль немного сложнее: это
Module
определенный несколькими типами:ProfileState
(является типом стейта модуля) иRootState
(корневой стейт для хранилища Vuex)
Module
- это тип определенный во Vuex:
// vuex/types/index.d.ts
export interface Module<S, R> {
namespaced?: boolean;
state?: S | (() => S);
getters?: GetterTree<S, R>;
actions?: ActionTree<S, R>;
mutations?: MutationTree<S>;
modules?: ModuleTree<R>;
}
Глядя на данный интерфейс, Module
представляет собой простой объект, агрегирующий (опционально) переданные действия/мутации/геттеры/стэйт и внутренние модули.
Давайте на примере разберем действия.
Actions.ts:
// profile/actions.ts
import { ActionTree } from 'vuex';
import axios from 'axios';
import { ProfileState, User } from './types';
import { RootState } from '../types';
export const actions: ActionTree<ProfileState, RootState> = {
fetchData({ commit }): any {
axios({
url: 'https://....'
}).then(
(response) => {
const payload: User = response && response.data;
commit('profileLoaded', payload);
},
(error) => {
console.log(error);
commit('profileError');
}
);
}
};
Чтобы экспортировать что-то ожидаемое типом Vuex'Module
, нам нужно объединить наши действия в ActionTree
, тип определенный во Vuex следующим образом:
// vuex/types/index.d.ts
export interface ActionTree<S, R> {
[key: string]: Action<S, R>;
}
Не составляет труда понять, что это представляет собой объект ожидающий некоторые ключи, имена экшнов, и Action
ассоциации (по прежнему ожидающие типы Module State и Root State).
В нашем случае, мы имеем просто ActionTree
содержащий простое действие fetchData
, которое выполняет асинхронную задачу (получает некоторые данные из сервиса) и коммитит успех или ошибку, полагаясь на ответ сервера. Полезная нагрузка коммита, в данном случае успеха, типизирована в User
.
Давайте посмотрим на мутации:
Mutations.ts:
// profile/mutations.ts
import { MutationTree } from 'vuex';
import { ProfileState, User } from './types';
export const mutations: MutationTree<ProfileState> = {
profileLoaded(state, payload: User) {
state.error = false;
state.user = payload;
},
profileError(state) {
state.error = true;
state.user = undefined;
}
};
Мутации следуют тем же подходам, что мы обсуждали для Действий и ожидают переменную типа MutationTree
определенную во Vuex следующим образом:
// vuex/types/index.d.ts
export interface MutationTree<S> {
[key: string]: Mutation<S>;
}
Завершая инициализацию модуля, мы так же выставляем нужные нам геттеры. В нашем случае это простой геттер возвращающий полное имя выбранных пользователей, комбинируя свойства хранилища firstName
и lastName
.
Да, вы можете сделать это с помощью класса для User
, но я хотел бы иметь базовый пример для геттеров.
Getters.ts:
// profile/getters.ts
import { GetterTree } from 'vuex';
import { ProfileState } from './types';
import { RootState } from '../types';
export const getters: GetterTree<ProfileState, RootState> = {
fullName(state): string {
const { user } = state;
const firstName = (user && user.firstName) || '';
const lastName = (user && user.lastName) || '';
return `${firstName} ${lastName}`;
}
};
GetterTree
определен во Vuex следующим образом:
// vuex/types/index.d.ts
export interface GetterTree<S, R> {
[key: string]: Getter<S, R>;
}
Теперь самое интересное: как нам все это подключить во Vue компонент?
Для следующего примера я использую vuex-class, чтобы подключить простой компонент к Vuex.
<template>
<div class="container">
<div v-if="profile.user">
<p>Full name: {{ fullName }}</p>
<p>Email: {{ email }}</p>
</div>
<div v-if="profile.error">Oops an error occured</div>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import { State, Action, Getter } from 'vuex-class';
import Component from 'vue-class-component';
import { ProfileState, User } from './store/profile/types';
const namespace: string = 'profile';
@Component
export default class UserDetail extends Vue {
@State('profile')
profile: ProfileState;
@Action('fetchData', { namespace })
fetchData: any;
@Getter('fullName', { namespace })
fullName: string;
mounted() {
// получение данных после монтирования компонента
this.fetchData();
}
// вычисляемое свойство email пользователя
get email() {
const user = this.profile && this.profile.user;
return (user && user.email) || '';
}
}
</script>
Пример выше - это базовый сингл файл компонент, содержащий template
(с жесткой логикой отображения первой секции когда определенное условие возвращает истину) и script
описывающий поведение нашего компонента. В данном примере я так же использую vue-class-component, чтобы использовать синтаксис основанный на классах (vuex-class так же зависит от этого модуля).
Благодаря Vuex-class, возможно использовать декораторы, чтобы получить все что нам нужно: State, Actions, Mutations, Getters и обертку namespace
.
Наш компонент будет иметь несколько вычисляемых свойств, одно называется profile
и относится к стейту Profile
, а второе ссылается на getter
который мы определили в модуле.
Данный пример использует два декоратора предоставляемых vuex-class: State и Getter. Для доступа к нужному модулю, объект (или BindingOptions) со свойством namespace
передается вторым параметром.
@State('profile') profile: ProfileState;
@Getter('fullName', { namespace }) fullName: string;
В нашем случае нам нужно описать экшн fetchData
с помощью декоратора Action
:
@Action('fetchData', { namespace }) fetchData: any;
и вызвать его в колбеке mounted
жизненного цикла компонента
mounted() {
// получение данных после монтирования компонента
this.fetchData();
}
Чтобы отрендерить что-то осмысленное, часть шаблона использует ранее объявленные геттеры для отображения fullName
и базовое вычисляемое свойство, чтобы отобразить email
пользователя.
<p>Full name: {{ fullName }}</p>
<p>Email: {{ email }}</p>
Это основы. Существуют другие способы взаимодействия Vue компонента с Vuex, но я считаю что это неплохой способ начать работать с typescript.
Конечно, существует много возможностей для улучшения в данном примере/коде, например, улучшение типизации кода, с целью получения более надежной логики или улучшения визуализации изменений модуля.
Надеюсь, данная статья вам понравилась!
Оригинальная статья: https://codeburst.io/vuex-and-typescript-3427ba78cfa8