Guía exhaustiva desde los conceptos básicos de Vue 3 hasta aplicaciones avanzadas con Nuxt.js
Vue es un framework progresivo para construir interfaces de usuario. Sus características principales incluyen:
# 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
<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>
Vue usa una sintaxis de template que extiende HTML:
<!-- Interpolación básica -->
<p>{{ mensaje }}</p>
<!-- Expresiones JavaScript -->
<p>{{ mensaje.split('').reverse().join('') }}</p>
<!-- 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>
export default {
data() {
return {
count: 0
};
},
methods: {
increment() {
this.count++;
}
},
computed: {
doubleCount() {
return this.count * 2;
}
},
mounted() {
console.log('Componente montado');
}
};
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
};
}
};
<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>
<template>
<Saludo nombre="Juan" :edad="25" />
</template>
<script>
import Saludo from './components/Saludo.vue';
export default {
components: {
Saludo
}
};
</script>
Vue 3 introduce dos formas principales de crear datos reactivos:
import { ref } from 'vue';
export default {
setup() {
const count = ref(0); // Para valores primitivos
function increment() {
count.value++; // Acceso con .value
}
return { count, increment };
}
};
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 };
}
};
<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>
npm install vee-validate @vee-validate/rules
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');
<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>
npm install vue-router@4
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;
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
createApp(App)
.use(router)
.mount('#app');
<template>
<nav>
<router-link to="/">Home</router-link>
<router-link to="/about">About</router-link>
</nav>
<router-view></router-view>
</template>
npm install pinia
import { defineStore } from 'pinia';
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0
}),
getters: {
doubleCount: (state) => state.count * 2
},
actions: {
increment() {
this.count++;
}
}
});
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import App from './App.vue';
const app = createApp(App);
app.use(createPinia());
app.mount('#app');
<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>
<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>
<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>
<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>
<template>
<p>Datos del ancestro: {{ sharedData }}</p>
</template>
<script>
import { inject } from 'vue';
export default {
setup() {
const sharedData = inject('sharedData');
return { sharedData };
}
};
</script>
export default {
mounted(el) {
el.focus();
}
};
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');
<template>
<input v-focus type="text">
</template>
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 };
}
<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>
<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>
<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>
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
);
}
};
npm install @vitejs/plugin-vue-jsx
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import vueJsx from '@vitejs/plugin-vue-jsx';
export default defineConfig({
plugins: [vue(), vueJsx()]
});
export default {
props: {
items: Array
},
setup(props) {
return () => (
<ul>
{props.items.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}
};
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);
}
};
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');
<template>
<p>{{ $translate('greetings.hello') }}</p>
</template>
npm install -D typescript @vue/runtime-core
<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>
npm install -D vitest @vue/test-utils @testing-library/vue jsdom
/// <reference types="vitest" />
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
export default defineConfig({
plugins: [vue()],
test: {
globals: true,
environment: 'jsdom',
}
});
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');
});
});
Nuxt.js es un framework de Vue que ofrece:
npx nuxi init mi-app-nuxt
cd mi-app-nuxt
npm install
npm run dev
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
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
<template>
<div>
<h1>About Page</h1>
<NuxtLink to="/">Home</NuxtLink>
</div>
</template>
<template>
<div>
<h1>User ID: {{ $route.params.id }}</h1>
</div>
</template>
<script setup>
const route = useRoute();
console.log(route.params.id);
</script>
<template>
<div>
<header>
<NuxtLink to="/">Home</NuxtLink>
<NuxtLink to="/about">About</NuxtLink>
</header>
<main>
<slot />
</main>
<footer>
<p>Footer content</p>
</footer>
</div>
</template>
<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>
<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>
<script setup>
const { data: posts } = await useFetch(
'https://jsonplaceholder.typicode.com/posts'
);
</script>
npm install @pinia/nuxt
export default defineNuxtConfig({
modules: ['@pinia/nuxt']
});
export const useCounterStore = defineStore('counter', {
state: () => ({ count: 0 }),
getters: {
double: (state) => state.count * 2
},
actions: {
increment() {
this.count++;
}
}
});
<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>
export default defineEventHandler((event) => {
return {
message: 'Hello API'
};
});
<script setup>
const { data } = await useFetch('/api/hello');
</script>
export default defineNuxtRouteMiddleware((to, from) => {
const auth = useAuthStore();
if (!auth.isAuthenticated && to.path !== '/login') {
return navigateTo('/login');
}
});
<script>
export default {
middleware: 'auth'
};
</script>
Nuxt tiene un ecosistema de módulos para añadir funcionalidades:
npm install @nuxtjs/tailwindcss @nuxtjs/i18n @nuxtjs/color-mode
export default defineNuxtConfig({
modules: [
'@nuxtjs/tailwindcss',
'@nuxtjs/i18n',
'@nuxtjs/color-mode'
],
i18n: {
locales: ['en', 'es'],
defaultLocale: 'en'
}
});
# Generación de sitio estático
npm run generate
# SSR tradicional
npm run build
npm run start