Manual Completo de Vue 3 Fundamentos hasta Nuxt.js Avanzado

Guía exhaustiva desde los conceptos básicos de Vue 3 hasta aplicaciones avanzadas con Nuxt.js

Fundamentos Vue 3
Vue Intermedio
Vue Avanzado
Nuxt.js

1. Fundamentos de Vue 3

1.1 Qué es Vue.js

Vue es un framework progresivo para construir interfaces de usuario. Sus características principales incluyen:

1.2 Configuración del Entorno

Terminal
# Crear nueva aplicación con Vite (recomendado)
npm create vite@latest mi-app-vue --template vue
cd mi-app-vue
npm install

# Alternativa con Vue CLI (legacy)
npm install -g @vue/cli
vue create mi-app-vue
cd mi-app-vue

1.3 Estructura Básica de un Componente

src/App.vue
<template>
  <div>
    <h1>Hola Mundo Vue</h1>
    <p>Contador: {{ count }}</p>
    <button @click="incrementar">
      Incrementar
    </button>
  </div>
</template>

<script>
import { ref } from 'vue';

export default {
  setup() {
    const count = ref(0);

    const incrementar = () => {
      count.value++;
    };

    return {
      count,
      incrementar
    };
  }
};
</script>

<style scoped>
h1 {
  color: #42b983;
}
</style>

1.4 Sintaxis de Template

Vue usa una sintaxis de template que extiende HTML:

Interpolación

<!-- Interpolación básica -->
<p>{{ mensaje }}</p>

<!-- Expresiones JavaScript -->
<p>{{ mensaje.split('').reverse().join('') }}</p>

Directivas

<!-- v-bind (enlace de atributos) -->
<div v-bind:id="idDinamico"></div>
<!-- Forma abreviada -->
<div :id="idDinamico"></div>

<!-- v-on (manejo de eventos) -->
<button v-on:click="metodo"></button>
<!-- Forma abreviada -->
<button @click="metodo"></button>

<!-- v-model (two-way binding) -->
<input v-model="texto" type="text">

<!-- v-if, v-else-if, v-else -->
<div v-if="mostrar">Visible</div>
<div v-else>Oculto</div>

<!-- v-for -->
<ul>
  <li v-for="(item, index) in items" :key="item.id">
    {{ index }} - {{ item.nombre }}
  </li>
</ul>

1.5 Options API vs Composition API

Options API (Vue 2 estilo)

export default {
  data() {
    return {
      count: 0
    };
  },
  methods: {
    increment() {
      this.count++;
    }
  },
  computed: {
    doubleCount() {
      return this.count * 2;
    }
  },
  mounted() {
    console.log('Componente montado');
  }
};

Composition API (Vue 3)

import { ref, computed, onMounted } from 'vue';

export default {
  setup() {
    const count = ref(0);
    
    const doubleCount = computed(() => count.value * 2);
    
    function increment() {
      count.value++;
    }
    
    onMounted(() => {
      console.log('Componente montado');
    });
    
    return {
      count,
      doubleCount,
      increment
    };
  }
};

1.6 Props y Comunicación entre Componentes

src/components/Saludo.vue
<template>
  <div>
    <h2>Hola {{ nombre }}</h2>
    <p>Tienes {{ edad }} años</p>
  </div>
</template>

<script>
export default {
  props: {
    nombre: {
      type: String,
      required: true
    },
    edad: {
      type: Number,
      default: 18
    }
  }
};
</script>
src/App.vue
<template>
  <Saludo nombre="Juan" :edad="25" />
</template>

<script>
import Saludo from './components/Saludo.vue';

export default {
  components: {
    Saludo
  }
};
</script>

1.7 Reactividad con ref y reactive

Vue 3 introduce dos formas principales de crear datos reactivos:

ref

import { ref } from 'vue';

export default {
  setup() {
    const count = ref(0); // Para valores primitivos
    
    function increment() {
      count.value++; // Acceso con .value
    }
    
    return { count, increment };
  }
};

