Vue.js: Cómo migrar un proyecto grande de Vue 2 a Vue 3

El equipo de Vue lanzó Vue 3.0 en septiembre de 2020. Esta nueva versión viene con muchas nuevas características, optimizaciones, pero también con algunos cambios. Descubre nuestra historia de migración.

El equipo de Vue.js lanzó Vue 3.0 en septiembre de 2020. Esta nueva versión viene con nuevas características y optimizaciones, pero también viene con algunos cambios.

En junio de 2021, el equipo de Vue.js lanzó la versión Vue 3.1. Esta nueva versión viene con @vue/compat (también conocido como "el build de migración"). Permite migrar grandes proyectos sin problemas mediante la ejecución de una base de código de Vue 2 junto con los cambios de Vue 3.

Migrar a Vue 3 puede ser una tarea importante (dependiendo del tamaño de tu proyecto). En Crisp, hemos migrado recientemente nuestra aplicación (250K líneas de código) de Vue 2.6 a Vue 3.2 en unas 2 semanas.

Preparamos esta migración leyendo el Tutorial oficial de migración a Vue, pero descubrimos muchas advertencias diferentes mientras migrábamos nuestro proyecto.

Pensamos que sería bueno compartir nuestros aprendizajes para que otras empresas puedan beneficiarse de nuestra experiencia. Este artículo te dirá:

  • Las principales diferencias entre Vue 2 y Vue 3
  • Los dilemas que nos hemos encontrado y cómo los hemos resuelto
  • Nuestra estrategia de migración a Vue.js

Algunos antecedentes de Vue 3

Creado en 2013, la filosofía inicial de Vue.js era disponer de una alternativa minimalista a AngularJS 1.

En ese momento, Angular era un framework voluminoso, con un montón de nuevas características. Las primeras versiones de Vue.js eran como un mini-angular framework con Templating, Data Binding, Filters y Directives.

Vue 2 fue lanzado en 2016 (casi al mismo tiempo que Angular 2), y ofreció una gran alternativa a AngularJS. De hecho, muchos desarrolladores que usaban Angular 1 decidieron pasarse a Vue 2 porque no les gustaba Angular 2: Vue 2 ofrecía algo tan sencillo como AngularJS, pero con mejores prestaciones.

Vue 3 se creó pensando en el rendimiento. Es una evolución más que una revolución. La mayoría de las nuevas características y cambios de última hora se han lanzado por razones de rendimiento.

El sistema interno de reactividad ha sido reelaborado desde cero, y por esta razón, se ha dejado de soportar IE 11 (lo cual no es una gran pérdida 😂).
Por último, esta nueva versión pretende ser más modular e introduce características como la API de composición.

Principales cambios en Vue 3

API global de Vue

La API global de Vue ha quedado obsoleta. Aunque sigue siendo compatible con @vue/compat, será necesario modificar la aplicación para que sea totalmente compatible con Vue 3.

Esto significa que no será posible utilizar API como Vue.set o Vue.delete. Como Vue 3 viene con un nuevo sistema de reactividad, el uso de esas APIs se vuelve inútil.

En lugar de usar Vue.set(object, key, value) tendrás que usar directamente object[key] = value. Lo mismo ocurre con Vue.delete(object, key), que puede sustituirse por delete object[key].

Filtros

Como se ha explicado anteriormente, Vue.js ha sido creado como una alternativa a Angular 1. Es por eso que los Filtros fueron soportados inicialmente.

El mayor problema de los Filtros es el rendimiento: la función filtro tiene que ejecutarse cada vez que se actualizan los datos. Por esta razón, el soporte de filtros ha sido eliminado con Vue 3.

Esto significa que no será posible usar cosas como {{ user.lastName | uppercase }} en las plantillas de Vue 3. En su lugar, tendrás que usar funciones computacionales como {{ user.lastName | uppercase }}.

En su lugar, tendrás que utilizar propiedades calculadas como {{ uppercasedLastName }} o métodos como {{uppercase(lastName)}}.

Instanciación de Vue y plugins

A partir de Vue 3, tu aplicación y plugins ya no se instancian globalmente. Esto significa que puedes tener múltiples aplicaciones Vue dentro del mismo proyecto.

Usando Vue 2:

import Vue from "vue";

new Vue({
  router,
  render: h => h(App)
}).$mount("#app");

Usando Vue 3:

import { createApp, h } from "vue";

const app = createApp({
  render: () => h(App)
});

app.use(router);

app.mount("#app");

v-if + v-for

El uso de condiciones v-if con listas v-for solía ser posible con Vue 2. Por razones de rendimiento, este comportamiento se ha desactivado en Vue 3.

A partir de Vue 3, tendrás que utilizar propiedades de lista computadas.

