Cómo actualizamos un gran servicio interno y qué resultó de él / Sudo Null IT News

¡Hola Habr!

Mi nombre es Egor Prokopyev y soy desarrollador front-end en Ozon.

La tercera versión del muy querido marco Vue ha estado disponible durante mucho tiempo y la mayoría de quienes lo utilizan se han actualizado a la nueva versión. Pero siempre habrá personas como nuestro equipo que dejarán esta transición en el último cajón de la deuda técnica; después de todo, ¡hay más tareas prioritarias! Sin embargo, tarde o temprano llega este día, y ahora nos ha llegado.

En este artículo quiero contarles cómo cambiamos a una nueva versión a pesar de que el servicio es bastante grande y muy importante para los usuarios. Pero primero lo primero.

Descripción del servicio

Nuestro equipo está desarrollando servicios internos para trabajar con pedidos. En particular, nos dedicamos a nuestras propias ventas, es decir, se trata de pedidos de productos que el propio Ozon encarga a un proveedor, almacena y vende de forma independiente en su propio nombre. De hecho, para organizar todo este flujo de trabajo, estamos desarrollando servicios internos, que a su vez son utilizados por los directivos.

La mayoría de nuestros servicios están escritos utilizando el marco Vue, pero para mayor comodidad utilizamos Nuxt.

Plataforma frontal

Como la mayoría de las grandes empresas, tenemos nuestra propia plataforma front-end, que es la llamada base de cualquier servicio nuevo. Está diseñado para salvarnos de reinventar la “bicicleta” y contiene todo tipo de funciones, lo que le permite comenzar a escribir la interfaz de un nuevo servicio muy rápidamente y sin ningún problema. Pero apareció más tarde que nuestro servicio, y esto llevó al hecho de que escribimos nuestra interfaz en Nuxt2 con nuestras propias “bicicletas”. Y, como suele suceder, todo se redujo a que ahora, además de pasar de la segunda versión a la tercera, debemos trasladar todo lo que hemos desarrollado por nuestra cuenta, apoyándonos en las soluciones de la plataforma. .

¿Por qué decidimos actualizar?

  1. EOL (fin de vida) oficial de Vue 2, que llegó el 31/12/2023. Esto significa que ya no recibe nuevas versiones, actualizaciones o correcciones.

  2. Vite es una alternativa rápida, potente y fácil de usar al paquete web.

  3. Nuevo Vue – nuevas características:

    1. Optimización y rendimiento mejorados: nueva vibración de árboles, renderizado más rápido, tamaño de paquete más pequeño.

    2. Composición API.

    3. Soporte completo de TypeScript.

    4. Teletransporte incorporado.

    5. Soporte del concepto de suspenso.

  4. Falta de soporte en desarrollos internos por no estar incluido en la plataforma.

  5. Actitud positiva por parte de la dirección para invertir tiempo en actualización.

Inicio de la transición

Bueno, ¡comencemos!

Para empezar cabe destacar que aquí sólo hablaré de uno de nuestros servicios. Ya mencioné que estamos desarrollando varios servicios, así que cuando comenzamos a transferir este servicio a una nueva versión del framework, creamos varios proyectos desde cero en Nuxt3. Esto sin duda nos dio experiencia y algunos conocimientos sobre la nueva versión del framework, lo que luego nos ayudó a traducir este servicio. También me gustaría mencionar que para todos nuestros servicios tenemos nuestra propia biblioteca de soluciones de código, componentes y soluciones de servicios comunes. Obviamente, esta biblioteca también se tradujo a una nueva versión del marco. Se compone principalmente de componentes ordinarios, por lo que su migración consistió únicamente en transferir componentes a una nueva versión del framework.