reactive

import { reactive } from 'vue';

export default {
  setup() {
    const state = reactive({
      count: 0,
      user: {
        name: 'Juan',
        age: 25
      }
    });
    
    function increment() {
      state.count++; // Acceso directo
    }
    
    return { state, increment };
  }
};

2. Vue Intermedio

2.1 Manejo de Formularios

Formularios con v-model

<template>
  <form @submit.prevent="submitForm">
    <input v-model="form.name" type="text" placeholder="Nombre">
    <input v-model="form.email" type="email" placeholder="Email">
    <textarea v-model="form.message"></textarea>
    
    <select v-model="form.country">
      <option value="mx">México</option>
      <option value="us">Estados Unidos</option>
    </select>
    
    <button type="submit">Enviar</button>
  </form>
</template>

<script>
import { reactive } from 'vue';

export default {
  setup() {
    const form = reactive({
      name: '',
      email: '',
      message: '',
      country: 'mx'
    });
    
    function submitForm() {
      console.log('Formulario enviado:', form);
    }
    
    return { form, submitForm };
  }
};
</script>

Validación con VeeValidate

Terminal
npm install vee-validate @vee-validate/rules
src/main.js
import { createApp } from 'vue';
import { Form, Field, ErrorMessage, defineRule, configure } from 'vee-validate';
import { required, email, min } from '@vee-validate/rules';

const app = createApp(App);

// Registra componentes globales
app.component('VForm', Form);
app.component('VField', Field);
app.component('ErrorMessage', ErrorMessage);

// Define reglas
defineRule('required', required);
defineRule('email', email);
defineRule('min', min);

app.mount('#app');
src/components/ValidatedForm.vue
<template>
  <VForm @submit="onSubmit" v-slot="{ errors }">
    <VField 
      name="email" 
      type="email" 
      rules="required|email" 
      v-model="form.email"
    />
    <ErrorMessage name="email" />
    
    <VField 
      name="password" 
      type="password" 
      rules="required|min:8" 
      v-model="form.password"
    />
    <ErrorMessage name="password" />
    
    <button type="submit">Enviar</button>
  </VForm>
</template>

<script>
import { reactive } from 'vue';

export default {
  setup() {
    const form = reactive({
      email: '',
      password: ''
    });
    
    function onSubmit(values) {
      console.log('Formulario válido:', values);
    }
    
    return { form, onSubmit };
  }
};
</script>

2.2 Routing con Vue Router

Terminal
npm install vue-router@4
src/router/index.js
import { createRouter, createWebHistory } from 'vue-router';
import Home from '../views/Home.vue';
import About from '../views/About.vue';

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    component: About
  },
  {
    path: '/user/:id',
    name: 'User',
    component: () => import('../views/User.vue')
  }
];

const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes
});

export default router;
src/main.js
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';

createApp(App)
  .use(router)
  .mount('#app');
src/App.vue
<template>
  <nav>
    <router-link to="/">Home</router-link>
    <router-link to="/about">About</router-link>
  </nav>
  
  <router-view></router-view>
</template>

2.3 Gestión de Estado con Pinia

Terminal
npm install pinia
src/stores/counter.js
import { defineStore } from 'pinia';

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0
  }),
  getters: {
    doubleCount: (state) => state.count * 2
  },
  actions: {
    increment() {
      this.count++;
    }
  }
});
src/main.js
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import App from './App.vue';

const app = createApp(App);

app.use(createPinia());
app.mount('#app');
src/components/Counter.vue
<template>
  <div>
    <p>Count: {{ count }}</p>
    <p>Double: {{ doubleCount }}</p>
    <button @click="increment">Increment</button>
  </div>
</template>

<script>
import { useCounterStore } from '../stores/counter';

export default {
  setup() {
    const counter = useCounterStore();
    
    return {
      count: counter.count,
      doubleCount: counter.doubleCount,
      increment: counter.increment
    };
  }
};
</script>