v-model

La API v-model ha cambiado un poco en Vue 3. En primer lugar, el valor de la propiedad ha cambiado de nombre a modelValue

<ChildComponent v-model="pageTitle" />

ChildComponent necesita ser reescrito así:

props: {
  modelValue: String // previously was `value: String`
},

emits: ['update:modelValue'],

methods: {
  changePageTitle(title) {
    this.$emit('update:modelValue', title)
  }
}


Lo mejor es que ahora es posible tener varios valores personalizados v-model, por ejemplo v-model:valueA, v-model:valueB, etc.

$emit

En Vue 2 era posible utilizar una instancia de Vue para crear un EventBus global con vm.$on y vm.$off. A partir de Vue 3, ya no es posible hacerlo, ya que vm.$on y vm.$off fueron eliminados de la instancia Vue.

Como reemplazo, sugerimos usar la librería Mitt:

mounted() {
  this.eventbus = mitt();

  eventbus.on("ready", () = {
    console.log("Event received");
  });

  eventbus.emit("ready");
}

El mismo código usando Vue 2 sería:

mounted() {
  this.$on("ready", () = {
    console.log("Event received");
  });

  this.$emit("ready");
}

Todavía es posible emitir eventos desde los componentes a sus padres, sin embargo, todos los eventos tienen que ser declarados a través de la nueva opción emit (es muy similar a la opción props existente).

Por ejemplo, si tu componente tiene una propiedad @click, emitida usando this.$emit("click"), tendrás que declarar el evento "click" en tu componente:

props: {
  name: {
    type: String,
    default: ""
  },
},

emits: ["click"], // events have to be declared here

data() {
  return {
    value: ""
  }
}


Estrategia de migración

Nuestra aplicación se llama Crisp. Es una gran aplicación de mensajería empresarial utilizada a diario por 300.000 empresas de todo el mundo. Las compañías utilizan Crisp para responder a sus clientes mediante una bandeja de entrada de equipo que centraliza el chat, correos electrónicos y muchos otros canales.

Como nuestra aplicación actual es utilizada por muchos usuarios diferentes, obviamente es importante para nosotros no romper nada. Por lo tanto, también necesitábamos mejorar nuestro software a diario, para poder iterar sobre errores y nuevas funciones.

Así que necesitábamos tener dos ramas diferentes:

  • nuestra rama principal, para Vue 2.6, con nuestro ciclo de vida de lanzamiento actual
  • una rama vue3, para trabajar en la migración del código base a Vue 3

Además, publicamos todos los días una beta que ejecuta nuestra base de código Vue 3, y respaldamos casi todos los cambios realizados en la rama Vue 2 en la rama Vue 3, para evitar tener dolores de cabeza una vez fusionada la base de código Vue 3.

Por último, decidimos no depender de las nuevas API de Vue, como la API de composición, y migrar sólo lo necesario. El motivo era reducir el riesgo de introducir regresiones.

Actualización de las herramientas de compilación de Vue

Nuestra aplicación se basa en Vue Cli (webpack). Una vez más, optamos por no migrar todavía a Vite, para evitar la introducción de nuevos problemas, ya que nuestro sistema de compilación es bastante complejo.

Migrar Vue Cli para soportar Vue 3 es bastante fácil.

Primero tendrás que editar tu archivo package.json para actualizar Vue.js y sus dependencias.

Así que reemplazamos "vue "^2.6.12" por "vue": "^3.2.6".

Adicionalmente, tendremos que usar "@vue/compat": "^3.2.6" , permitiendo migrar sin problemas el código base de Vue 2 a Vue 3.

También tendremos que actualizar "vue-template-compiler": "^2.6.12" a "@vue/compiler-sfc": "^3.2.6".

Ahora, tendremos que actualizar nuestro archivo vue.config.js y editar la función chainWebpack. Esto forzará a todas tus librerías existentes a usar el paquete @vue/compat.

  // Vue 2 > Vue 3 compatibility mode
  config.resolve.alias.set("vue", "@vue/compat");

  config.module
    .rule("vue")
    .use("vue-loader")
    .loader("vue-loader")
    .tap(options => {
      // Vue 2 > Vue 3 compatibility mode
      return {
        ...options,
        compilerOptions: {
          compatConfig: {
            // default everything to Vue 2 behavior
            MODE: 2
          }
        }
      };
    });

Actualizando nuestro archivo main.js

Ahora necesitamos instanciar Vue así:

import { createApp, h, configureCompat } from "vue";

const app = createApp({
  render: () => h(App)
});

app.use(router);
app.use(store);
// Initiate other plugins here

configureCompat({
  // default everything to Vue 2 behavior
  MODE: 2
});


app.mount("#app");

Actualizando Vue Router