El principal problema es saber por dónde empezar. Debido a que Ozon es una gran empresa con una gran cantidad de comandos e interfaces, los colegas del equipo de la plataforma crearon para nosotros una herramienta de traducción de proyectos: codemod. En Internet puedes encontrar un montón de codemods que te ayudan a migrar de una versión de una biblioteca o lenguaje a otro, pero cada uno de ellos tiene sus propias características y requisitos para el estado inicial del código. Esto también sucedió en nuestro caso. El codemod desarrollado por el equipo de la plataforma funcionó bastante bien con aquellos proyectos que se escribieron originalmente usando la plataforma. Pero nuestro proyecto se implementó utilizando nuestros propios desarrollos, por lo que todas las partes del servicio traducidas por codemod tuvieron que ser editadas exhaustivamente para que finalmente funcionaran. Por lo tanto, se decidió iniciar el proyecto desde cero utilizando la plataforma, ya que la tenemos (pero en su ausencia, naturalmente comenzaremos simplemente iniciando un nuevo proyecto en una nueva versión).

Bien, el proyecto ha sido iniciado, ¿qué sigue? ¿Por dónde empezar?

Todos deciden por sí mismos cómo les resulta más fácil traducir un proyecto grande, pero decidimos comenzar traduciendo nuestro repositorio. Esto nos pareció práctico por varias razones:

  1. La tienda es bastante grande y se utiliza en muchos lugares. En nuestro proyecto, la tienda es especialmente importante, ya que contiene toda la lógica principal para trabajar con datos. Inicialmente, nuestro servicio tenía sólo una página que mostraba una lista de pedidos. Era posible realizar varias manipulaciones con cada pedido, desde ajustes hasta agregar/cambiar/eliminar un comentario. Y por lo tanto, se decidió dividir toda la tienda en módulos, donde cada módulo era responsable de su propia funcionalidad, adjunta directamente a los pedidos (puedes ver todos los módulos en la siguiente ilustración). Por ejemplo, demand-lists-filters es responsable de los filtros y demand-lists-reports es de los informes generados.

  2. Después de traducir páginas/componentes, inmediatamente podremos comprobar la funcionalidad de la parte traducida.

  3. No tenemos que traducir el código que funciona con nuestras API porque la plataforma lo hace por nosotros. Además, todos los métodos y tipos se generan automáticamente utilizando el paquete swagger-typescript-api, que le permite trabajar cómodamente con Rest API, siempre que escriba en TypeScript. Este paquete de punto final genera métodos y los tipos utilizados en esos métodos para trabajar rápidamente con las API. La única condición importante es que la API tenga un contrato arrogante. Si quieres saber más, puedes ir a repositorio-github y leer más.

Cambiar a Pinia

Nuestra tienda, escrita en Vuex, constaba de una cantidad bastante grande de módulos. Para ser precisos, de 48.

Anteriormente usamos Vuex de una manera inusual, usando clases con decoradores y otras características. Ahora hemos cambiado a Pinia, ya que se ha convertido en la biblioteca oficial de administración estatal en Vue 3, lo que proporciona una alternativa más liviana pero igualmente fácil de usar a Vuex.

Las principales ventajas de Pinia que noté:

  • Facilidad de uso: trabajar con Pinia es simple, todo es intuitivo e incluso un especialista novato puede comprender rápidamente qué y cómo funciona.

  • Soporte TypeScript listo para usar.

  • Un sistema de tienda simple y flexible en lugar de módulos y espacios de nombres Vuex.

  • Excelente integración con DevTools.

A continuación puede ver las diferencias al escribir el mismo módulo, pero utilizando bibliotecas de almacenamiento de diferentes estados. Dicho esto, aquí puedes ver otra ventaja de usar tanto Nuxt3 como Pinia.

La ventaja de Pinia es que ya no existe un concepto tan claramente definido de cambiar de estado mediante mutaciones, como en Vuex en Nuxt2, y puedes cambiar el estado en cualquier función sin pérdida de reactividad.