2.4 Comunicación entre Componentes

Props y Eventos (Padre-Hijo)

src/components/ParentComponent.vue
<template>
  <ChildComponent 
    :message="parentMessage" 
    @update="handleUpdate"
  />
</template>

<script>
import { ref } from 'vue';
import ChildComponent from './ChildComponent.vue';

export default {
  components: { ChildComponent },
  setup() {
    const parentMessage = ref('Hijo');
    
    function handleUpdate(newMessage) {
      parentMessage.value = newMessage;
    }
    
    return { parentMessage, handleUpdate };
  }
};
</script>
src/components/ChildComponent.vue
<template>
  <div>
    <p>Mensaje del padre: {{ message }}</p>
    <button @click="notifyParent">Notificar</button>
  </div>
</template>

<script>
export default {
  props: {
    message: String
  },
  emits: ['update'],
  setup(props, { emit }) {
    function notifyParent() {
      emit('update', 'Nuevo mensaje del hijo');
    }
    
    return { notifyParent };
  }
};
</script>

Provide/Inject (Componentes anidados)

src/components/AncestorComponent.vue
<template>
  <ParentComponent />
</template>

<script>
import { provide, ref } from 'vue';
import ParentComponent from './ParentComponent.vue';

export default {
  components: { ParentComponent },
  setup() {
    const sharedData = ref('Datos compartidos');
    
    provide('sharedData', sharedData);
    
    return {};
  }
};
</script>
src/components/ChildComponent.vue
<template>
  <p>Datos del ancestro: {{ sharedData }}</p>
</template>

<script>
import { inject } from 'vue';

export default {
  setup() {
    const sharedData = inject('sharedData');
    
    return { sharedData };
  }
};
</script>

2.5 Custom Directives

src/directives/vFocus.js
export default {
  mounted(el) {
    el.focus();
  }
};
src/main.js
import { createApp } from 'vue';
import App from './App.vue';
import vFocus from './directives/vFocus';

const app = createApp(App);

app.directive('focus', vFocus);

app.mount('#app');
src/App.vue
<template>
  <input v-focus type="text">
</template>

3. Vue Avanzado

3.1 Composition API Avanzada

Composables (Custom Hooks)

src/composables/useMouse.js
import { ref, onMounted, onUnmounted } from 'vue';

export function useMouse() {
  const x = ref(0);
  const y = ref(0);
  
  function update(event) {
    x.value = event.pageX;
    y.value = event.pageY;
  }
  
  onMounted(() => window.addEventListener('mousemove', update));
  onUnmounted(() => window.removeEventListener('mousemove', update));
  
  return { x, y };
}
src/components/MouseTracker.vue
<template>
  <p>Posición del mouse: {{ x }}, {{ y }}</p>
</template>

<script>
import { useMouse } from '../composables/useMouse';

export default {
  setup() {
    const { x, y } = useMouse();
    
    return { x, y };
  }
};
</script>

Suspense (Async Components)

src/components/AsyncComponent.vue
<template>
  <div>{{ data }}</div>
</template>

<script>
export default {
  async setup() {
    const response = await fetch('https://api.example.com/data');
    const data = await response.json();
    
    return { data };
  }
};
</script>
src/App.vue
<template>
  <Suspense>
    <template #default>
      <AsyncComponent />
    </template>
    <template #fallback>
      <p>Cargando...</p>
    </template>
  </Suspense>
</template>

<script>
import { defineAsyncComponent } from 'vue';

const AsyncComponent = defineAsyncComponent(() => 
  import('./components/AsyncComponent.vue')
);

export default {
  components: { AsyncComponent }
};
</script>

3.2 Render Functions y JSX

Render Function Básica

src/components/RenderComponent.vue
import { h } from 'vue';

export default {
  props: {
    level: {
      type: Number,
      required: true
    }
  },
  setup(props) {
    return () => h(
      'h' + props.level, // tipo de elemento
      {}, // props o atributos
      'Título nivel ' + props.level // contenido
    );
  }
};

