JavaScript Vue

Как перейти с Vuex на Pinia

Теперь, когда Pinia стал официально рекомендуемым глобальным решением для управления состоянием для Vue.js, вам может потребоваться или вы захотите перейти глобально с Vuex. Как это сделать будет описано в записи. Давайте начнем.

Зачем переходить с Vuex на Pinia?

Некоторые преимущества Pinia по сравнению с Vuex включают в себя:

  • Pinia является модульным расширением (что способствует хорошей организации по логическому принципу и поддерживает разделение кода).
  • Pinia удаляет мутации (больше не нужно писать новую мутацию для обновления одной части состояния)
  • Pinia более интуитивно понятна (похожа на обычный JavaScript — чтение свойств и вызов методов, а также меньше концепций для изучения, чем Vuex).
  • Никаких “магических строк”, за которыми нужно следить (названия мутаций и действий)
  • Нет объекта контекста, с которым можно возиться в параметрах действия
  • Pinia имеет полную поддержку типов TypeScript

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

Настройка контекста

Чтобы помочь вам лучше следовать этому руководству, есть пара вещей, которые вы должны знать о кодовой базе, над которой мы работаем.

  • Используется Vue CLI
  • Vue 3
  • Используется простой JavaScript (без TypeScript)

Имея ввиду вышеперечисленное, некоторые из шагов могут немного отличаться для вас в зависимости от настройки, но в основном они должны быть одинаковыми.

Установка Pinia и модификация конфигураций Webpack

Первым шагом в миграции является установка пакета pinia npm.

npm install pinia

Я решил оставить Vuex установленным во время миграции, а затем удалить его только после завершения миграции. Эта стратегия означает, что приложение будет продолжать работать, и я могу выполнять поэтапную миграцию по одному store.

Затем вам нужно установить плагин Pinia в файле main.js.

import { createPinia } from 'pinia'
app.use(createPinia())

После этого запуская dev сервер получаем кучу ошибок компиляции, которые выглядят примерно так.

Это особенность WebPack’a и может быть исправлено с помощью следующей конфигурации, добавленной в vue.config.js.

module.exports = {
  configureWebpack: {
    module: {
      rules: [
        {
          test: /\.mjs$/,
          include: /node_modules/,
          type: 'javascript/auto'
        }
      ]
    }
  }
}

Определение хранилища из модулей Vuex

Дальше выбираем свой модуль Vuex под названием auth.js для преобразования в stores Pinia. Это ни в коем случае не был самым коротким из моих хранилищ, но более подходящий пример для урока. Вот его урезанная версия:

// src/store/modules/auth.js (Vuex)
export default {
  namespaced: true,
  state: {
    authId: null,
    authUserUnsubscribe: null,
    authObserverUnsubscribe: null
  },
  getters: {
    authUser: (state, getters, rootState, rootGetters) => {
      return rootGetters['users/user'](state.authId)
    }
  },
  actions: {
    signInWithEmailAndPassword (context, { email, password }) {
      return firebase.auth().signInWithEmailAndPassword(email, password)
    },
        async signOut ({ commit }) {
      await firebase.auth().signOut()
      commit('setAuthId', null)
    },
        //здесь было еще 9 действий...
  },
  mutations: {
    setAuthId (state, id) {
      state.authId = id
    },
    // здесь было еще 2 мутации 
        // (каждая для обновления одной части состояния выше)... 
  }
}

Мы можем создать что-то похожее на модуль Vuex в Pinia, определив то, что известно как Pinia “store“. Общепринято создавать хранилища Pinia в каталоге с именем store, и возможно называть их в следующем формате: [StoreName]Store.js.

Вот пример шаблонного кода для Pinia store.

// src/stores/AuthStore.js (Pinia)
import { defineStore } from 'pinia'

export const useAuthStore = defineStore('AuthStore', {
  state: () => {
    return {

    }
  },
  getters: {},
  actions: {}
})

Преобразование состояния, определенного Vuex в Pinia

Теперь мы можем начать перемещать элементы из модуля аутентификации Vuex в AuthStore Pinia.

Само состояние в значительной степени портируемое один к одному. Я могу просто скопировать и вставить его.

// src/stores/AuthStore.js (Pinia)
export const useAuthStore = defineStore('AuthStore', {
  state: () => {
    return {
      authId: null,
      authUserUnsubscribe: null,
      authObserverUnsubscribe: null
    }
  },
    //...
})

