Vue 3 в деле: Как мы обновили большой внутренний сервис и что из этого вышло

от автора

Привет, Хабр!

Меня зовут Егор Прокопьев, и я фронтенд-разработчик в Ozon.

Третья версия полюбившегося многими фреймворка Vue вышла уже давно, и большинство использующих его произвели обновление до новой версии. Но всегда будут такие, как наша команда, кто откладывал этот переход в дальний ящик техдолга — ведь есть более приоритетные задачи! Однако рано или поздно этот день настаёт, и вот и для нас он пришёл.

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

Описание сервиса

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

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

Фронтенд-платформа

Как и в большинстве крупных компаний, у нас есть своя фронтенд-платформа, которая является так называемым фундаментом для любого нового сервиса. Она призвана избавить нас от изобретения «велосипеда» и несёт в себе всевозможные фичи, что позволяет очень быстро начать писать интерфейс нового сервиса без каких-либо проблем. Но она появилась позже, чем наш сервис, и это привело к тому что свой интерфейс мы писали на Nuxt2 со своими «велосипедами». И, как это обычно бывает, всё свелось к тому, что теперь, помимо того, что надо перейти с второй версии на третью, так и всё, что мы нарабатывали своими силами, переносить, опираясь на платформенные решения.

Почему мы решили обновиться?

  1. Официальный EOL (end of life) Vue 2, который наступил 31.12.2023. Это означает, что он больше не получает новых версий, обновлений и исправлений.

  2. Vite — быстрая, мощная и простая в обращении альтернатива webpack.

  3. Новый Vue — новые фичи:

    1. Улучшенная оптимизация и производительность — новый tree-shaking, быстрый рендеринг, меньший размер собранного бандла.

    2. Composition Api.

    3. Полная поддержка TypeScript.

    4. Встроенный Teleport.

    5. Поддержка концепции Suspense.

  4. Отсутствие поддержки во внутренних наработках, потому что не входит в платформу.

  5. Положительный настрой руководства на инвестицию времени в обновление.

Начало перехода

Что ж, приступаем!

Для начала стоит отметить, что здесь я расскажу только про один из наших сервисов. Я уже упоминал, что мы занимаемся разработкой нескольких сервисов, поэтому когда мы приступили к переводу данного сервиса на новую версию фреймворка, нами с нуля были сделаны несколько проектов на Nuxt3. Это, несомненно, дало нам опыт и некоторые знания по новой версии фреймворка, которые потом помогли нам при переводе этого сервиса. Также хочу упомянуть, что для всех наших сервисов у нас есть своя библиотека общих кодовых решений, компонентов, сервисных решений. Данная библиотека также заведомо была переведена на новую версию фреймворка. В основном она состоит из обычных компонентов, поэтому её миграция заключалась лишь в переводе компонентов на новую версию фреймворка.

Основная проблема — это понять, с чего же начать. Ввиду того, что Ozon — большая компания, с большим количеством команд и интерфейсов, коллеги из команды платформы создали для нас инструмент перевода проектов — кодмод. На просторах интернета можно найти кучу таких кодмодов, которые помогают мигрировать с одной версии библиотеки, языка на другую, но каждая из них имеет свои особенности и требования к начальному состоянию кода. Так случилось и в нашем случае. Кодмод, разработанный командой платформы, достаточно хорошо работал с теми проектами, которые изначально были написаны с использованием платформы. Но наш проект был реализован с помощью своих наработок, поэтому все переведённые кодмодом части сервиса приходилось сильно редактировать, чтобы они в итоге стали рабочими. Поэтому было принято решение инициировать проект с нуля с помощью платформы, благо она у нас есть (но в её отсутствие мы естественно начнём с простой инициации нового проекта на новой версии).

Хорошо, проект инициировали, а что дальше? С чего начать?

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

  1. Стор довольно массивный и много где используется. В нашем проекте стор имеет особенно важное значение, так как вся основная логика работы с данными заключена именно в нём. Изначально в нашем сервисе была лишь одна страница с отображением списка заказов. С каждым заказом можно было проводить различные манипуляции — от корректировки до добавления/изменения/удаления комментария. И поэтому было принято решение побить весь стор на модули, где каждый модуль отвечал за свою функциональность, прилагаемую непосредственно к заказам (увидеть все модули можно на иллюстрации, представленной ниже). Например, demands-lists-filters отвечает за фильтры, а demands-lists-reports — за формируемые отчёты.

  2. После перевода страниц/компонентов мы сразу сможем проверить работоспособность переведённой части.

  3. У нас нет необходимости переводить код, который работает с нашими API, потому что это сделано за нас платформой. Вдобавок все методы и типы автоматически генерируются с помощью пакета swagger-typescript-api, который позволяет удобно работать с Rest API при условии, что вы пишете на TypeScript. Этот пакет по эндпойнту генерирует методы и типы, используемые в этих методах, для быстрой работы с API. Единственным важным условием является наличие swagger-контракта у API. Если вам захотелось узнать больше, можете перейти в github-репозиторий и почитать подробней.