JSX en Vue

Terminal
npm install @vitejs/plugin-vue-jsx
vite.config.js
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import vueJsx from '@vitejs/plugin-vue-jsx';

export default defineConfig({
  plugins: [vue(), vueJsx()]
});
src/components/JsxComponent.jsx
export default {
  props: {
    items: Array
  },
  setup(props) {
    return () => (
      <ul>
        {props.items.map(item => (
          <li key={item.id}>{item.name}</li>
        ))}
      </ul>
    );
  }
};

3.3 Plugin Development

src/plugins/i18n.js
export default {
  install(app, options) {
    // Método global
    app.config.globalProperties.$translate = (key) => {
      return key.split('.').reduce((o, i) => {
        if (o) return o[i];
      }, options);
    };
    
    // Directiva
    app.directive('my-directive', {
      mounted(el, binding) {
        // lógica de la directiva
      }
    });
    
    // Provide/Inject
    app.provide('i18n', options);
  }
};
src/main.js
import { createApp } from 'vue';
import App from './App.vue';
import i18n from './plugins/i18n';

const app = createApp(App);

app.use(i18n, {
  greetings: {
    hello: 'Hola!'
  }
});

app.mount('#app');
src/components/Example.vue
<template>
  <p>{{ $translate('greetings.hello') }}</p>
</template>

3.4 TypeScript con Vue

Terminal
npm install -D typescript @vue/runtime-core
src/components/TypedComponent.vue
<template>
  <div>
    <p>{{ user.name }} - {{ user.age }}</p>
    <button @click="incrementAge">Incrementar edad</button>
  </div>
</template>

<script lang="ts">
import { defineComponent, reactive } from 'vue';

interface User {
  name: string;
  age: number;
}

export default defineComponent({
  props: {
    initialName: {
      type: String,
      required: true
    }
  },
  setup(props) {
    const user = reactive<User>({
      name: props.initialName,
      age: 25
    });
    
    function incrementAge() {
      user.age++;
    }
    
    return {
      user,
      incrementAge
    };
  }
});
</script>

3.5 Testing en Vue

Terminal
npm install -D vitest @vue/test-utils @testing-library/vue jsdom
vite.config.js
/// <reference types="vitest" />
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';

export default defineConfig({
  plugins: [vue()],
  test: {
    globals: true,
    environment: 'jsdom',
  }
});
src/components/__tests__/Counter.spec.js
import { mount } from '@vue/test-utils';
import Counter from '../Counter.vue';

describe('Counter.vue', () => {
  it('increments count when button is clicked', async () => {
    const wrapper = mount(Counter);
    
    expect(wrapper.text()).toContain('Count: 0');
    
    await wrapper.find('button').trigger('click');
    
    expect(wrapper.text()).toContain('Count: 1');
  });
});

4. Nuxt.js: Vue para Producción

4.1 Introducción a Nuxt.js

Nuxt.js es un framework de Vue que ofrece:

4.2 Crear un Proyecto Nuxt.js

Terminal
npx nuxi init mi-app-nuxt
cd mi-app-nuxt
npm install
npm run dev

4.3 Estructura del Proyecto

nuxt-app/
├── assets/          # Assets compilados (SCSS, imágenes)
├── components/      # Componentes Vue reutilizables
├── composables/     # Composables auto-importados
├── layouts/         # Layouts de la aplicación
├── pages/           # Rutas de la aplicación
├── plugins/         # Plugins de Vue
├── public/          # Archivos estáticos
├── server/          # API routes y server middleware
├── app.vue          # Componente principal
├── nuxt.config.ts   # Configuración de Nuxt
└── tsconfig.json    # Configuración de TypeScript

4.4 Sistema de Enrutamiento

Nuxt usa un sistema de enrutamiento basado en archivos:

pages/
├── index.vue        # Ruta /
├── about.vue        # Ruta /about
├── users/
│   ├── index.vue    # Ruta /users
│   └── [id].vue     # Ruta dinámica /users/:id
└── blog/
    ├── index.vue    # Ruta /blog
    └── [slug].vue   # Ruta dinámica /blog/:slug

Página básica

pages/about.vue
<template>
  <div>
    <h1>About Page</h1>
    <NuxtLink to="/">Home</NuxtLink>
  </div>
</template>

Ruta dinámica

pages/users/[id].vue
<template>
  <div>
    <h1>User ID: {{ $route.params.id }}</h1>
  </div>
</template>

<script setup>
const route = useRoute();
console.log(route.params.id);
</script>

4.5 Layouts

layouts/default.vue
<template>
  <div>
    <header>
      <NuxtLink to="/">Home</NuxtLink>
      <NuxtLink to="/about">About</NuxtLink>
    </header>
    
    <main>
      <slot />
    </main>
    
    <footer>
      <p>Footer content</p>
    </footer>
  </div>
</template>
pages/index.vue
<template>
  <div>
    <h1>Home Page</h1>
  </div>
</template>

<script>
// Usará el layout default.vue automáticamente
export default {
  // Para usar un layout diferente:
  // layout: 'otro-layout'
};
</script>

4.6 Data Fetching

useAsyncData (Composable)

pages/posts.vue
<template>
  <div>
    <ul>
      <li v-for="post in posts" :key="post.id">
        {{ post.title }}
      </li>
    </ul>
  </div>
</template>

<script setup>
const { data: posts } = await useAsyncData('posts', () => 
  $fetch('https://jsonplaceholder.typicode.com/posts')
);
</script>

useFetch (Shortcut)

pages/posts.vue
<script setup>
const { data: posts } = await useFetch(
  'https://jsonplaceholder.typicode.com/posts'
);
</script>

4.7 State Management con Pinia

Terminal
npm install @pinia/nuxt
nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@pinia/nuxt']
});
stores/counter.js
export const useCounterStore = defineStore('counter', {
  state: () => ({ count: 0 }),
  getters: {
    double: (state) => state.count * 2
  },
  actions: {
    increment() {
      this.count++;
    }
  }
});
pages/index.vue
<template>
  <div>
    <p>Count: {{ counter.count }}</p>
    <p>Double: {{ counter.double }}</p>
    <button @click="counter.increment()">Increment</button>
  </div>
</template>

<script setup>
const counter = useCounterStore();
</script>

4.8 API Routes

server/api/hello.ts
export default defineEventHandler((event) => {
  return {
    message: 'Hello API'
  };
});
pages/index.vue
<script setup>
const { data } = await useFetch('/api/hello');
</script>

4.9 Middleware

middleware/auth.ts
export default defineNuxtRouteMiddleware((to, from) => {
  const auth = useAuthStore();
  
  if (!auth.isAuthenticated && to.path !== '/login') {
    return navigateTo('/login');
  }
});
pages/dashboard.vue
<script>
export default {
  middleware: 'auth'
};
</script>

4.10 Módulos de Nuxt

Nuxt tiene un ecosistema de módulos para añadir funcionalidades:

Terminal
npm install @nuxtjs/tailwindcss @nuxtjs/i18n @nuxtjs/color-mode
nuxt.config.ts
export default defineNuxtConfig({
  modules: [
    '@nuxtjs/tailwindcss',
    '@nuxtjs/i18n',
    '@nuxtjs/color-mode'
  ],
  i18n: {
    locales: ['en', 'es'],
    defaultLocale: 'en'
  }
});

4.11 Despliegue de Aplicaciones Nuxt

Configuración para producción

Terminal
# Generación de sitio estático
npm run generate

# SSR tradicional
npm run build
npm run start

Opciones de despliegue:

Despliegue en Vercel

  1. Crear cuenta en vercel.com
  2. Conectar tu repositorio GitHub/GitLab/Bitbucket
  3. Configurar variables de entorno
  4. Seleccionar preset de Nuxt.js
  5. Deploy automático con cada push