La ventaja de Nuxt3 (o más precisamente, la Composition Api, que se introdujo en Vue 3) se puede ver aquí si prestas atención al trabajo con tokens de cancelación de solicitudes. Para trabajar con la API, utilizamos tokens que, a su vez, deben actualizarse y mediante los cuales se deben detener las solicitudes si es necesario. Y anteriormente, para cada solicitud, creábamos un token separado y una mutación separada para trabajar con él, pero ahora hemos escrito un elemento componible simple que lleva estas funciones, lo que ha hecho la vida con ellas más fácil y hay menos código.

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,
    }
})

Luego simplemente acumulamos paciencia y transferimos poco a poco cada módulo de almacenamiento, intentando no olvidar nada por ningún lado.

Al traducir el repositorio, a menudo nos encontramos con el hecho de que es necesario utilizar notificaciones que se implementaron utilizando el complemento Nuxt, por lo que para una traducción más cómoda, primero traduciremos todos los complementos. No hay nada complicado en transferir complementos de la segunda versión a la tercera. A diferencia de la segunda versión, ahora necesita usar la función defineNuxtPlugin, que acepta una función con un solo argumento: nuxtApp, y en lugar de la función inyectar, ahora necesita usar la función proporcionar. De lo contrario, no debería haber dificultades durante la traducción. Una cosa que vale la pena mencionar es que si usa Nuxt y no registra complementos explícitamente en nuxt. configuración. ts(js): Nuxt le permite no conectar complementos explícitamente, es decir, todo lo que se encuentre en la carpeta de complementos en la raíz del proyecto se conectará automáticamente. Y debes tener en cuenta que los complementos están conectados en orden alfabético. Y si necesita, por ejemplo, que un determinado complemento se inicialice tras otro, conéctelos explícitamente a través de la configuración o asígneles un nombre para que se ubiquen en la secuencia deseada.

Decidimos que sería más fácil cambiar el nombre de los archivos del complemento, por lo que agregamos prefijos de letras para el orden que necesitábamos para conectarlos.

Traducción de componentes y páginas.

Habiendo traducido el almacenamiento y los complementos (y al mismo tiempo todo tipo de elementos auxiliares como middlewares, etc.), pasamos a la parte interesante, es decir, las páginas y los componentes. Aquí, como en cualquier otro lugar, cada uno decide por sí mismo cómo le conviene más hacer una traducción, pero a nosotros se nos ocurrió la siguiente secuencia, que resultó ser la más conveniente y productiva para nosotros:

  1. Traducimos todos los componentes comunes que se utilizan en otros componentes/páginas.

  2. Vamos página por página y primero traducimos todos los componentes relacionados con esta página y luego la página misma. En nuestro caso tampoco hubo problemas especiales. Pero mencionaré que en Nuxt3 (Vue 3) el sistema de reactividad ha cambiado un poco. Ahora está construido sobre Proxy, por lo que los métodos Vue.set y Vue.delete, cuyo uso era necesario para trabajar con objetos y matrices, ya no son necesarios. En Vue3 tenemos dos tipos de declaraciones de estado reactivo: ref y reactiva. Las principales diferencias entre ellos:

    1. Ref se usa principalmente para valores primitivos y reactivo se usa para objetos, matrices y colecciones de mapas/conjuntos.

    2. Para acceder al valor de un estado declarado vía ref, debemos acceder a la propiedad value, y con reactivo el acceso se produce como si fuera una variable regular.

    3. Hay una serie de otras características que vale la pena considerar, pero todas están bien descritas en documentación. Pruebe bien todas las funciones después de la migración para asegurarse de que todo funcione según lo previsto.

Luego, con la misma mesura, cuidado y criterio (como con los módulos de la tienda), traducimos componente por componente para finalmente traducirlo todo. Tenemos alrededor de 231 componentes, que tradujimos en aproximadamente 1,5 semanas, así que no te desesperes, ¡todo es posible!

Mediciones y conclusiones