Переход на Pinia

Наш стор, написанный на Vuex, состоял из достаточно большого количества модулей. Если быть точным — из 48.

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

Основные преимущества Pinia, которые мне удалось заметить:

  • Простота использования — работать с Pinia просто, всё интуитивно понятно, и даже начинающий специалист сможет быстро понять, что и как устроено.

  • Поддержка TypeScript из коробки.

  • Простая, гибкая система сторов (stores), вместо модулей и пространств имён Vuex.

  • Отличная интеграция с DevTools.

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

Преимущество Pinia заключается в том, что здесь уже нет такой чётко поставленной концепции изменения состояний через мутации, как в Vuex на Nuxt2, и можно без потери реактивности менять состояние в любой функции.

Преимущество Nuxt3 (а если быть точнее, Composition Api, которое представлено во Vue 3) здесь видно, если обратить внимание на работу с токенами отмены запросов. Для работы с API, мы используем токены, которые, в свою очередь, нужно обновлять, и по которым нужно останавливать запросы при необходимости. И ранее для каждого запроса мы заводили отдельный токен и отдельную мутацию для работы с ним, теперь же мы написали простенький composable, который несёт в себе эти функции, что сделало жизнь с ними проще, а кода стало меньше.

Vuex + Nuxt2
import { VuexModule, Module, VuexMutation, VuexAction, InjectNuxtContext, WithNuxtContext } from '@gdz/types' import axios from 'axios'  import { NuxtAppContext } from '~/types' import { IUsersStore, ProcessedUser } from '~/types/common/users' import { UserInfoType } from '~/api/types' import { getProcessedUsers } from '~/utils/users'  @Module({     stateFactory: true,     namespaced: true, }) export default class Users extends VuexModule implements WithNuxtContext<IUsersStore> {     lib!: NuxtAppContext      cancelToken: IUsersStore['cancelToken'] = axios.CancelToken.source()      availableUsers: IUsersStore['availableUsers'] = []      @VuexMutation     saveUsers(users: UserInfoType[]) {         this.availableUsers = users     }      @VuexMutation     createCancelToken() {         this.cancelToken = axios.CancelToken.source()     }      @VuexAction     @InjectNuxtContext     async getUsers() {         if (this.availableUsers.length > 0) {             return         }         try {             this.cancelToken.cancel()             this.context.commit('createCancelToken')             const { users } = await this.lib.$api.demandsLists.getDemandsListsUsers(this.cancelToken.token)             this.context.commit('saveUsers', users)         } catch (error) {             if (axios.isCancel(error)) {                 return             }             console.error(error)         }     }      get processedAvailableUsers(): (UserInfoType & ProcessedUser)[] {         return getProcessedUsers(this.availableUsers)     } }

Pinia + Nuxt3
import axios from 'axios' import { defineStore } from 'pinia' import { ref, computed } from 'vue'  import { getApi } from '~/api/client' import type { IUsersStore, ProcessedUser } from '~/types/common/users' import { getProcessedUsers } from '~/utils/users' import { useCancelToken } from '~/utils/store' import type { DemandListUserType } from '~/api/types'  export const useUsersStore = defineStore('users', () => {     const api = getApi()      const cancelToken = useCancelToken()      const availableUsers = ref<IUsersStore['availableUsers']>([])      async function getUsers() {         if (availableUsers.value.length > 0) {             return         }         try {             cancelToken.recreateCancelToken()             const { users } = await api.gdzApiGateway.demandsLists.getDemandsListsUsers(cancelToken.cancelToken.value.token)             availableUsers.value = users || []         } catch (error) {             if (axios.isCancel(error)) {                 return             }             console.error(error)         }     }      const processedAvailableUsers = computed<(DemandListUserType & ProcessedUser)[]>(() => {         return getProcessedUsers(availableUsers.value)     })      return {         availableUsers,         getUsers,         processedAvailableUsers,     } })

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

Переводя хранилище, натыкаемся часто на то, что необходимо использовать уведомления (нотификации), которые были реализованы с помощью Nuxt-плагина, поэтому для более комфортного перевода, сделаем сначала перевод всех плагинов. Ничего хитрого в переводе плагинов со второй версии на третью нет. В отличие от второй версии, теперь нужно использовать функцию defineNuxtPlugin, которая принимает функцию с одним лишь аргументом — nuxtApp, ну и вместо функции inject теперь нужно использовать функцию provide. В остальном сложностей при переводе быть не должно. Стоит лишь упомянуть про один момент, что если вы используете Nuxt и явно не прописываете плагины в файле nuxt. config. ts(js) — Nuxt позволяет не подключать явно плагины, то есть всё, что размещено в папке plugins в корне проекта будет подключаться автоматически. И нужно иметь в виду, что плагины подключаются в алфавитном порядке. И если вам необходимо, например, чтобы какой-то определённый плагин инициализировался после другого, то либо подключайте их явно через конфиг, либо именуйте так, чтобы они располагались в нужной последовательности.

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