Конечно, доступ к состоянию — это совсем другое дело, но мы вернемся к этому после того, как закончим преобразование определения модуля Vuex в определение хранилища Pinia.

Преобразование геттеров, определенных в Vuex, в Pinia

Далее, у меня есть один геттер внутри auth.js, который обращается к другому модулю Vuex, чтобы получить фактического пользователя на основе authId в состоянии.

// src/store/modules/auth.js (Vuex)
getters:{
    authUser: (state, getters, rootState, rootGetters) => {
   return rootGetters['users/user'](state.authId)
  }
}

Чтобы сфокусироваться на одном модуле за раз, я продолжу использовать пользовательский модуль Vuex, импортировав store Vuex и получив доступ к геттерам, поскольку rootGetters недоступны в геттерах Pinia.

// src/stores/AuthStore.js (Pinia)

import store from '@/store' // это store Vuex (и он является временным, пока миграция не будет завершена)
//...
getters:{
    authUser(){ // должна быть нестрелочная функция
      return store.getters['users/user'](this.authId) // получаем authId в `this`
    }
}

Обратите внимание, что больше не нужно предоставлять все различные аргументы геттеру. Также можно получить доступ к части состояния authId в этом параметре состояния, а не в переданном параметре state.

Изменим объявление функции на стиль без стрелок, чтобы это правильно относилось к store. Если бы стрелочная функция была сохранена, то можно было бы получить доступ к состоянию следующим образом:

authUser: (state) =>{ 
 return store.getters['users/user'](state.authId) // получаем authId в `state`
}

Преобразование Actions определенных Vuex в Pinia

Далее нам нужно позаботиться о действиях. Вот как они выглядят в Vuex вместе с вызываемыми ими mutations.

// src/store/modules/auth.js (Vuex)
export default {

  actions: {
    signInWithEmailAndPassword (context, { email, password }) {
      return firebase.auth().signInWithEmailAndPassword(email, password)
    },
        async signOut ({ commit }) {
      await firebase.auth().signOut()
      commit('setAuthId', null)
    },
        //здесь было больше 9 действий...
  },
  mutations: {
    setAuthId (state, id) {
      state.authId = id
    },
        // ...
  }
}

Первый является прямолинейным, поскольку он не изменяет никакого состояния. Необходимо просто импортировать firebase и удалить аргумент контекста, поскольку действиям Pinia он не нужен.

// src/stores/AuthStore.js (Pinia)
import firebase from '@/helpers/firebase'

actions: {
  signInWithEmailAndPassword ({ email, password }) {
    return firebase.auth().signInWithEmailAndPassword(email, password)
  }
}
//...

Действие signOut вызывает мутацию для обновления состояния.

// src/store/modules/auth.js (Vuex)
async signOut ({ commit }) {
  await firebase.auth().signOut()
  commit('setAuthId', null)
},

В Pinia нам больше не нужно вызывать мутации. Вместо этого мы можем изменить состояние напрямую.

// src/stores/AuthStore.js (Pinia)
async signOut ({ commit }) {
  await firebase.auth().signOut()
  this.authId = null
}

Это означает, что нам не нужно портировать какие-либо мутации. Кроме того, нам больше не нужна функция фиксации из объекта контекста. Этого понятия просто не существует в Pinia.