Cuando hayamos traducido todo, podremos ver cómo funciona para nosotros y, lo más importante, comprobar cuánto ha cambiado la velocidad de implementación (compilación).

En primer lugar, vale la pena señalar que el servidor de desarrollo local en realidad se inicia mucho más rápido en la combinación Vite + Nuxt3 que en el Webpack + Nuxt2 utilizado anteriormente. Además, no se olvide de HMR de Vite, que no reconstruye el paquete, sino que solo actualiza el módulo afectado, es decir, no provoca una recarga de la página ni un reinicio de todos los estados, lo que, a su vez, también, aunque sea un poco, ahorra tiempo y nervios al desarrollador.

En segundo lugar, veamos qué pasó con la compilación y cómo ha cambiado el tiempo que llevó entregar el código a producción.

Y aquí notaremos que antes (en Nuxt2 + Webpack) podíamos ver los siguientes valores:

De los valores presentados vemos que el tiempo de implementación promedio (columna TTM) es de ~ 7-8 minutos y, a veces, incluso aumenta a 14 minutos.
Después de cambiar a la nueva versión obtuvimos los siguientes resultados:

Vemos que el valor medio, aunque ligeramente, disminuyó hasta ~6 minutos. Sí, hay saltos de hasta 7-8, pero, lamentablemente, el tiempo de implementación depende de muchos otros factores.

En vista de todo lo anterior, concluimos que cambiar a una nueva versión de Vue (Nuxt) no sólo acelera la implementación, sino que también acelera el tiempo de desarrollo. También agrega comodidad y confort al desarrollador que posteriormente trabajará con el proyecto. La aceleración y la comodidad del desarrollador están garantizadas por las mismas innovaciones de Vue3 sobre las que escribí anteriormente, pero para resumir:

  1. Composition Api nos permite escribir código de componentes modulares y flexibles. Esto nos permite estructurar mejor nuestro código, reutilizar la lógica y, en general, hace que nuestro código sea más legible.

  2. TypeScript listo para usar, lo que ahorra tiempo al modificar configuraciones e instalar paquetes adicionales, y le permite comenzar a escribir código de inmediato. La escritura y el autocompletado también se han mejorado en general.

  3. Reactividad más transparente.

  4. Lanzamiento y montaje más rápidos.

  5. Pinia es liviano, rápido y fácil de entender incluso para quienes no son expertos en Vue (y tiene soporte completo para TypeScript).

  6. Complemento Vue Devtools conveniente y rápido para depurar.

Entre otras cosas, migrar a una nueva versión es una buena razón para deshacerse del legado. En nuestro caso, al traducir una biblioteca común de componentes, nos deshicimos de un montón de código innecesario y no optimizado, lo que tuvo un efecto positivo en la compilación y la implementación. Y, por supuesto, no debemos olvidarnos de la relevancia. Con esto quiero decir que la nueva versión se está desarrollando activamente, aparecen nuevas funciones y nuevas bibliotecas que tarde o temprano pueden ayudarlo a resolver un problema en particular.

De nuestras observaciones, puedo notar que ahora, después de la actualización, hemos comenzado a implementar tareas de diversos grados de complejidad más rápidamente. Si antes una tarea media tardaba entre 3 y 4 días, ahora la completamos en 2 o 3 días. La velocidad de implementación ha aumentado, lo que ha permitido en repetidas ocasiones realizar las revisiones necesarias y, en general, realizar lanzamientos más rápidos.

Por lo tanto, si todavía se pregunta si actualizar o no, o no lo piensa en absoluto, o le atormentan las dudas de que actualizar a una nueva versión del marco sea solo una pérdida de tiempo y recursos, entonces espero nuestro ejemplo puede darle confianza. Después de todo, cambiar a una nueva versión del marco significa no solo actualizar el número en package.json, sino también un montón de nuevas características interesantes, lo que aumenta la velocidad y la comodidad durante el desarrollo.

¡Les deseo a todos éxito en esto!

Publicaciones Similares

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *