En esta página
Componentes SFC y script setup
¿Qué es un Single File Component?
Un Single File Component (SFC) es el formato nativo de Vue para escribir componentes. Un archivo .vue encapsula tres bloques en un solo archivo:
<script setup>— La lógica del componente (TypeScript)<template>— La estructura HTML del componente<style scoped>— Los estilos CSS del componente
Esta colocación tiene enormes ventajas: todo lo relacionado con un componente vive junto, los estilos son automáticamente aislados, y los editores como VS Code (con la extensión Volar/Vue Official) ofrecen soporte completo de sintaxis, autocompletado y refactoring.
Anatomía de un SFC
El bloque script setup
<script setup lang="ts">
import { ref } from 'vue'
const saludo = ref('¡Hola, Vue!')
</script>lang="ts" activa TypeScript. La directiva setup hace que todo lo declarado en este bloque esté automáticamente disponible en el template, sin necesidad de return {} como en la Options API.
El bloque template
<template>
<div>
<p>{{ saludo }}</p>
</div>
</template>El template puede tener múltiples elementos raíz (fragments), a diferencia de Vue 2 que requería un solo elemento raíz. Usa la sintaxis de dobles llaves {{ }} para la interpolación de texto.
El bloque style scoped
<style scoped>
p {
color: #6366f1;
font-size: 1.25rem;
}
</style>La palabra clave scoped es crucial: hace que estos estilos solo afecten a este componente. Sin scoped, los estilos son globales y pueden afectar toda la aplicación.
Crear un proyecto con create-vue
create-vue es el andamio oficial basado en Vite:
npm create vue@latest mi-proyecto
cd mi-proyecto
npm install
npm run devDurante el proceso interactivo, selecciona:
- ✅ TypeScript
- ✅ Vue Router (para navegación)
- ✅ Pinia (para estado global)
- ✅ ESLint (para calidad de código)
- ✅ Vitest (para testing)
El punto de entrada: main.ts
Toda aplicación Vue comienza con main.ts. Aquí se crea la instancia de la aplicación, se registran plugins y se monta en el DOM:
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import router from './router'
import App from './App.vue'
const app = createApp(App)
app.use(createPinia()) // Plugin de estado global
app.use(router) // Plugin de enrutamiento
app.mount('#app') // Monta en el elemento con id="app"El orden importa: los plugins deben usarse antes de mount().
Componentes hijos e importación
En Vue SFC, importar un componente lo hace disponible directamente en el template gracias a script setup:
<script setup lang="ts">
// Solo importar es suficiente — no hay que registrar en components: {}
import MiBoton from './components/MiBoton.vue'
import TarjetaUsuario from './components/TarjetaUsuario.vue'
</script>
<template>
<!-- Uso en PascalCase (recomendado) o kebab-case -->
<TarjetaUsuario nombre="Ana" />
<mi-boton>Clic aquí</mi-boton>
</template>La convención es usar PascalCase para los componentes en el template. Vue acepta ambos (PascalCase y kebab-case), pero PascalCase distingue visualmente los componentes del HTML nativo.
Props — comunicación padre a hijo
Las props son la forma de pasar datos de un componente padre a un componente hijo. Con script setup y TypeScript, se declaran con defineProps<T>():
<script setup lang="ts">
// Sintaxis de genérico — la más recomendada
const props = defineProps<{
titulo: string
subtitulo?: string // Opcional
nivel: 1 | 2 | 3 // Union type
}>()
// Acceso: props.titulo, props.subtitulo
console.log(props.titulo)
</script>Para valores por defecto, usamos withDefaults():
<script setup lang="ts">
const { titulo, nivel = 1 } = withDefaults(
defineProps<{
titulo: string
nivel?: 1 | 2 | 3
}>(),
{
nivel: 1
}
)
// Con Vue 3.5 también puedes usar desestructuración directa con valores por defecto
</script>El sistema de slots
Los slots permiten que el componente padre inyecte contenido dentro del componente hijo. Son el equivalente Vue de children en React:
<!-- Componente Panel.vue -->
<template>
<div class="panel">
<header>
<slot name="cabecera">Título por defecto</slot>
</header>
<main>
<slot /> <!-- slot por defecto -->
</main>
<footer>
<slot name="pie" />
</footer>
</div>
</template><!-- Uso del componente Panel -->
<template>
<Panel>
<template #cabecera>
<h2>Mi panel</h2>
</template>
<p>Contenido principal del panel.</p>
<template #pie>
<button type="button">Guardar</button>
</template>
</Panel>
</template>defineComponent — el modo alternativo
Si no usas script setup (por ejemplo, al escribir componentes como objetos de configuración puro para TypeScript), puedes usar defineComponent():
import { defineComponent, ref } from 'vue'
export default defineComponent({
name: 'MiComponente',
props: {
titulo: {
type: String,
required: true
}
},
setup(props) {
const activo = ref(false)
return { activo }
}
})Sin embargo, para proyectos nuevos, script setup es siempre preferible. Es más conciso, tiene mejor soporte de tipos y el compilador de Vue lo optimiza más agresivamente.
Estilos — las opciones disponibles
Vue SFC soporta varias estrategias de estilo:
<!-- CSS puro con scope -->
<style scoped>
.boton { background: blue; }
</style>
<!-- SCSS/SASS -->
<style scoped lang="scss">
.tarjeta {
&:hover { transform: translateY(-2px); }
}
</style>
<!-- Módulos CSS (alternative a scoped) -->
<style module>
.boton { background: blue; }
</style>
<script setup lang="ts">
import { useCssModule } from 'vue'
const css = useCssModule()
// uso: :class="css.boton"
</script>El scoped es suficiente para la mayoría de proyectos. Los CSS Modules son útiles cuando necesitas acceder al nombre de clase generado dinámicamente desde JavaScript.
Buenas prácticas con SFC
- Nombres en PascalCase:
TarjetaUsuario.vue, notarjeta-usuario.vue - Un componente por archivo: Nunca exportes múltiples componentes desde un SFC
- Componentes pequeños: Si un componente supera las 150 líneas de template, considera dividirlo
- Props en la interfaz: Siempre tipa las props con TypeScript genérico
- Estilos siempre scoped: Evita estilos globales salvo en
src/assets/osrc/styles/ - Lógica compleja en composables: Si el
script setupcrece demasiado, extrae la lógica
Práctica
- Crea tu primer SFC: Crea un componente
src/components/Bienvenida.vuecon un propnombre: stringy un estado reactivocontadorVisitas. Muéstralos en el template. - Usa slots: Crea un componente
Contenedor.vuecon un slot por defecto y un slot nombrado#encabezado. Úsalo desdeApp.vuecon contenido personalizado. - Estilos scoped: Añade estilos al componente
Bienvenida.vuey verifica que no afectan a otros elementos de la página. Prueba también::v-deep()para afectar elementos hijos.
En la siguiente lección aprenderemos toda la sintaxis de templates y las directivas de Vue.
<script setup lang="ts">
import { ref, computed } from 'vue'
// Props tipadas con TypeScript genérico
const { nombre, email, rol = 'usuario' } = defineProps<{
nombre: string
email: string
rol?: 'admin' | 'usuario' | 'editor'
}>()
// Estado local reactivo
const expandido = ref(false)
// Estado derivado
const iniciales = computed(() =>
nombre
.split(' ')
.map(p => p[0])
.join('')
.toUpperCase()
)
function toggleExpandir(): void {
expandido.value = !expandido.value
}
</script>
<template>
<article class="tarjeta" :class="{ expandida: expandido }">
<div class="avatar">{{ iniciales }}</div>
<div class="info">
<h3>{{ nombre }}</h3>
<span class="rol">{{ rol }}</span>
<p v-if="expandido">{{ email }}</p>
</div>
<button type="button" @click="toggleExpandir">
{{ expandido ? 'Menos' : 'Más' }}
</button>
</article>
</template>
<style scoped>
.tarjeta {
display: flex;
align-items: center;
gap: 1rem;
padding: 1rem;
border: 1px solid #e2e8f0;
border-radius: 0.5rem;
}
.tarjeta.expandida {
background: #f8fafc;
}
.avatar {
width: 3rem;
height: 3rem;
border-radius: 50%;
background: #6366f1;
color: white;
display: grid;
place-items: center;
font-weight: bold;
}
</style>
import { createApp } from 'vue'
import App from './App.vue'
// Crear la aplicación Vue
const app = createApp(App)
// Los plugins se montan antes de mount()
// app.use(router)
// app.use(pinia)
app.mount('#app')
Inicia sesión para guardar tu progreso