Мощный модуль для типизации Vuex

    Мотивом для написания данной статьи послужила другая статья на тему типизации Vue и, соответственно, Vuex. К моему удивлению я не обнаружил там упоминания модуля, который, по моему мнению, является лучшим в своем роде «типизатором» Vuex. Поиск по Хабру, да и вообще по Рунету (на самом деле и в англоязычных источниках не просто сходу найти какие-либо упоминания), увы, не дал никаких результатов. Данная статья не является подробным разбором и многостраничным мануалом по использованию и настройке, но скорее способом поделиться с вами, уважаемые Vue-ниндзя, инструментом, который отлично справляется со своей задачей.

    vuex-smart-module


    У кого совсем нет времени: Github.

    Главное предназначение модуля, как вы успели догадаться, — это полноформатное покрытие хранилища Vuex типами. Как внутри, так и непосредственно в самих компонентах. Модуль написан основным контрибьютором (@ktsn) библиотек Vuex и vue-class-component.

    Вода


    Признаться, мой путь в Typescript начался еще совсем недавно, в т.ч. и с такими штуками как декораторы, потому не могу сравнить данную библиотеку с другими аналогами. Мои попытки настроить и использовать другие инструменты (например vuex-module-decorators) приводили меня к разным проблемам, которые в итоге так или иначе не позволяли реализовать то, что мне было нужно (либо я просто, как говориться, не умел их готовить). С vuex-smart-module мне очень повезло — библиотека появилась именно в тот момент, когда я переводил проект (и хранилище) на Typescript. Теперь все отлично работает, а код радует глаз.

    Примеры


    У библиотеки, на мой взгляд, хорошая документация, которая покрывает все возможные случаи с которым вам придется столкнуться (а если нет — то в тестах можно найти все остальное, нетривиальное). Однако, чтобы хоть как-то разбавить статью кодом, приведу базовые примеры подключения и использования, пару примеров «из жизни», а так же как это работает в связке с декораторами (там есть нюанс).

    Создание модуля


    Создается модуль при помощи классов. Некоторым это может показаться непривычно отпугивающим, но поверьте, привыкаешь быстро.

    store/root.ts

    // Импорт базовых классов
    import { Getters, Mutations, Actions, Module } from 'vuex-smart-module'
    
    // Стейт
    class RootState {
      count = 1
    }
    
    // Геттеры
    // Необходимо расширить класс типами из RootState
    class RootGetters extends Getters<RootState> {
      get double() {
        // У инстанса геттера есть свойство `state`
        return this.state.count * 2
      }
    
      get triple() {
        // Для использования других геттеров есть свойство `getters`
        return this.getters.double + this.state.count
      }
    }
    
    // Мутации
    // Так же как и геттеры, класс мутаций расширяется типами RootState
    class RootMutations extends Mutations<RootState> {
      increment(payload: number) {
        // У мутаций так же есть свойство `state`
        this.state.count += payload
      }
    }
    
    // Действия
    // Здесь аналогично расширяется класс
    // Но есть один нюанс, класс нужно расширить типами этого же класса, явно указав это в параметрах
    class RootActions extends Actions<
      RootState,
      RootGetters,
      RootMutations,
      RootActions
    > {
      incrementAsync(payload: { amount: number; interval: number }) {
        // У инстанса действия есть свойства `state`, `getters`, `commit` и `dispatch` 
        return new Promise(resolve => {
          setTimeout(() => {
            this.commit('increment', payload.amount)
          }, payload.interval)
        })
      }
    }
    
    // Экспорт модуля
    export default new Module({
      state: RootState,
      getters: RootGetters,
      mutations: RootMutations,
      actions: RootActions
    })

    Подключение


    /store/index.ts

    import Vue from 'vue'
    import * as Vuex from 'vuex'
    import { createStore } from 'vuex-smart-module'
    import RootStore from './root'
    
    Vue.use(Vuex)
    
    export const store = createStore(
      RootStore,
      {
        strict: process.env.NODE_ENV !== 'production'
      }
    )
    

    Модули


    Подключение модулей аналогично тому, как это происходит в обычном Vuex. Их нужно указать в свойстве modules у RootStore:

    import FooStore from './modules/foo'
    
    /* … */
    
    export default new Module({
      state: RootState,
      getters: RootGetters,
      mutations: RootMutations,
      actions: RootActions,
      modules: {
        FooStore
      }
    })
    

    Использование внутри компонента


    Пользоваться стором можно как через глобальное свойство this.$store, так и через мэппинг, который во многом похож на тот, что есть во Vuex:

    import Vue from 'vue'
    
    // Импорт корневого стора (тоже самое и с любым другим модулем)
    // import FooStore from '@/store/modules/foo'
    import RootStore from '@/store/root'
    
    export default Vue.extend({
      computed: RootStore.mapGetters(['double']),
    
      methods: RootStore.mapActions({
        incAsync: 'incrementAsync'
      }),
    
      created() {
        console.log(this.double)
        this.incAsync(undefined)
      }
    })


    Типизация


    Пример типизации commit и dispatch:
    import { categories } from '@/api'
    
    export type Category {
      attributes: {
        hasPrice: boolean;
        icon: string
        lvl: number
        name: string
        slug: string
      };
      id: number
    }
    
    export interface IParams {
        city_id: number
    }
    
    class AppState {
        categories: Category[] = []
    }
    
    /* ... */
    
    class AppMutations extends Mutations<AppState> {
      setCategories(categories: Category[]) {
        this.state.categories = categories
      }
    }
    
    class AppActions extends Actions<
      AppState,
      AppGetters,
      AppMutations,
      AppActions
    > {
      async getCategories({params}: {params: IParams}): Promise<Category[]> {
        return categories.get({params}).then(
          ({ data }: { data: Category[] }) => {
            this.commit("setCategories", data)
            return data
          }
        )
      }
    }


    Приемы


    Подключение с использованием декораторов (vue-property-decorator)


    import { Vue, Component } from "vue-property-decorator"
    
    // Импорт корневого стора (тоже самое и с любым другим модулем)
    // import FooStore from '@/store/modules/foo'
    import RootStore from "@/store/root"
    
    // Обратите внимание, что для того, чтобы все заработало в рамках Typescript, необходимо расширить класс таким образом:
    const Mappers = Vue.extend({
      computed: {
        ...RootStore.mapGetters(["double"])
      },
      methods: {
        ...RootStore.mapActions({
          incAsync: 'incrementAsync'
        })
      }
    });
    
    @Component
    export default class MyApp extends Mappers {
      created() {
        console.log(this.double)
        this.incAsync(undefined)
      }
    }
    

    Использование модуля внутри модуля


    /store/module/bar.ts

    import { Store } from 'vuex'
    import { Getters, Actions, Module, Context } from 'vuex-smart-module'
    
    // Импорт другого модуля
    import FooStore from './foo'
    
    /* … */
    
    class BarGetters extends Getters {
      // Объявление контекста
      foo!: Context<typeof FooStore>;
    
      // Вызывается посли инициализации модуля
      $init(store: Store<any>): void {
        // Создание и сохранение контекста
        this.foo = FooStore.context(store)
      }
    
      get excited(): string {
        return this.foo.state.value + '!' // -> hello!
      }
    }
    
    /* … */
    

    Сброс хранилища


    Иногда может потребоваться сбросить хранилище на значения по умолчанию, делается это довольно просто:

    class FooState {
      /* ... */
    }
    
    class FooMutations extends Mutations<FooState> {
      reset () {
          const s = new FooState()
          Object.keys(s).forEach(key => {
            this.state[key] = s[key]
          })
      }
    }

    Финал


    Надеюсь, что вам было интересно, ну или, по крайней мере, вы узнали об этой библиотеке. Кто знает, может быть начиная со следующего проекта (а может быть и рефакторинг текущих не за горами?) вы начнете, как и я, использовать vuex-smart-module (или вообще Typescript в целом)? Лично мой переход на Typescript был довольно болезненным (за 1.5-2 года я принимался за попытки перейти на него раза 3-4 минимум, но каждый раз упирался в какие-то проблемы, непонимание. Меня часто преследовало ощущение, что разработка на Typescript занимает в 2-3 раза больше времени, чем раньше, т.к. теперь нельзя просто «по-быстрому набросать». Но однажды, перешагнув на «светлую сторону статической типизации», я ощутил всю мощь типов и то, как они позволяют в конечном итоге ускорить процесс разработки, что не менее важно, отладки кода (пожалуй, в те же самые 2-3 раза), а так же облегчить его дальнейшую поддержку.

    P.S. Не забудьте поставить звезду этому модулю. :)

    Благодарность
    В заключение хочу поблагодарить мою любимую жену за терпение, котейку за приятное урчание рядом на столе, соседей за тишину и, конечно же, вас за внимание!

    Only registered users can participate in poll. Log in, please.

    Используете ли вы Typescript в своих Vue-проектах?

    • 35.4%Да35
    • 21.2%Думаю, что пришло время использовать21
    • 1.0%Использую другие инструменты (Flow)1
    • 42.4%Нет42
    Share post
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 20

      0
      А какие проблемы у вас были с vuex-module-decorators?
        0
        Признаться у меня в воспоминаниях осталось только что-то вроде «не сработало». Если мне не изменяет память (это было около 4-х месяцев назад) я пытался использовать модуль внутри модуля… Либо архитектура приложения что-то не позволяла сделать. Еще возможно, что были проблемы с типизацией, которые могли быть из-за неполноценной на тот момент поддержки IDE (WebStorm) Typescript (после недавних обновлений стало намного лучше). На vuex-smart-module остановился потому что он позволил все реализовать без танцев, ошибок и нетривиальных конструкций. А может все было просто из-за моей не опытности. Но могу наверняка сказать, что с моей небольшой подготовкой (на тот момент) именно этот модуль оказался идеальным инструментом, с которым удалось сходу (почти) начать работать.
        +3

        Так самое-то важное, а как затипизированы commit и dispatch?

          0
          Примерно вот так (вырезал кусками и кое-что поменял, прошу прощения, если что-то упустил):
          export type Category {
            attributes: {
              hasPrice: boolean;
              icon: string;
              lvl: number;
              name: string;
              slug: string;
            };
            id: number;
          }
          
          export interface IParams {
          	city_id: number
          }
          
          class AppState {
          	categories: Category[] = [];
          }
          
          /* ... */
          
          class AppMutations extends Mutations<AppState> {
            setCategories(categories: Category[]) {
              this.state.categories = categories;
            }
          }
          
          class AppActions extends Actions<
            AppState,
            AppGetters,
            AppMutations,
            AppActions
          > {
            async getCategories({params}: {params: IParams}): Promise<Category[]> {
              return categoriesAPI.get({params}).then(
                ({ data: categories }: { data: Category[] }) => {
                  this.commit("setCategories", categories);
          
                  return categories;
                }
              );
            }
          }


          Обновил статью. Спасибо, что указали на промах.
            +1

            Главная проблема типизации в этой строчке:


            this.commit("setCategories", categories)


            Как вы обеспечиваете гарантию того, что такой mutation действительно существует и принимает именно такой набор аргументов?

              0
              Я, возможно, не правильно вас понял. Ответ лежит на поверхности: гарантии обеспечивает модуль vuex-smart-module, который берет на себя эту работу. Он предоставляет классы, которые через дженерики расширяются соответсвующими типами. В примере в статье видно, как это происходит. Таким образом, если вы попытаетесь указать в commit несуществующую мутацию, то Typescript вам об этом сообщит, подсветит место и подскажет, что не так. То же самое касается и принимаемых наборов аргументов.

              Гарантия существования мутации и принимаемых аргументов:
              class AppMutations extends Mutations<AppState> {
                setCategories(categories: Category[]) {
                  this.state.categories = categories;
                }
              }
              
              class AppActions extends Actions<
                AppState,
                AppGetters,
                AppMutations,
                AppActions
              > { /* ... */ }
              


              Вот пара файлов, в которых можно увидеть, как это реализовано: раз, два.
                0

                Мне кажется, что проблема вот тут
                this.commit("setCategories12345", categories);
                Если в названии мутации написать людей текст, то он будет комплимент, но не будет работать. Это — отсутствие типизация. Почему бы не указывать вместо строкового литерала именно член класса?

                  0
                  Если в названии мутации написать людей текст, то он будет комплимент, но не будет работать.

                  Это на Алиэкспресском написано?
                    0
                    В моем случае при настроенной IDE (в т.ч. и в консоли вылетает) я получаю вот такой результат:

                    image
                      0

                      Теперь не уверен. А у вас какая ide?

                        0
                        У меня WebStorm последней версии. Посмотрите, может у вас TS не настроен?.. Либо VSCode как альтернатива — он работает с TS идеально.
            +1
            Typescript, конечно, классная вещь, но хотя некоторые концепции требуют немалого напряжения извилин, так например Mapped Types осознать было не просто, но зато когда, наконец, понимаешь это, то поражаешься, насколько мощный это инструмент. С помощью этих Mapped Types создал свою библиотечку для управления состоянием в Angular, и теперь задумался — может ее и для Vue можно адаптировать.
              0

              ох, я бы сейчас не стал начинать проект на class components, сильно больно будет обновляться на vue 3

                +1
                старый синтаксис никуда не денется ведь
                  0

                  ну так я про синтаксис ничего не говорил, но вот в 3 версии искаропки не будет поддержки class api, рисковать ради непонятно чего? нафиг, лучше на объектном синтаксисе делать, что бы потом можно было легко обновится на 3 версию. или, даже лучше, быстро сконвертировать в новый функциональный синтаксис.

                    0
                    так и во втором не было поддержки class api. В чём проблема?
                      0

                      во втором таки поддержка class api официальная, репо классов лежит в репо вуя, а в 3 версии поддержку уже не обещают.

                        0
                        vue-class-component это всё равно не часть фреймворка. Ничто не помешает ей работать с Vue 3 и дальше
                +1

                А мне было интересно и полезно, спасибо автору за статью (:
                До этого работал с Vue только в связке с Flow, т.к. осознание преимущества типизации зашло не сразу (не у меня, у тиммейтов), а переделывать было уже некогда. Теперь вижу как можно подружить Vue с TS и первое впечатление крайне положительное (:

                  0
                  По идее, TS можно настроить так, что он практически ни на что не будет ругаться. Это позволит прозрачно перейти на TS без изменения кода. Особенно мне понравилось, что когда ты основательно типизируешь всякие сущности, то IDE в дальнейшем сразу подсказывает тебе какие есть ключи, что можно использовать сразу, а что надо проверить на null и т.д… В итоге отладки стало в разы меньше. (Понятное дело, что это могут решить тесты, но вы же понимаете...)

                Only users with full accounts can post comments. Log in, please.