// src/stores/AuthStore.js (Pinia)
async signOut () { //..

Заключительные шаги переноса модуля auth Vuex в Pinia AuthStore

На этом этапе были перенесены 9 действий, которые существовали в модуле auth.js Vuex, и процесс выглядел почти так же, как описанные выше шаги, за исключением:

  • Изменены строки, в которых dispatch использовался для вызова других actions, определенных в том же модуле, чтобы ссылаться на actions в this
// this в Vuex
*await* dispatch('fetchAuthUser')

// стал таким в Pinia
*await* this.fetchAuthUser()
  • Для actions, вызванных из других хранилищ, временно отправляем их из импортированного store Vuex. (Сделаем то же самое для mutations определенных в корневом хранилище но зафиксированных в auth.js)
import store from "@/store"

// this in Vuex
dispatch('users/createUser')
// became this in Pinia (this is just temporary!)
store.dispatch('users/createUser')

// likewise this in Vuex
commit('setItem', { resource: 'posts', item }, { root: true })
// became this in Pinia (this is just temporary!)
store.commit('setItem', { resource: 'posts', item }, { root: true })
  • В редком случае, когда определялось действие Vuex со стрелочной функцией, мы переписывали ее в функцию без стрелок для Pinia, чтобы она правильно ссылалась на хранилище Pinia.
// написано для Vuex
fetchAuthUser: async ({ dispatch, state, commit }) => {/*...*/}

// переписано для Pinia
async fetchAuthUser () {/*...*/}
  • Был удален объекта контекста в качестве первого аргумента действий.

Оставшиеся mutations я оставил в одном месте, так как они не нужны в Pinia.

При этом модуль auth.js Vuex больше не нужен, поэтому он удален из импорта и из регистраций в корневом хранилище Vuex.

// src/store/index.js
// ...
~~import auth from './modules/auth'~~ //removed this
export default createStore({
  modules: {
    //...
    ~~auth~~ // и удаляем это
  },
  //...
})

Обновление взаимодействия со стороны компонента

Первая часть работы была удобной. Но у нас все еще идет работа над модулем аутентификации Vuex. Чтобы выяснить, где еще вызывается модуль, можно поискать “auth/” по всему проекту.

Большинство найденных результатов были в компонентах. Два были в файле, где настраиваются и регистрируются маршруты с помощью Vue Router.

Это изображение имеет пустой атрибут alt; его имя файла - search-results-for-auth-action-dispatches-in-vs-code-1024x683.jpg

Вызов действий Pinia через Options API

У нас есть два варианта развития событий. Можно заменить диспетчеры Vuex вызовами действий Pinia через API options или API композиции. Pinia поддерживает оба варианта. Начнем с API Options.

В TheNavbar.vue, обрабатывается метод signOut, когда пользователь щелкает ссылку Sign Out.


<a
  @click.prevent="$store.dispatch('auth/signOut'),
  $router.push({name: 'Home'})"
>
    Sign Out
</a>

Вместо доступа к хранилищу в $store, как в случае с Vuex, мы должны импортировать композитный компонент Pinia AuthStore в наш компонент компонент.


import { useAuthStore } from '../stores/AuthStore'

Затем мы также можем импортировать helper mapActions из Pinia и использовать его для сопоставления нашего действия signOut с методом локального компонента.

import { mapActions } from 'pinia'
export default{
  methods:{
    ...mapActions(useAuthStore, ['signOut'])
  }
  //...
}

Наконец можно обновить обработчик кликов для ссылки “Sign Out”.

@click.prevent="signout() //..."

Вызов действия Pinia через Composition API

Теперь давайте рассмотрим использование API композиции. В SignIn.vue метода signIn мы отправляем действие signInWithEmailAndPassword.

methods:{
  async signIn () {
    try {
      await this.$store.dispatch('auth/signInWithEmailAndPassword', { ...this.form })
      this.successRedirect()
    } catch (error) {
        alert(error.message)
    }
  },
}

Мы снова импортируем композитный useAuthStore, но на этот раз он вызывается из параметра настройки и деструктурируем из него желаемое действие.

setup () {
  const { signInWithEmailAndPassword } = useAuthStore()
},

Чтобы показать действие остальной части компонента, мы вернем его из setup.

setup () {
  const { signInWithEmailAndPassword } = useAuthStore()
  return { signInWithEmailAndPassword }
},

И, наконец, мы заменим диспетчер на прямой вызов действия.

// заменим это
await this.$store.dispatch('auth/signInWithEmailAndPassword', { ...this.form })
// на это
await this.signInWithEmailAndPassword({ ...this.form })

Проверка прогресса в браузере

Следуя приведенному выше шаблону, мы продолжили заменять все экземпляры диспетчеров Vuex вызовами действий Pinia. Перейдя на главную страницу, в консоли есть 2 ошибки, связанные с store.

Мы забыли про mapActions и mapGetters. Поиск mapActions(‘auth показывает 2 изменения, которые нужно внести.

И mapGetters('auth еще три.

Чтение Pinia Getter через Options API

Экземпляры mapAction, которые были обработаны точно такие же, как и вызовы диспетчеров ранее. Однако для чтения геттеров необходимы действия, которых мы еще не видели. Давайте рассмотрим их подробнее.

В TheNavbar.vue есть экземпляр mapGetters, используемый для получения authUser.

computed: {
  ...mapGetters('auth', ['authUser'])
},

Чтобы это работало с Pinia и Options API необходимо импортировать mapState из Pinia и заменить им вызов Vuex mapGetters.

import { useAuthStore } from '../stores/AuthStore'

~~import { mapGetters } from 'vuex'~~ // remove this
import { mapActions, mapState } from 'pinia' // add this

computed:{
    ~~...mapGetters('auth', ['authUser']),~~ // remove this
  ...mapState(useAuthStore, ['authUser']) // add this
}

Обратите внимание, что в Pinia нет mapGetters. При использовании с Options API mapState полезен для доступа как к состоянию и к геттерам.

Чтение Pinia Getter через Composition API

В Profile.vue есть еще один экземпляр mapGetters('auth.

// C vuex
...mapGetters('auth', { user: 'authUser' }),

Теперь давайте переключим его с помощью API композиции. Поскольку в Profile.vue уже установлен параметр настройки, который использует действие из AuthStore, можно подумать, что можно получить доступ к геттеру следующим образом:

// (с Pinia)
setup () {
  const { fetchAuthUsersPosts, authUser } = useAuthStore() // ❌ это не работает
  return { fetchAuthUsersPosts, authUser }
},

Это не будет работать. Чтобы сохранить реактивность при удалении геттеров и состояния из хранилищ, нам нужно использовать вспомогательную функцию storeToRefs.

// (с Pinia)
import { storeToRefs } from 'pinia'
//...
setup () {
  const { fetchAuthUsersPosts } = useAuthStore()
  const { authUser } = storeToRefs(useAuthStore())
  return { fetchAuthUsersPosts, authUser }
},

И наконец, если вы были внимательны можно заметить, что мы переименовали геттер authUser внутри хелпера Vuex mapGetter в хелпер.

// (с Vuex)
...mapGetters('auth', { user: 'authUser' }),

Мы можем сделать это и для Pinia.

// (с Pinia)
setup () {
  //...
  return { fetchAuthUsersPosts, user: authUser }
},

Мы поработаем с последним mapGetter таким же образом.

Обновление чтения из состояния во всем приложении

Затем мы можем позаботиться обо всех экземплярах, где мы считываем состояние из старого модуля Vuex, следующим образом: $store.state.auth.

Поиск по регулярному выражению показывает 3 места, которые требуют обновлений.

Regx используется здесь чтобы можно было искать экземпляры с $ и без начального $.

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

Это решение для PostList.vue:

// это в Vuex
<a v-if="post.userId === $store.state.auth.authId" //...

// стал в Pinia
<a v-if="post.userId === authId" //...
//...
import { useAuthStore } from '@/stores/AuthStore'
import { mapState } from 'pinia'
//...
computed: {
  ...mapState(useAuthStore, ['authId']),
},

Решение для двух вхождений в router/index.js похоже, но поскольку мы не были в контексте компонента в котором ссылки автоматически разворачиваются пришлось указать .value.

// src/router/index.js
router.beforeEach(async (to, from) => {
  const { authId } = storeToRefs(useAuthStore())
  //...
  if (to.meta.requiresAuth && !authId.value /* <- здесь используем .value */) { 
    //...
  }
})

Обновление чтения из состояния в модулях Vuex

Последний шаг это изменить экземпляры внутри оставшихся модулей Vuex, где доступ к состоянию модуля аутентификации осуществляется в rootState.auth. Этот шаг необязателен, если вы планируете выполнить всю миграцию один раз, тем не менее настоятельно рекомендуется это сделать. Конечно cкоро вы удалите эти модули Vuex, но это может помочь вам сосредоточиться на одном store за раз, запуская тесты и выполняя ручное тестирование после завершения каждого модуля для сохранения перехода. Кроме того, когда вы копируете свои действия в новые хранилища Pinia для модификации, этот доступ к состоянию из других хранилищ уже будет правильно настроен. Таким образом, настоятельно рекомендуется переходить к следующему модулю Vuex только после того, как приложение полностью заработает.

Поиск rootState.auth выдал 6 результатов.

Мы можем отредактировать их все, заменив rootState.auth.authId на authId из store Pinia, как показано ниже.


import { useAuthStore } from '@/stores/AuthStore'
//...
actions:{
  async createPost ({ commit, state, rootState }, post) {
      const authStore = useAuthStore() // use AuthStore 

      // это было в Vuex
      post.userId = rootState.auth.authId
      //стало в Pinia
      post.userId = authStore.authId
  }
  //...
}

Просто убедитесь, что вы вызываете const authStore = useAuthStore() в каждом действии, когда вы пытаетесь получить доступ к authStore.authId таким образом, что он фактически определен в текущей области. У вас может возникнуть желание вызвать useAuthStore() в начале файла, но это не сработает, поскольку вы можете использовать хранилища Pinia только в контексте приложения Vue, когда определено корневое хранилище Pinia.

To top