Objetivo y requisitos
Vamos a distribuir un contador escrito en Vue como un Custom Element que se pueda usar en HTML. defineCustomElement crea la clase del elemento y customElements.define registra su nombre. El consumidor no necesita montar una aplicación Vue, aunque el paquete generado sí incluye el runtime de Vue.
Parte de un proyecto Vue 3 con Vite, TypeScript y el plugin oficial de Vue. El sufijo .ce.vue activa el modo Custom Element del plugin para incluir el CSS del componente dentro del shadow DOM.
Counter.ce.vue completo
Guarda el archivo en src/Counter.ce.vue:
<script setup lang="ts">
import { ref } from 'vue'
const props = defineProps({ initial: { type: Number, default: 0 } })
const emit = defineEmits<{ change: [value: number] }>()
const count = ref(props.initial)
function increment() {
count.value++
emit('change', count.value)
}
</script>
<template>
<button type="button" @click="increment">Contador: {{ count }}</button>
</template>
<style scoped>
button { padding: .75rem 1rem; border: 0; border-radius: .5rem; background: #047857; color: white; cursor: pointer; }
</style>
initial se lee al crear el contador; no representa un valor controlado que se sincronice después. Los eventos emitidos por Vue se convierten en CustomEvent y sus argumentos se entregan dentro de detail como un array.
Registra el elemento
En src/register.ts:
import { defineCustomElement } from 'vue'
import Counter from './Counter.ce.vue'
const VueCounter = defineCustomElement(Counter)
if (!customElements.get('vue-counter')) {
customElements.define('vue-counter', VueCounter)
}
El nombre debe contener un guion. La comprobación evita registrar dos veces el mismo nombre si la entrada se carga de nuevo.
Compila una biblioteca
Para esta demostración, usa este vite.config.ts. No marca Vue como dependencia externa: así el HTML de prueba puede consumir un único módulo que incluye el runtime.
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
build: {
lib: {
entry: 'src/register.ts',
formats: ['es'],
fileName: () => 'vue-counter.js',
},
},
})
Ejecuta npm run build y sirve dist por HTTP. Puedes usar npx vite preview y su dirección de red local. Añade demo.html dentro de dist después de compilar:
<!doctype html>
<html lang="es">
<meta charset="utf-8" />
<title>Contador Vue en HTML</title>
<script type="module" src="./vue-counter.js"></script>
<vue-counter initial="2"></vue-counter>
<p id="result" aria-live="polite">Pulsa el contador</p>
<script type="module">
document.querySelector('vue-counter').addEventListener('change', (event) => {
document.getElementById('result').textContent = `Valor recibido: ${event.detail[0]}`
})
</script>
</html>
Resultado esperado y errores frecuentes
Abre /demo.html desde el servidor de preview: el botón empieza en 2 y el primer clic muestra 3 en el botón y en el párrafo externo. Inspecciona el shadow DOM para ver los estilos. Sirve el archivo por HTTP en lugar de abrirlo con file://.
Si los estilos desaparecen, comprueba el sufijo .ce.vue. Si el consumidor no recibe el número, usa event.detail[0]. Si el registro falla, revisa el nombre con guion y que no esté definido antes. Al integrar el elemento dentro de otra app Vue, configura compilerOptions.isCustomElement para que el compilador lo trate como elemento nativo.
Esta distribución incluye Vue y tiene un coste de descarga: mide el paquete antes de adoptar el patrón para muchos widgets. No presupongas que el componente queda libre de dependencias solo por usarlo en HTML. Consulta Vue y Web Components.
Sigue leyendo
Una idea lleva a otra.
)
)
)
)