Перевод компонентов и страниц

Переведя хранилище и плагины (а заодно и всякие вспомогательные штуки типа middlewares и т.д.) мы переходим к самому сладкому, а именно к страницам и компонентам. Здесь, как и в любом другом месте, каждый сам решает, как ему удобнее сделать перевод, но мы вывели следующую последовательность, которая для нас оказалась максимально удобной и продуктивной:

  1. Переводим все общие компоненты, которые используются в других компонентах/страницах.

  2. Идём постранично и переводим сначала все компоненты, которые относятся к этой странице, а потом уже и саму страницу. В нашем случае здесь также не было никаких особых проблем. Но упомяну, что в Nuxt3 (Vue 3) немного изменилась система реактивности. Теперь она построена на Proxy, поэтому методы Vue.set и Vue.delete, использование которых необходимо было для работы с объектами и массивами, теперь не нужны. В Vue3 у нас есть два вида объявления реактивного состояния — это ref и reactive. Основные отличия между ними:

    1. Ref преимущественно используется для примитивных значений, а reactive — для объектов, массивов, и коллекций Map/Set.

    2. Для доступа к значению состояния, объявленного через ref, мы должны обращаться к value-свойству, а при reactive обращение происходит, как к обычной переменной.

    3. Есть ещё ряд особенностей, которые стоит учитывать, но они все хорошо описаны в документации. Хорошо тестируйте весь функционал, после миграции, чтобы быть уверенными в том, что все работает так, как вы и задумали.

Далее все так же размеренно, внимательно и рассудительно (как и с модулями стора) переводим компонент за компонентом, чтобы в конечном итоге перевести все. У нас это примерно 231 компонент, которые мы переводили примерно 1,5 недели, поэтому не отчаивайтесь, все возможно!

Замеры и выводы

Когда мы все перевели, мы можем посмотреть, как это у нас работает, а самое главное — проверить, насколько изменилась скорость деплоя (сборки).

Во-первых, стоит отметить, что локальный dev-сервер действительно запускается намного быстрее в связке Vite + Nuxt3, чем ранее используемые Webpack + Nuxt2. Плюс не забываем про HMR от Vite, которые не перестраивает бандл, а лишь обновляет затронутый модуль, т. е не приводит к перезагрузке страницы и сбросу всех состояний, что тоже, в свою очередь, хоть и немного, но сохраняет нервы и время разработчику.

Во-вторых, проверим, что произошло со сборкой и как изменилось время доставки кода в продакшн.

И тут мы заметим, что ранее (на Nuxt2 + Webpack) мы могли видеть следующие значения:

Из представленных значений видим, что среднее значение времени деплоя (столбец TTM) ~ 7-8 минут, и даже иногда поднималось до 14 минут.
После перехода на новую версию получили такие результаты:

Видим, что среднее значение хоть и незначительно, но уменьшилось до ~ 6 минут. Да, бывают скачки до 7-8, но, к сожалению, время деплоя зависит и от множества других факторов.

Ввиду всего вышесказанного делаем такой вывод, что переход на новую версию Vue (Nuxt) нам не только ускоряет деплой, но и ускоряет время разработки. А также добавляет удобства и комфорта разработчику, который будет впоследствии работать с проектом. Ускорение и комфорт разработчика обеспечиваются всё теми же новшествами Vue3, о которых я писал ранее, но резюмирую:

  1. Composition Api позволяет нам писать гибкий модульный код компонентов. Это позволяет нам лучше структурировать код, повторно использовать логику и в целом делает код более читаемым.

  2. TypeScript из коробки, что экономит время на модификацию конфигов и установку дополнительных пакетов, а разрешает сразу приступать к написанию кода. Также улучшена в целом типизация и автодополнения.

  3. Более прозрачная реактивность.

  4. Более быстрые запуск и сборка.

  5. Pinia — лёгкая, быстрая и понятная даже неспециалисту по Vue (и полная поддержка TypeScript).

  6. Удобный и быстрый Vue Devtools-плагин для отладки.

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

Из наших наблюдений могу отметить, что теперь, после обновления, мы стали быстрее реализовывать задачи разной степени сложности. Если ранее на среднюю задачу уходило по 3–4 дня, то теперь мы справляемся за 2–3 дня. Скорость деплоя увеличилась, что неоднократно позволяло делать необходимые хотфиксы, да и в целом делать релизы быстрее.

Поэтому, если вы всё ещё думаете, обновляться или нет, или вы вообще об этом не думаете, или вас терзают сомнения, что переход на новую версию фреймворка — это лишь трата времени и ресурсов, то, надеюсь, наш пример сможет придать вам уверенности. Ведь переход на новую версию фреймворка — это не только обновление числа в package.json, но и куча новых прикольных фич, увеличение скорости и комфорта при разработке.

Желаю всем успехов в этом!


ссылка на оригинал статьи https://habr.com/ru/articles/844074/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *