Миграция на Vue 2.7

Здравствуйте! В данной статье я бы хотел поделиться своим опытом обновления проекта, написанного на Vue 2.6. Помимо обновления самого vue и компонентов, я на примерах покажу как мне удалось обновить другие зависимости проекта и адаптировать их для работы с Composition API, среди них: Vuex, BootstrapVue, AgGrid и VueFormGenerator.

История Composition API во Vue

React

Как ни странно, но данному нововведению мы обязаны React-у, а точнее представленной в 2018 году концепции react-хуков.

Создатель vue, Эван Ю о реакт хуках

Создатель vue, Эван Ю о реакт хуках

Я никогда не трогал React, лишь бегло прочитал его документацию, поэтому не могу объективно высказаться об этой возможности библиотеки, однако почти все React-разработчики заявляют что хуки позволяют в значительной мере упростить разработку и создавать переиспользуемый код; более того функциональные компоненты с использованием хуков это уже стандарт разработки на React.

Vue 3

С выходом vue 3-ей версии, разработчикам стал доступен новый подход к созданию компонентов, схожий с функциональными компонентами реакта — Composition API: метод setup и <script setup> для использования в однофайловых компонентах.

Сравнивая composition API с options API, в качестве его преимуществ обычно перечисляют:

  • простота и лаконичность

  • возможность создавать переиспользуемые куски логики (вместо миксинов)

  • улучшенная поддержка TypeScrit

  • большая производительность (по заявлениям создателей)

Стоит также учесть и критические оценки такого подхода:

Источники

Composition API во Vue 3 — плюсы, минусы и опыт использования

Обзор Vue Composition API. Реальность оказалась сложнее

В целом можно заметить что все недостатки или опасения по поводу использования composition API упираются в опыт и знания разработчика: composition API предоставляет гораздо больше инструментов для работы с реактивностью, что естественно требует некоторых усилий для понимания и осторожности, особенно если разработчик привык к options или class API.

Vue 2.7

В июле 2022 года вышел релиз vue 2.7, в котором composition API было добавлено из коробки (ранее для этого требовалась библиотека @vue/composition-api), и добавлена возможность использовать

Получите следующую картину:
constructor: true
created: false

Это относится и к функциям в объектах, объявленным в классе, вы просто не имеете доступ к актуальному состоянию объекта (this в данном случае всё ещё тот объект полученный из класса).

// ...
  obj = {
    f: () => {
      // this != контекст компонента
    }
  }
// ...

Поэтому в таких случаях приходилось создавать отдельный метод в классе и передавать уже его, даже если требовалось просто изменить какое-то одно поле в объекте, или что-то сэмитить.

// ...
  private schema: FormSchema = {
    fields: [
      {
        // ...
        onChanged: this.onSelected
      },
      {
        // ...
        onChanged: this.onSelected
      }
    ]
  }
// ...
  private async onSelected() {
    // Какая-то логика
  }
// ...

Однако до vue 2.7 внутри объектов можно было обращаться к пропсам и сторам (если смаппить их в methods).

Логическая композиция

До появления composition API единственным способом создать переиспользуемую логику компонентов были миксины, у них есть существенные недостатки:

  • Пересечение пространств имён

  • Сложность типизации

  • Непрозрачность требований (например если миксин требует наличия определённых пропсов или методов у компонента)

  • Сложность понимания и отладки: когда вы обращаетесь ко всем методам и объектам через this, может быть затруднительно понять откуда он взялся: из самого объекта, из стора или из какого-то миксина.

Только в одном случае миксины могут дать некоторое преимущество: если требуется создать переиспользуемые пропсы/эмиты, вот тут решение с composable будет слегка сложнее, далее я покажу эту проблему на примере библиотеки vue-form-generator.

Шаги для миграции

Обновление vue

В первую очередь я обновил сам vue до версии 2.7, и сразу у меня перестала работать часть компонентов, в которых использовались пропсы при объявлении внутренних объектов, как в примере выше. Я решил сразу переписать такие компоненты на composition API, но можно и попробовать забирать нужные пропсы в хуках жизненного цикла.

Vuex / pinia

На очереди хранилище, в целом тут не возникло особых сложностей, я подключил pinia и постепенно переписал все модули vuex. Если вы тоже использовали VuexSmartModule и не хотите переписывать все компоненты, то pinia сторы тоже можно смаппить следующим образом:

const Mappers = Vue.extend({
  computed: {
    // Либо mapState/mapActions из pinia
    ...mapStores(useUserStore /*, и другие*/)
  }
})

@Component
export default class SomeComponent extends Mappers {
// ...
}

Для сравнения покажу вышеописанный стор для тостов, теперь уже на pinia.

toasts_store.ts

...
export const useToastStore = defineStore('toast', () => {
  const count = ref(0),
    toasts = ref([])

  function pushToast(toast: Toast) {
    toasts.value.push({ ...toast, id: count.value++ })
  }
  
  function delToast(id: number) {
    toasts.value = toasts.value.filter((p) => p.id !== id)
  }

  return {
    count,
    toasts,
    pushToast,
    delToast
  }
})

Единственной проблемой стала необходимость доступа к сторам в роутере, а точнее в нашем проекте для запуска приложения использовался следующий кусок кода:

store.dispatch('user/fetchCurrentUser').then(() => {
  new Vue({
    router,
    store,
    // прочие плагины
    render: h => h(App)
  }).$mount('#app')
})

То есть до начала работы приложения требовалось получить данные пользователя для использования их в роутере.

Возможно правильнее было бы вынести загрузку пользователя в middleware роутера, но оказалось что у нас много где данные хранилищ используются ещё и в redirect хуках роутера, которые не могут быть асинхронными, соответственно там загружать данные не получится.

Я выкрутился из этого следующим образом: до создания основного инстанса Vue с корневым компонентом приложения создал пустой инстанс с одной лишь pinia.

// Всё это внутри асинхронной функции

const pinia = createPinia()

// Фиктивный инстанс vue для инициализации хранилища
const piniaLoadApp = new Vue({ pinia })

await useUserStore().fetchCurrentUser()

new Vue({
  pinia,
  router,
  i18n,
  render: (h) => h(App)
}).$mount('#app')

piniaLoadApp.$destroy()

Соответственно после этого мы получим доступ ко всем сторам.

BootstrapVue

В целом работа с данной библиотекой не изменилась, а чтобы получить доступ к объектам $bvToast и $bvModal в 

VueFormGenerator

VueFormGenerator — небольшая библиотека для генерации форм с помощью JSON схем, она больше не поддерживается разработчиками и в целом мне не очень нравится, но выкинуть из проектов её пока что не получилось, так как на ней у нас сделаны все формы.

Для её адаптации к composition API пришлось сделать громоздкий composable на основе их миксина abstractField, а для переиспользования пропсов и эмитов я сделал объекты, передаваемые в defineProps и defineEmits. Кода там много, поэтому просто оставлю ссылку на gist.

Простейшее кастомное поле (обычный лэйбл) с использованием данного composable:



Чего мы добились

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

Особым приятным бонусом стала возможность использовать библиотеку VueUse, с помощью которой например получилось заменить стор для размера окна и миксин для отслеживания изменения его размера, всего одной функцией useWindowSize.

Чуть больше про сам процесс миграции можете прочитать в этой статье.

© Habrahabr.ru