Modernizar el flujo de desarrollo de aplicaciones Vue implica reemplazar herramientas obsoletas como vue-cli@3 por soluciones más ligeras y eficientes, como Vite. Aunque este artículo parte del contexto histórico de Vue CLI, su enfoque real es la construcción robusta de aplicaciones Vue mediante configuración modular, gestión de dependencias y arquitectura escalable — sin depender de scripts heredados ni estructuras rígidas.
- Alternativa Recomendada: Migrar a Vite
En lugar de instalar vue-cli globalmente (lo cual ya está desaconsejado), se recomienda usar Vite, un entorno de desarrollo extremadamente rápido basado en ESM nativo y HMR instantáneo:
npm create vite@latest my-vue-app -- --template vue
cd my-vue-app
npm install
npm run dev
Vite elimina la necesidad de webpack.config.js personalizado para casos comunes, peero mantiene total flexibilidad mediante vite.config.ts.
- Configuración Modular de Webpack (para entornos heredados)
Si aún se requiere soporte para webpack (por ejemplo, en proyectos existentes), una configuración limpia y mantenible se estructura así:
2.1 Archivo base: vite.config.ts (o webpack.config.js)
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import { resolve } from 'path';
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
'@assets': resolve(__dirname, 'src/assets'),
'@components': resolve(__dirname, 'src/components')
}
},
build: {
rollupOptions: {
external: ['vue'],
output: {
globals: {
vue: 'Vue'
}
}
}
}
});
2.2 Gestión de recursos estáticos
Para integrar HTML dinámico sin html-webpack-plugin, Vite usa index.html como punto de entrada raíz. Basta colocar:
<!-- src/index.html -->
<html lang="es">
<head>
<meta charset="UTF-8" />
<title>App Vue</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
- Transformación de Código Moderno
Para compatibilidad con navegadores antiguos, Vite utiliza @vitejs/plugin-legacy:
npm install -D @vitejs/plugin-legacy
// vite.config.ts
import legacy from '@vitejs/plugin-legacy';
export default defineConfig({
plugins: [
vue(),
legacy({
targets: ['defaults', 'not IE 11']
})
]
});
- Integración de Bibliotecas UI
En lugar de importar Element Plus globalmente (lo que aumenta el tamaño del bundle), se aplica carga diferida y registro local:
// src/plugins/element.ts
import { App } from 'vue';
import { ElButton, ElInput, ElMessage } from 'element-plus';
export function setupElement(app: App) {
app.component(ElButton.name, ElButton);
app.component(ElInput.name, ElInput);
app.config.globalProperties.$message = ElMessage;
}
Luego, en main.ts:
import { createApp } from 'vue';
import App from './App.vue';
import { setupElement } from './plugins/element';
const app = createApp(App);
setupElement(app);
app.mount('#app');
- Gestión Centralizada de Peticiones HTTP
Se evita vue-axios (obsoleto) y se implementa un cliennte personalizado con interceptores y tipado:
// src/utils/request.ts
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios';
const apiClient = axios.create({
baseURL: import.meta.env.VUE_APP_API_BASE || '/api',
timeout: 10000,
headers: { 'Content-Type': 'application/json' }
});
// Interceptor de solicitud
apiClient.interceptors.request.use(
(config: AxiosRequestConfig) => {
const token = localStorage.getItem('auth_token');
if (token && config.headers) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
// Interceptor de respuesta
apiClient.interceptors.response.use(
(response: AxiosResponse) => response,
(error) => {
if (error.response?.status === 401) {
localStorage.removeItem('auth_token');
window.location.href = '/login';
}
return Promise.reject(error);
}
);
export default apiClient;
- Enrutamiento con Vue Router 4
La configuración se separa claramente en rutas, guardias y layouts:
// src/router/index.ts
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router';
import LayoutDefault from '@/layouts/LayoutDefault.vue';
import NotFound from '@/views/NotFound.vue';
const routes: Array<RouteRecordRaw> = [
{
path: '/',
component: LayoutDefault,
children: [
{ path: '', name: 'home', component: () => import('@/views/Home.vue') }
]
},
{
path: '/user/:id',
name: 'user-detail',
component: () => import('@/views/UserDetail.vue'),
props: true // Habilita props automáticos desde parámetros
},
{ path: '/:pathMatch(.*)*', name: 'not-found', component: NotFound }
];
const router = createRouter({
history: createWebHistory(),
routes
});
// Guardia global de autenticación
router.beforeEach((to, from, next) => {
const requiresAuth = to.meta.requiresAuth as boolean | undefined;
const isAuthenticated = !!localStorage.getItem('auth_token');
if (requiresAuth && !isAuthenticated) {
next({ name: 'login', query: { redirect: to.fullPath } });
} else {
next();
}
});
export default router;
- Estado Global con Pinia (reemplazo oficial de Vuex)
Pinia ofrece tipado nativo, menor verbosidad y mejor soporte para TypeScript:
npm install pinia
// src/stores/index.ts
import { createPinia } from 'pinia';
export const pinia = createPinia();
// src/stores/counter.ts
import { defineStore } from 'pinia';
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
message: 'Hola desde Pinia'
}),
getters: {
isEven(): boolean {
return this.count % 2 === 0;
}
},
actions: {
increment() {
this.count++;
},
decrement() {
this.count--;
}
}
});
Uso en componente:
<script setup lang="ts">
import { useCounterStore } from '@/stores/counter';
const counter = useCounterStore();
</script>
<template>
<div>
<h2>Contador: {{ counter.count }}</h2>
<p>¿Es par? {{ counter.isEven ? 'Sí' : 'No' }}</p>
<button @click="counter.increment">+</button>
</div>
</template>
- Estructura de Carpetas Recomendada
src/
├── assets/ # Imágenes, fuentes, estilos globales
├── components/ # Componentes reutilizables (sin lógica de negocio)
├── composables/ # Hooks personalizados (useFetch, useAuth, etc.)
├── layouts/ # Plantillas de página (Header + Sidebar + Main)
├── router/ # Configuración de rutas y guardias
├── stores/ # Estado global (Pinia)
├── utils/ # Funciones auxiliares (request, validators, helpers)
├── views/ # Componentes de nivel de ruta (páginas completas)
├── App.vue
└── main.ts
- Buenas Prácticas Clave
- No usar
vue add: Genera código inflexible y sobrescribe archivos críticos. Prefiere instalación manual y configuración explícita. - Evitar
requireo CommonJS: Usaimportestático y ESM nativo para mejor tree-shaking y compatibilidad con Vite. - Tipado estricto: Configura
tsconfig.jsoncon"strict": truey usadefineComponentodefineSetupen composición. - Variables de entorno: Usa
import.meta.env.VUE_APP_*(Vite) oprocess.env.NODE_ENV(webpack), nunca variables globales.