Tendremos que utilizar la última versión de Vue Router "vue-router": "4.0.11"

Usar la última versión de Vue.js Router no es muy diferente. La principal diferencia es que tendrás que habilitar manualmente el modo historia usando:

import { createWebHistory, createRouter } from "vue-router";

var router = createRouter({
  history: createWebHistory(),
  routes: [
    // all your routes
  ]
});


Migración de filtros

Nuestra aplicación actual depende mucho de los filtros (unos 200 usos). Lo primero fue averiguar cuántos filtros estábamos usando. Como los IDEs modernos (VSCode, SublimeText) soportan búsquedas Regex, hicimos una regex para poder encontrar todos los usos de filtros Vue en nuestras plantillas: {{ (.*?)\|(.*?) }}

Como Vue 3 abandona completamente el soporte de Filtros, intentamos encontrar una manera elegante de seguir usando Filtros. La solución fue migrar los filtros de Vue a Singletons Helpers personalizados.

Regex Search Using Sublime Text

Por ejemplo en Vue 2:

Vue.filter("uppercase", function(string) {
  return string.toUpperCase();
});

Pasa a ser en Vue 3:

uppercase(string) {
  return string.toUpperCase();
}

export { uppercase };

Luego instanciamos globalmente la clase StringsFilter:

import { uppercase } from "@/filters/strings";

app.config.globalProperties.$filters = {
  uppercase: uppercase
};

Por último, podemos utilizar nuestro filtro:

{{ firstName | uppercase }} se convierte en {{ $filters.uppercase(firstName) }}

Corregir VueJS errores

A medida que avance el proceso de migración, detectarás errores en la consola de tu navegador. El modo de compatibilidad con Vue 3 incluye diferentes registros que te ayudarán a migrar tu aplicación a Vue 3.

En lugar de intentar migrar tu aplicación característica por característica o plantilla por plantilla, te recomendamos migrar API por API.

Por ejemplo, si ves el aviso de depreciación WATCH_ARRAY en los registros, te recomendamos que eches un vistazo a la guía de migración proporcionada en los registros.

A continuación, migra paso a paso cada watch array.

Una vez migrados todos los watch arrays, puedes desactivar el modo de compatibilidad para esta función:

Vue 3 compat mode comes with explicit warnings

Actualización de bibliotecas

Vue 3 viene con el paquete @vue/compat, compatible con las bibliotecas Vue 2 y Vue 3. Sin embargo, el modo @vue/compat también viene con un rendimiento degradado.

El uso del modo de compatibilidad sólo debe hacerse durante la conversión de su aplicación a Vue 3 y todas sus bibliotecas. Una vez que todo el proyecto y las bibliotecas se han convertido, puedes deshacerte del modo compat.

Para ello, tendremos que migrar todas las librerías a compatibles con Vue 3.

Todas las librerías oficiales de Vue (Vuex, Vue Router) han sido portadas a Vue 3, mientras que la mayoría de los paquetes populares ya tienen versiones compatibles con Vue 3.

Alrededor del 20% de las bibliotecas de la comunidad que utilizamos no tenían una versión Vue 3, por lo que optamos por bifurcar todas las bibliotecas que no han sido portadas a Vue 3 todavía, por ejemplo vue-router-prefetch.

Portar una librería a Vue 3 suele ser bastante sencillo y lleva entre 30 minutos y medio día, dependiendo de la complejidad de la librería.

Una vez que terminamos de migrar una librería para que soporte Vue 3, creamos un "pull request" a la librería original, para que otros puedan beneficiarse de nuestro trabajo.

Acerca del rendimiento

Vue 3 viene con muchas mejoras de rendimiento gracias al nuevo sistema de reactividad interna. El tamaño del heap de Javascript se ha reducido del 20-30% en la mayoría de los casos y del 50% para algunas listas complejas. En nuestro caso de uso de mensajería, hemos observado grandes mejoras en la CPU.

Nuestra nueva aplicación Crisp es más rápida y ligera:


Conclusión

Mientras escribimos este artículo, estamos desplegando la nueva aplicación Crisp en producción. Migrar esta aplicación nos llevó alrededor de 2 semanas, siendo 2 desarrolladores, casi a tiempo completo.

En comparación, pasar de Angular 1 a Vue 2 nos llevó unos 9 meses.

Cambiar de Vue 2 a Vue 3 es una tarea importante pero no imposible. Esta nueva versión de Vue ofrece algunas mejoras importantes en cuanto a rendimiento.
Para agradecer a la comunidad Vue.js, hemos empezado a contribuir al proyecto como patrocinador de oro. Como empresa, sentimos que tenemos que potenciar el proyecto de código abierto para construir un mundo mejor en el que el software y el entorno puedan hablar entre sí.