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

Меня зовут Егор Прокопьев, и я фронтенд-разработчик в 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, но и куча новых прикольных фич, увеличение скорости и комфорта при разработке.

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

Комментарии (20)


  1. BerkutEagle
    20.09.2024 08:40
    +1

    Скорость деплоя это конечно хорошо, а как на счёт скорости работы? Есть сравнение производительности итогового продукта?


    1. EgorkaMeow Автор
      20.09.2024 08:40

      Специально мы никакие тесты по производительности не делали, так как это была не первопричина нашего обновления. Но есть скриншоты из вкладки "perfomance" chrome devtools:

      Vue3/Nuxt3
      Vue3/Nuxt3
      Vue2/Nuxt2
      Vue2/Nuxt2

      Замеры, конечно, сделаны не при "идеальных" условиях, но всё равно видно, что выигрыш есть и по времени и по памяти.


  1. gmtd
    20.09.2024 08:40

    Вы пишете внутренние сервисы на Nuxt?
    С SSR?


    1. Dromo
      20.09.2024 08:40

      Nuxt, в отличии от Next, умеет работать в SPA режиме


    1. grevling95
      20.09.2024 08:40

      Nuxt вроде как не обязательно SSR, это по сути связка вью + сервер, на котором можно прокси роутинг написать


    1. EgorkaMeow Автор
      20.09.2024 08:40

      Да, внутренние сервисы мы пишем на Nuxt
      В нашем случае в SSR нет необходимости, поэтому мы пишем сервисы в SPA режиме


      1. gmtd
        20.09.2024 08:40

        Что вас привлекло в нем?

        Относительно Vue


        1. EgorkaMeow Автор
          20.09.2024 08:40

          Vue - основной фреймворк в Ozon для написания интерфейсов. Почему выбрали именно его, я достоверно ответить не могу (это было ещё до моего прихода в Ozon). Cмею предположить, что он больше подходил под необходимые задачи, вот его и выбрали


          1. gmtd
            20.09.2024 08:40

            Я не точно спросил - имелось ввиду Nuxt
            Почему он в данном случае? Чем лучше простого Vue?


  1. sfirestar
    20.09.2024 08:40

    Спасибо за статью! Сам проходил подобный путь и ничуть не жалею, что ещё полтора года назад полностью перешёл на третью версию.


  1. dastiw1
    20.09.2024 08:40

    Ожидал увидеть как вы решили проблему параллельной разработки во время переписывания под vue@3. Вы просто остановили добавление новых фич и доработки и просто переписывали?

    И еще интересно, у вас не было проблем с юнит тестами?


    1. EgorkaMeow Автор
      20.09.2024 08:40

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

      Что касается unit-тестов, то не припомню ничего экстраординарного. Поэтому вряд ли были какие-то проблемы


      1. MORD74
        20.09.2024 08:40

        А чего сложного вводить новый функционал и переводить на vue 3? У нас намного глобальнее crm и она была написана на vue 2. И мы просто постепенно все переводили на 3 версию.


        1. EgorkaMeow Автор
          20.09.2024 08:40

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

          А так, по данному вопросу нет единого правильного ответа: каждый делает настолько, насколько позволяют ресурсы. И если у вас всё удалось – вы молодцы!


          1. MORD74
            20.09.2024 08:40

            Понял) спасибо за ответ

            Мы бы просто за пару недель не сделали. Переписывали и бэк и фронт. А это колоссальный объем работы был


  1. muba1
    20.09.2024 08:40

    Сколько времени в итоге занял переход?)


    1. EgorkaMeow Автор
      20.09.2024 08:40

      Переход в итоге занял примерно 2 недели, то есть 1 спринт)


  1. ivcoder
    20.09.2024 08:40

    Сейчас занимаюсь тем же, но мы решили не использовать axios, вместо него fetch. А т.к. для нас SEO имеет значение, все это еще и на SSR, со всеми своими нюансами. Хочется спросить, сколько времени у вас занял перевод всего проекта на Nuxt3?


    1. EgorkaMeow Автор
      20.09.2024 08:40

      При наших условиях, мы потратили на миграцию примерно 2 недели (1 спринт)