
Foto de redes sociales de Federico Bottos en Unsplash
Una pequeña biblioteca de toño con herramientas incluidas. Demostración en vivo
Haga preguntas en el repositorio de discusiones dedicadas, para ayudar a la comunidad en torno a este proyecto a crecer ♥
Inspirado en Vue 3 " One Piece ", UCE-Template proporciona un elemento personalizado <template> personalizado para definir componentes de manera Vue .
< template is =" uce-template " >
< style scoped >
span { color: green }
</ style >
< the-green >
The < span > {{thing}} </ span > is green
</ the-green >
< script type =" module " >
export default {
setup ( ) {
return { thing : 'world' }
}
}
</ script >
</ template >Agregue esta biblioteca a la ecuación y vea que arrance todos los componentes definidos.
<template lazy> , para resolver su definición solo cuando está en vivo<custom-element shadow> Components, y opcionalmente sombreado <style shadow> Styles@uce , para crear UI reactivas y másresolve(name, module) Si bien se sugiere instalar la CLI a nivel mundial, debido a una dependencia no-súper-luz, sigue siendo un comando npx de distancia:
# check all options and usage
npx uce-template --help
# works with files
npx uce-template my-component.html
# works with stdin
cat my-component.html | uce-templateEso es todo, pero, por supuesto, debemos asegurarnos de que el diseño producido todavía funcione como se esperaba.
Cualquier plantilla que extienda uce-template debe contener al menos un elemento personalizado en él, ya sea regular o extendido incorporado:
<!-- register regular-element -->
< template is =" uce-template " >
< regular-element >
regular
</ regular-element >
</ template >
<!-- register builtin-element as div -->
< template is =" uce-template " >
< div is =" builtin-element " >
builtin
</ div >
</ template > Cualquier plantilla puede contener una sola etiqueta <script> , y/o una o más definiciones <style> .
Si un componente contiene definiciones {{slot.name}} , los nodos del HTML vivo, antes de que se actualice el componente, se colocará allí una vez en vivo.
Vea este ejemplo en vivo para comprender más.
Cada " componente " podría definirse con o sin su propio contenido estático o dinámico.
Dicho contenido se utilizará para representar cada elemento personalizado una vez " montado " (en vivo) y según cada cambio de estado reactivo, pero solo si la plantilla no es vacía.
Todas las partes dinámicas deben estar envueltas dentro de {{dynamic}} soportes rizados como se muestra aquí:
< my-counter >
< button onclick = {{dec}} > - </ button >
< span > {{state.count}} </ span >
< button onclick = {{inc}} > + </ button >
</ my-counter > Las referencias state , dec e inc se pasarán a través del nodo del script, si corresponde.
Cada vez que se representa el componente, se invoca su devolución de llamada de actualización proporcionando el elemento en sí como un contexto .
< button is =" my-button " >
I am a {{this.tagName}}
</ button >Con respecto a Shadowdom , su polyfill no está incluido en este proyecto, pero es posible definir un componente a través de su raíz de sombra agregando un atributo de sombra :
< my-counter shadow >
<!-- this content will be in the shadowRoot -->
< button onclick = {{dec}} > - </ button >
< span > {{state.count}} </ span >
< button onclick = {{inc}} > + </ button >
</ my-counter > El atributo shadow está open de forma predeterminada, pero también se puede especificar como shadow=closed .
Con respecto a {{JS}} , si atribuye, y desea usar los espacios {{ JS }} alrededor, el atributo debe estar en citas, de lo contrario, la plantilla HTML rompe el diseño de manera inesperada.
<!-- OK -->
< my-counter >
< button onClick = {{dec}} > - </ button >
</ my-counter >
<!-- OK -->
< my-counter >
< button onClick =" {{ dec }} " > - </ button >
</ my-counter >
<!-- IT BREAKS!!! -->
< my-counter >
< button onClick = {{ dec }} > - </ button >
</ my-counter ><!--{{interpolation}}--> casoComo todo aquí se basa principalmente en el comportamiento estándar de HTML , hay casos en los que una interpolación debe envolverse como comentarios.
La regla general es que si no ve el diseño, o que lee un error de plantilla malo , es posible que su interpolación haya sido tragada por el elemento de plantilla .
Esto sucede principalmente con elementos como Tabla , Select y otros elementos que aceptan solo un tipo específico de nodo infantil, pero no de texto.
<!-- ? this won't work as expected -->
< table is =" my-table " >
< tbody > {{rows}} </ tbody >
</ table >
<!-- ? this works ? -->
< table is =" my-table " >
< tbody > <!--{{rows}}--> </ tbody >
</ table > En el primer caso, el <tbody> ignoraría cualquier nodo que no sea <tr> excepto los comentarios , porque los comentarios no se tragan ni se pierden en el proceso.
Puede ver la definición del archivo dbmonster.html tanto para el componente personalizado <table> como para el componente <tr> .
Un componente puede tener uno o más estilos, dentro de un alcance específico:
<style> aplicará su contenido a nivel mundial, útil para abordar my-counter + my-counter {...} Casos, como ejemplo<style scoped> aplicará su contenido con el nombre del elemento personalizado (es decir, my-counter span, my-counter button {...} )<style shadow> aplicará su contenido sobre el shadowroot , suponiendo que el componente se define con un atributo shadow No hay nada especial que considerar aquí, excepto que los estilos globales pueden interferir con IE11 si es demasiado molesto, ya que una vez más, IE11 no entiende el propósito y el comportamiento del elemento <template> .
Una definición puede contener solo una etiqueta de script , y dicho script se manejará virtualmente como un módulo .
Dado que IE11 no es compatible con los elementos <template> , si no se especifica el type , IE11 intentará evaluar todos los scripts en la página de derecha.
En consecuencia, el atributo type realmente puede tener algún valor, ya que es completamente irrelevante para esta biblioteca, pero dicho valor no debe ser compatible con IE11, y module es solo un valor que IE11 ignoraría.
El script puede contener una default export , o incluso un module.exports = ... , donde dicha exportación podría tener una setup(element) { ... } que devuelve lo que esperan las partes dinámicas del componente:
< script type =" module " >
import { reactive } from '@uce' ;
export default {
setup ( element ) {
const state = reactive ( { count : 0 } ) ;
const inc = ( ) => { state . count ++ } ;
const dec = ( ) => { state . count -- } ;
return { state , inc , dec } ;
}
} ;
</ script > El ayudante reactivo @uce permite actualizar automáticamente la vista cada vez que cambia una de sus propiedades.
Para saber más sobre los cambios reactivos, lea esta publicación media.
setup Si se encuentra una <script type="module" setup> , el contenido del script se invoca con el elemento en sí como contexto.
Demostración en vivo
< x-clock > </ x-clock >
< template is =" uce-template " >
< x-clock > {{time}} </ x-clock >
< script type =" module " setup >
let id = 0 ;
export default {
get time ( ) {
return ( new Date ) . toISOString ( ) ;
}
} ;
this . connected = e => id = setInterval ( this . render , 1000 / 30 ) ;
this . disconnected = e => clearInterval ( id ) ;
</ script >
</ template > Este atajo es especialmente útil para los componentes que no necesitan configurar ObservedAttributes , pero es posible que necesite configurar accesorios , y para este último caso, el atributo setup debe contener props .
< script type =" module " setup =" props " >
// props are defined as key => defaultValue pairs
export const props = {
name : this . name || 'anonymous' ,
age : + this . age || 0
} ;
</ script > Este objetivo de la sección es mostrar ejemplos básicos a complejos a través de UCE-Template , donde algún ejemplo podría usar la extensión .uce para limitar los componentes dentro de sus propios archivos.
.uce archivos como html Si está utilizando el código VS, puede ctrl+shift+p , escriba configuración JSON , elija Abrir configuración (JSON) y agregue lo siguiente a dicho archivo para resaltar los archivos .uce
{
"other-settings" : "..." ,
"files.associations" : {
"*.uce" : "html"
}
} Si definimos los componentes como view/my-component.uce
Este enfoque simplifica muchos paquetes, dependencias, hinchazón innecesario, y se puede hacer al incluir solo uce-template y el pequeño (364 bytes) UCE-cargador como bootstrap, definiendo finalmente dependencias adicionales utilizadas en los componentes.
import { parse , resolve } from 'uce-template' ;
import loader from 'uce-loader' ;
// optional components dependencies
import something from 'cool' ;
resolve ( 'cool' , something ) ;
// bootstrap the loader
loader ( {
on ( component ) {
// ignore uce-template itself
if ( component !== 'uce-template' )
fetch ( `view/ ${ component } .uce` )
. then ( body => body . text ( ) )
. then ( definition => {
document . body . appendChild (
parse ( definition )
) ;
} ) ;
}
} ) ;La misma técnica podría usarse directamente en cualquier página HTML , escribiendo algún código que pueda ser compatible con IE11 también.
<!doctype html >
< html >
< head >
< script defer src =" //unpkg.com/uce-template " > </ script >
< script defer src =" //unpkg.com/uce-loader " > </ script >
< script defer >
addEventListener (
'DOMContentLoaded' ,
function ( ) {
uceLoader ( {
Template : customElements . get ( 'uce-template' ) ,
on : function ( name ) {
if ( name !== 'uce-template' ) {
var xhr = new XMLHttpRequest ;
var Template = this . Template ;
xhr . open ( 'get' , name + '.uce' , true ) ;
xhr . send ( null ) ;
xhr . onload = function ( ) {
document . body . appendChild (
Template . from ( xhr . responseText )
) ;
} ;
}
}
} ) ;
} ,
{ once : true }
) ;
</ script >
</ head >
< body >
< my-component >
< p slot =" content " >
Some content to show in < code > my-component </ code >
</ p >
</ my-component >
</ body >
</ html >uce-template cargada cargadaSi la mayoría de nuestras páginas no usan componentes, agregar 7k+ de JS en la parte superior de cada página podría no ser deseado.
Sin embargo, podemos seguir el mismo enfoque de componentes cargados perezosos , excepto que nuestro cargador estará a cargo de traer también la biblioteca de plantilla de UCE , ya sea cuando se encuentre una plantilla de UCE en sí, o cualquier otro componente.
import loader from 'uce-loader' ;
loader ( {
on ( component ) {
// first component found, load uce-template
if ( ! this . q ) {
this . q = [ component ] ;
const script = document . createElement ( 'script' ) ;
script . src = '//unpkg.com/uce-template' ;
document . body . appendChild ( script ) . onload = ( ) => {
// get the uce-template class to use its .from(...)
this . Template = customElements . get ( 'uce-template' ) ;
// load all queued components
for ( var q = this . q . splice ( 0 ) , i = 0 ; i < q . length ; i ++ )
this . on ( q [ i ] ) ;
} ;
}
// when uce-template is loaded
else if ( this . Template ) {
// ignore loading uce-template itself
if ( component !== 'uce-template' ) {
// load the component on demand
fetch ( `view/ ${ component } .uce` )
. then ( body => body . text ( ) )
. then ( definition => {
document . body . appendChild (
this . Template . from ( definition )
) ;
} ) ;
}
}
// if uce-template is not loaded yet
// add the component to the queue
else
this . q . push ( component ) ;
}
} ) ;Usando esta técnica, nuestra carga útil JS por página ahora se reduciría a menos de 0.5k una vez anterior, el código se agrupa y minifica, mientras que todo lo demás sucederá automáticamente solo si hay componentes en algún lugar de la página.
Como la página podría contener otros elementos personalizados de terceros y bibliotecas, podría ser una buena idea predefinir un conjunto bien conocido de componentes esperados, como opuesto a tratar de cargar cualquier elemento personalizado posibles a través de la view/${...}.uce
Las técnicas de carga perezosa anteriores ya funcionarían bien, pero en lugar de verificar que el nombre del componente no es uce-template , podríamos usar un conjunto :
loader ( {
known : new Set ( [ 'some-comp' , 'some-other' ] ) ,
on ( component ) {
if ( this . known . has ( component ) )
fetch ( `view/ ${ component } .uce` )
. then ( body => body . text ( ) )
. then ( definition => {
document . body . appendChild (
parse ( definition )
) ;
} ) ;
}
} ) ; La ventaja de esta técnica es que el conjunto known podría generarse dinámicamente a través de la lista de view/*.uce para que nada se rompa si el componente encontrado no es parte de la familia UCE-Template .
uce-template inevitablemente necesita usar Function para evaluar las plantillas parciales o el requisito in-script (...) .
Se recomienda aumentar la seguridad utilizando el nonce ijeLM8+5uwZ7ZXFmK+H2dwIWdiKJ1A4zhZIsq2Ffqqo= o el atributo de integridad , confiando a través de scripts solo CSP que provienen de nuestro propio dominio.
< meta http-equiv =" Content-Security-Policy " content =" script-src 'self' 'unsafe-eval' " >
< script defer src =" /js/uce-template.js "
integrity =" sha256-ijeLM8+5uwZ7ZXFmK+H2dwIWdiKJ1A4zhZIsq2Ffqqo= "
crossorigin =" anonymous " >
</ script >Tenga en cuenta que estos valores cambian en cada versión, así que asegúrese de tener la última versión (este readMe refleja lo último).
Como lo es para UCE, si la definición contiene métodos onEvent(){...} , se utilizarán para definir el componente.
Sin embargo, dado que los estados generalmente están desacoplados del componente en sí, es una buena idea usar un mapa débil para relacionar cualquier componente con su estado y ... no te preocupes, débilmap es compatible de forma nativa en IE11 también!
Demostración en vivo
< button is =" my-btn " >
Clicked {{times}} times!
</ button >
< script type =" module " >
const states = new WeakMap ;
export default {
setup ( element ) {
const state = { times : 0 } ;
states . set ( element , state ) ;
return state ;
} ,
onClick ( ) {
states . get ( this ) . times ++ ;
// update the current view if the
// state is not reactive
this . render ( ) ;
}
} ;
</ script >Tenga en cuenta que este ejemplo cubre cualquier caso de uso de componente de estado vs, ya que usar el mapas débil es una recomendación.
Si se define el objeto props , y dado que los Props * actualizan la vista automáticamente una vez cambiado, es posible que no necesitemos un mapas débil para relacionar el estado del componente.
Demostración en vivo
< button is =" my-btn " > </ button >
< template is =" uce-template " >
< button is =" my-btn " >
Clicked {{this.times}} times!
</ button >
< script type =" module " >
export default {
props : { times : 0 } ,
onClick ( ) {
this . times ++ ;
}
} ;
</ script >
</ template > La ventaja de usar accesorios es que es posible definir un estado inicial a través de los atributos, o mediante la configuración directa cuando se renderiza a través de la utilidad html , de modo que tener un botón con times="3" , como ejemplo, se representaría que se hizo clic 3 veces. de inmediato.
< button is =" my-btn " times =" 3 " > </ button > La import {ref} from '@uce' aelper simplifica la recuperación del nodo por ref="name" atributo.
< element-details >
< span ref =" name " > </ span >
< span ref =" description " > </ span >
</ element-details >
< template is =" uce-template " >
< element-details > </ element-details >
< script type =" module " setup >
import { ref } from '@uce' ;
const { name , description } = ref ( this ) ;
name . textContent = 'element name' ;
description . textContent = 'element description' ;
</ script >
</ template > La import {slot} from '@uce' ayudante simplifica la recuperación de ranuras por su nombre, devolviendo una matriz de elementos agrupados a través del mismo nombre.
Esto se puede usar para colocar ranuras únicas en interpolaciones, como se muestra en este ejemplo, o para colocar múltiples ranuras dentro del mismo nodo.
Demostración en vivo
< filter-list >
Loading filter ...
< ul >
< li slot =" list " > some </ li >
< li slot =" list " > searchable </ li >
< li slot =" list " > text </ li >
</ ul >
</ filter-list >
< template is =" uce-template " >
< filter-list >
< div >
< input placeholder = filter oninput = {{filter}} >
</ div >
< ul >
{{list}}
</ ul >
</ filter-list >
< script type =" module " >
import { slot } from '@uce' ;
export default {
setup ( element ) {
const list = slot ( element ) . list || [ ] ;
return {
list ,
filter ( { currentTarget : { value } } ) {
for ( const li of list )
li . style . display =
li . textContent . includes ( value ) ? null : 'none' ;
}
} ;
}
} ;
</ script >
</ template >Sin embargo , en los casos en que el orden del mismo nombre de ranuras no se visualiza necesariamente secuencialmente, siempre es posible pasar una matriz de nodos.
Es decir, cualquier valor de interpolación puede ser un nodo DOM, algún valor o una matriz de nodos, funciona de la misma manera que µHTML.
Demostración en vivo
< howto-tabs >
< p > Loading tabs ... </ p >
< howto-tab role =" heading " slot =" tab " > Tab 1 </ howto-tab >
< howto-panel role =" region " slot =" panel " > Content 1 </ howto-panel >
< howto-tab role =" heading " slot =" tab " > Tab 2 </ howto-tab >
< howto-panel role =" region " slot =" panel " > Content 2 </ howto-panel >
</ howto-tabs >
< template is =" uce-template " >
< howto-tabs >
{{tabs}}
</ howto-tabs >
< script type =" module " >
import { slot } from '@uce' ;
export default {
setup ( element ) {
const { tab , panel } = slot ( element ) ;
const tabs = tab . reduce (
( tabs , tab , i ) => tabs . concat ( tab , panel [ i ] ) ,
[ ]
) ;
return { tabs } ;
}
} ;
</ script >
</ template > El sistema de módulo proporcionado por UCE-Template es extremadamente simple y completamente extensible, de modo que cada componente pueda import any from 'thing'; mientras se haya thing /resuelto a través de la biblioteca.
Si vamos a definir un solo punto de entrada del paquete, y sabemos que cada componente necesitaría una o más dependencia, podemos hacer lo siguiente:
import { resolve } from 'uce-template' ;
import moduleA from '3rd-party' ;
const moduleB = { any : 'value' } ;
resolve ( 'module-a' , moduleA ) ;
resolve ( 'module-b' , moduleB ) ;Una vez que esto construya un punto de entrada de la página web única, todos los componentes podrían importar de inmediato todos los módulos base/predeterminados, además de todos los prepolnos.
Demo en vivo (ver HTML y JS Panel + Consola)
< my-comp > </ my-comp >
< script type =" module " >
import moduleA from 'module-a' ;
import moduleB from 'module-a' ;
export default {
setup ( ) {
console . log ( moduleA , moduleB ) ;
}
}
</ script > En caso de que el componente definido importe algo de un archivo externo, como import module from './js/module.js' , dicha importación se resolvería perezosamente, junto con cualquier otro módulo que aún no se conoce, lo que significa que ./js/module.js archivo podría contener algo como esto:
// a file used to bootstrap uce-template component
// dependencies can always use the uce-template class
const { resolve } = customElements . get ( 'uce-template' ) ;
// resolve one to many modules
resolve ( 'quite-big-module' , { ... } ) ;Un script de componentes puede importar este archivo y acceder a sus módulos exportados justo después.
Demostración en vivo
< script type =" module " >
import './js/module.js' ;
import quiteBigModule from 'quite-big-module' ;
export default {
setup ( ) {
console . log ( quiteBigModule ) ;
}
}
</ script > Junto con el componente cargado perezoso , este enfoque permite enviar componentes que se basan completamente en una definición externa de archivos vue/comp.uce , donde cualquiera de estos componentes también puede compartir uno o más archivos .js capaces de resolver cualquier módulo necesario aquí o allá (dependencias compartidas en un archivo, como opuesto a las dependencias por cada componentes enviados).
Como archivo independiente, mi tamaño de elementos personalizados es de alrededor de 2.1k , pero dado que comparte casi todas las bibliotecas que UCE usa también, agruparlo, parecía la mejor manera de hacerlo, lo que resultó en solo 1k extra para un módulo que se ajusta a aproximadamente 7k al presupuesto.
Por otro lado, debido a que el polyfill no es molesto y se basa en detecciones de características de tiempo de ejecución, esto significa que a nadie debería importarle traer otro polyfill, pero también Chrome , Firefox y Edge , no se tocará, de modo que cada elemento personalizado se ejecute de forma nativa, ya sea Builtin o Regular.
En el caso Safari , o en WebKit , solo se proporcionan elementos personalizados creados, mientras que en IE11 y el antiguo MS Edge , ambos se extienden y se parcan los elementos regulares.
Eso es: ¡no se preocupe por ningún polifill, porque todo ya está incluido aquí!
Si se dirige a los navegadores que ya conoce ya proporciona elementos personalizados V1 nativos, puede usar esta versión de ESM que excluye todos los polyfills e incluya solo la lógica.
El paquete es.js actual es de hecho ~ 7k Gzipped y ~ 6.5k brotli, por lo que es posible ahorrar incluso un ancho de banda adicional en su proyecto.
Bueno, en tal caso, si ese es el único navegador de destino, el módulo @WebReflection/Custom-Elements-Builtin debe incluirse antes de que el módulo UCE-Template aterrice en la página.
< script defer src =" //unpkg.com/@webreflection/custom-elements-builtin " > </ script >
< script defer src =" //unpkg.com/uce-template " > </ script >Esto asegurará que tanto los extensiones regulares como Builtin funcionen como se esperaba.
Desafortunadamente, Shadowdom es una de esas especificaciones imposibles de pololear, pero la buena noticia es que rara vez necesitarás Shadowdom en la plantilla de UCE , pero si tu navegador es compatible, puedes usar Shadowdom tanto como quieras.
Sin embargo , hay al menos dos posibles polyfills parciales a considerar: Adjunta SHADOW, que es minimalista y liviano, y Shadydom, que está más cerca de los estándares, pero definitivamente más pesado, aunque ambos polyfills pueden, y deberían ser inyectados solo si el navegador actual lo necesita, así que pegar este código en la parte superior de su página HTML también puede llevar a la sombra a Ie11, u otros.
<!-- this must be done before uce-template -->
< script >
if ( ! document . documentElement . attachShadow )
document . write ( '<script src="//unpkg.com/attachshadow"><x2fscript>' ) ;
</ script >
< script defer src =" //unpkg.com/uce-template " > </ script > Como cada navegador moderno tendrá document.documentElement.attachShadow , el document.write ocurrirá solo en IE11 sin comprometer, o penalizar los navegadores de escritorio móviles y modernos.
Ps El <x2fscript> no es un error tipográfico, es necesario no tener un diseño roto debido a la etiqueta de script de cierre
{{...}} en lugar de ${...} ? Por mucho que me hubiera encantado tener ${...} límites de interpolación, IE11 se rompería si un elemento en el DOM contiene ${...} como atributo.
Debido a que {{...}} es una alternativa bien establecida, he decidido evitar posibles problemas de IE11 de emparejamiento de mono y simplemente seguir con una alternativa estándar de facto.
También vale la pena considerar que Vue usa {{...}} también, y también muchos otros motores basados en plantillas.
Function ? Como se explica en la parte " CSP e Integrity/Nonce " de cómo/ejemplos, es necesario usar Function por al menos dos razones:
"use strict"; directiva y pasar por una declaración with(object) , necesaria para comprender las interpolaciones sin crear un motor JS completo desde el rasguñorequire funcionalidad dentro de <script type="module"> contenido Pero incluso si no hubiera ninguna Function en la ecuación, el análisis y la ejecución de una etiqueta <script> para definir elementos personalizados habría sido exactamente el mismo equivalente del uso de Function , porque CSP habría necesitado reglas especiales de todos modos, ya que la operación es básicamente una llamada eval en el contexto global.
Como resumen, en lugar de engañar al navegador con prácticas que son tan seguras o tan inseguras, como una llamada Function , simplemente he usado Function en su lugar, manteniendo el tamaño del código razonable.
Este proyecto es como un desempeño como los elementos personalizados nativos, excepto por el costo de definición, que es una operación única por cada clase de elemento personalizado único, por lo tanto, irrelevante a largo plazo, y hay una sobrecarga insignificante dentro del plantilla inicial de la lógica de análisis, pero su ejecución repetida es tan rápida como puede ser UHTML , y si verifica el último estatus de estatus que encontrará uno de los mejores años.
Puede consultar la demostración clásica de DBMonster aquí, y ver que funciona bien.
Nada en esta biblioteca está bloqueando, y los módulos se resuelven una vez solo, incluso las importaciones relativas de ruta.
La lógica es bastante simple: si el nombre del módulo no se ha resuelto y es una importación relativa, una solicitud asíncrona se realizará y evaluará más adelante, mientras que si el módulo no se resuelve, y es un nombre calificado, se resolverá solo una vez que algún código lo proporcione.
Todo esto, además de la importación para requerir la resolución, se maneja por el ayudante UCE-Require, no se combina deliberadamente con este módulo en sí, como es de esperar que pueda inspirar y ser utilizado por otros proyectos también.
Si desea comprender más sobre uce-template y cómo funciona, consulte esta página.