
Foto de mídia social de Federico Bottos no Unsplash
Uma pequena biblioteca Toolless com ferramentas incluídas. Demoção ao vivo
Faça perguntas no repositório de discussões dedicadas, para ajudar a comunidade em torno deste projeto a crescer ♥
Inspirado no Vue 3 " One Piece ", o UCE-Template fornece um elemento <template> personalizado para definir componentes de maneira 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 >Adicione esta biblioteca à equação e consulte a Bootstrapping todos os componentes definidos.
<template lazy> componente, para resolver sua definição somente quando viva<custom-element shadow> Componentes e estilos opcionalmente sombreados <style shadow>@uce Virtual, para criar UIs reativas e maisresolve(name, module) exportado utilitário exportado Embora seja sugerido instalar a CLI globalmente, devido a alguma dependência não mais iluminada, ainda é um comando npx :
# 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-templateÉ isso, mas é claro que devemos ter certeza de que o layout produzido ainda funciona como esperado?
Qualquer modelo que estenda uce-template deve conter pelo menos um elemento personalizado nele, ou estender regular ou embutido:
<!-- 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 > Qualquer modelo pode conter uma única tag <script> e/ou uma ou mais definições <style> .
Se um componente contiver {{slot.name}} definições, nós do HTML vivo, antes que o componente seja atualizado, será colocado lá uma vez ao vivo.
Veja este exemplo ao vivo para entender mais.
Cada " componente " pode se definir com, ou sem, com seu próprio conteúdo estático ou dinâmico.
Esse conteúdo será usado para renderizar cada elemento personalizado uma vez " montado " (ao vivo) e por cada mudança de estado reativo, mas apenas se o modelo não for vazio.
Todas as partes dinâmicas devem ser envolvidas em {{dynamic}} colchetes encaracolados, como mostrado aqui:
< my-counter >
< button onclick = {{dec}} > - </ button >
< span > {{state.count}} </ span >
< button onclick = {{inc}} > + </ button >
</ my-counter > As referências de state , dec e inc serão passadas através do nó de script, se houver.
Sempre que o componente é renderizado, seu retorno de chamada de atualização é chamado, fornecendo o elemento em si como um contexto .
< button is =" my-button " >
I am a {{this.tagName}}
</ button >Em relação à sombra , seu polyfill não está incluído neste projeto, mas é possível definir um componente através de sua raiz de sombra , adicionando um 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 > O atributo shadow está open por padrão, mas também pode ser especificado como shadow=closed .
Em relação a {{JS}} , se atributo, e você gostaria de usar {{ JS }} espaços, o atributo deve estar em aspas, caso contrário, o modelo HTML quebra o layout de maneiras inesperadas.
<!-- 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 tudo aqui é baseado principalmente no comportamento padrão do HTML , há casos em que uma interpolação deve ser envolvida como comentário.
A regra geral é que, se você não vir o layout ou ler algum erro de modelo ruim , é possível que sua interpolação possa ter sido engolida pelo elemento de modelo .
Isso acontece principalmente com elementos como tabela , seleção e outros elementos que aceitam apenas um tipo específico de nó filho, mas não o 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 > No primeiro caso, o <tbody> ignoraria qualquer nó que não seja um <tr> , exceto os comentários , porque os comentários não são engolidos ou perdidos no processo.
Você pode ver a definição do arquivo dbmonster.html para o componente personalizado <table> e o <tr> personalizado.
Um componente pode ter um ou mais estilos, dentro de um escopo específico:
<style> genérico aplicará seu conteúdo globalmente, útil para abordar os casos my-counter + my-counter {...} , como exemplo<style scoped> aplicará seu conteúdo prefixado com o nome do elemento personalizado (ou seja my-counter span, my-counter button {...} )<style shadow> aplicará seu conteúdo na parte superior da raiz de sombra , assumindo que o componente seja definido com um atributo shadow Não há nada de especial a considerar aqui, exceto que os estilos globais podem interferir no IE11 se muito invasivo, pois mais uma vez o IE11 não entende o objetivo e o comportamento do elemento <template> .
Uma definição pode conter apenas uma tag de script e esse script será praticamente tratado como um módulo .
Como o IE11 não é compatível com elementos <template> , se o type não for especificado, o IE11 tentará avaliar todos os scripts na página direita.
Consequentemente, o atributo type pode realmente ter qualquer valor, pois é completamente irrelevante para essa biblioteca, mas esse valor não deve ser compatível com o IE11, e module é apenas um valor que o IE11 ignoraria.
O script pode conter uma default export , ou mesmo um module.exports = ... , onde essa exportação pode ter um método setup(element) { ... } que retorna o que as partes dinâmicas do componente esperam:
< 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 > O ajudante reativo @uce permite atualizar automaticamente a visualização sempre que uma de suas propriedades mudar.
Para saber mais sobre mudanças reativas, leia este post médio.
setup Se um <script type="module" setup> for encontrado, o conteúdo do script será invocado com o próprio elemento como contexto.
Demoção ao 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 > Esse atalho é especialmente útil para componentes que não precisam configurar o observado , mas podem precisar configurar adereços e, para o último caso, o atributo setup deve conter 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 > Esse objetivo da seção é mostrar exemplos básicos para complexos via UCE-Template , onde algum exemplo pode usar a extensão .uce para limitar os componentes em seus próprios arquivos.
.uce arquivos como html Se você estiver usando o código VS, poderá Ctrl+Shift+P , digitar Configurações JSON , Escolha Configurações de Open (JSON) e adicione o seguinte a esse arquivo para destacar os arquivos .uce como HTML :
{
"other-settings" : "..." ,
"files.associations" : {
"*.uce" : "html"
}
} Se definirmos componentes como view/my-component.uce poderíamos decidir incluí-los preguiçosamente ou melhor, apenas quando estes forem encontrados na página atual.
Essa abordagem simplifica muitos pacotes, dependências, inchaço desnecessário, e isso pode ser feito incluindo apenas uce-template e os pequenos (364 bytes) -carregador de UCE como bootstrap, eventualmente definindo dependências extras usadas entre os 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 )
) ;
} ) ;
}
} ) ;A mesma técnica pode ser usada diretamente em qualquer página HTML , escrevendo algum código que também pode ser compatível com o IE11 .
<!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-templateSe a maioria de nossas páginas não usar componentes, adicionar 7k+ de JS na parte superior de cada página poderá ser indesejada.
No entanto, podemos acompanhar a mesma abordagem de componentes carregados preguiçosos , exceto que nosso carregador será responsável por trazer também a biblioteca da UCE-TEMPLATE , quando um próprio modelo UCE é encontrado ou qualquer outro 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 essa técnica, nossa carga JS por página agora seria reduzida para menos de 0,5k uma vez acima do código é agrupado e minificado, enquanto todo o resto acontecerá automaticamente apenas se houver componentes em algum lugar da página.
Como a página pode conter outros elementos personalizados de terceiros e bibliotecas, pode ser uma boa idéia predefinar um conjunto bem conhecido dos componentes esperados, como oposto de tentar carregar quaisquer elementos personalizados possíveis através da solicitação view/${...}.uce
Técnicas anteriores de carregamento preguiçoso já funcionariam muito bem, mas, em vez de verificar se o nome do componente não é uce-template , poderíamos usar um 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 )
) ;
} ) ;
}
} ) ; A vantagem dessa técnica é que o conjunto known pode ser gerado dinamicamente através da lista de arquivos de view/*.uce .
uce-template Inevitavelmente precisa usar Function para avaliar o modelo parcial ou o requerido no script (...) .
Recomenda -se aumentar a segurança usando o nonce ijeLM8+5uwZ7ZXFmK+H2dwIWdiKJ1A4zhZIsq2Ffqqo= ou o atributo de integridade , confiando via scripts apenas CSP que vem de nosso próprio domínio.
< 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 >Observe que esses valores são alterados em cada versão , portanto, verifique se você tem a versão mais recente (este readme reflete o mais recente).
Como é para UCE, se a definição contiver métodos onEvent(){...} , estes serão usados para definir o componente.
No entanto, como os estados geralmente são dissociados do próprio componente, é uma boa idéia usar um mapa fraco para relacionar qualquer componente com seu estado e ... não se preocupe, o mapa fraco também é apoiado nativamente no IE11 !
Demoção ao 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 >Observe que este exemplo abrange qualquer caso de uso de estado versus componente , pois o uso do mapa fraco é uma recomendação.
Se o objeto props for definido e, como os adereços * atualizam a exibição automaticamente, uma vez alterados, talvez não precisemos de um mapa fraco para relacionar o estado do componente.
Demoção ao 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 > A vantagem do uso de adereços é que é possível definir um estado inicial por meio de atributos ou por meio de configuração direta quando renderizada através do utilitário html , de modo que ter um botão com times="3" , como exemplo, seria renderizado mostrando cliques 3 vezes! agora mesmo.
< button is =" my-btn " times =" 3 " > </ button > O import {ref} from '@uce' Helper simplifica a recuperação do nó por atributo ref="name" .
< 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 > A import {slot} from '@uce' Helper simplifica a recuperação de slots pelo nome, retornando uma variedade de elementos agrupados com o mesmo nome.
Isso pode ser usado para colocar slots únicos em interpolações, como mostrado neste exemplo, ou para colocar vários slots dentro do mesmo nó.
Demoção ao 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 >No entanto , nos casos em que a ordem dos slots de mesmo nome não é necessariamente visualizada sequencialmente, é sempre possível passar uma variedade de nós.
Ou seja, qualquer valor de interpolação pode ser um nó DOM, algum valor ou uma matriz de nós, da mesma maneira que µHTML funciona.
Demoção ao 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 > O sistema de módulos fornecido pelo UCE-Template é extremamente simples e totalmente extensível, para que cada componente possa import any from 'thing'; Contanto que thing tenha sido fornecida/resolvida pela biblioteca.
Se vamos definir um único ponto de entrada de pacote e sabemos que cada componente precisaria de uma ou mais dependência, podemos fazer o seguinte:
import { resolve } from 'uce-template' ;
import moduleA from '3rd-party' ;
const moduleB = { any : 'value' } ;
resolve ( 'module-a' , moduleA ) ;
resolve ( 'module-b' , moduleB ) ;Uma vez que isso construa o ponto de entrada da página da web única, todos os componentes poderiam importar imediatamente todos os módulos básicos/padrão, além de todos os pré-resolvidos.
Demoção ao vivo (veja o painel HTML e JS + Console)
< 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 > Caso o componente definido importe algo de um arquivo externo, como import module from './js/module.js' faria, essa importação seria preguiçosamente resolvida, juntamente com qualquer outro módulo que ainda não seja conhecido, o que significa que ./js/module.js arquivo pode conter algo assim:
// 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' , { ... } ) ;Um script de componente pode importar esse arquivo e acessar seus módulos exportados logo após.
Demoção ao vivo
< script type =" module " >
import './js/module.js' ;
import quiteBigModule from 'quite-big-module' ;
export default {
setup ( ) {
console . log ( quiteBigModule ) ;
}
}
</ script > Juntamente com o componente carregado preguiçoso , essa abordagem possibilita o envio de componentes que são totalmente baseados em uma definição externa de arquivo vue/comp.uce , onde qualquer um desses componentes também pode compartilhar um ou mais arquivos .js capazes de resolver qualquer módulo necessário aqui ou ali (dependências compartilhadas em um arquivo, como oposto de dependências de cada componentes enviados).
Como arquivo independente, meu tamanho de elementos personalizados é de cerca de 2,1k , mas como é compartilhar quase todas as bibliotecas que a UCE também usa, agachar -o parecia o melhor caminho a percorrer, resultando em apenas 1k extra para um módulo que se encaixa em aproximadamente 7k a 10k .
Por outro lado, como o poli -preenchimento não é intrusivo e baseado em detecções de recursos de tempo de execução, isso significa que ninguém deve se preocupar em trazer nenhum outro poli -preenchimento de sempre, mas também cromo , firefox e borda , será intocado, para que todo elemento personalizado seja executado nativamente, estenda ou regular embutida.
No caso Safari , ou com base no webkit , apenas os elementos personalizados incorporados são fornecidos, enquanto no IE11 e na antiga borda do MS , ambas as extensas construídas e elementos regulares são corrigidos.
É isso: não se preocupe com nenhum poli, porque tudo já está incluído aqui!
Se você estiver segmentando os navegadores que você conhece já fornece elementos personalizados nativos V1, pode usar esta versão ESM que exclui todos os poli -preenchimentos e inclua apenas a lógica.
O pacote es.js atual é de fato ~ 7k gzipped e ~ 6,5k brotli, de modo que é possível economizar largura de banda extra em seu projeto.
Bem, nesse caso, se esse for o único navegador de destino, o módulo @webReflection/elementos personalizados-Builtin deve ser incluído antes do módulo UCE-TEMPLATLE TERRAS na página.
< script defer src =" //unpkg.com/@webreflection/custom-elements-builtin " > </ script >
< script defer src =" //unpkg.com/uce-template " > </ script >Isso garantirá que as extensas regulares e construídas funcionem conforme o esperado.
Infelizmente, o Shadowdom é uma dessas especificações impossíveis de polir, mas a boa notícia é que você raramente precisará de sombra no tempo da UCE , mas se o seu navegador for compatível, poderá usar o Shadowdom o quanto quiser.
No entanto , existem pelo menos dois poli -preenchimentos parciais possíveis a serem considerados: Anexshadow, que é minimalista e leve, e Shadydom , que está mais próximo dos padrões, mas definitivamente mais pesado, embora ambos os poli -preenchidos possam e deveriam ser injetados apenas para que o Browser atenda a ele, que eu prejudicaria muito, que eu prejudicaria muito o seu código.
<!-- 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 todo navegador moderno terá document.documentElement.attachShadow , o document.write .
PS O <x2fscript> não é um erro de digitação, é necessário para não ter um layout quebrado devido à tag de script de fechamento
{{...}} em vez de ${...} ? Por mais que eu adorasse ter ${...} limites de interpolação, o IE11 quebraria se um elemento no DOM contiver ${...} como atributo.
Como {{...}} é uma alternativa bem estabelecida, decidi evitar possíveis problemas do IE11 e simplesmente manter uma alternativa padrão de fato.
Também vale a pena considerar que o Vue usa {{...}} também, assim como muitos outros motores baseados em modelos.
Function é necessária? Conforme explicado na parte " CSP & Integridade/Nonce " do como/exemplos, é necessário usar Function por pelo menos dois motivos:
"use strict"; diretiva e passe por uma declaração with(object) , necessário para entender as interpolações sem criar um mecanismo JS inteiro a partir do arranhãorequire funcionalidade dentro de <script type="module"> conteúdo Mas mesmo que não houvesse Function na equação, analisar e executar uma tag <script> para definir elementos personalizados teria exatamente o mesmo equivalente ao uso Function , porque o CSP precisaria de regras especiais de qualquer maneira, pois a operação é basicamente uma chamada de avaliação no contexto global.
Como resumo, em vez de enganar o navegador com práticas tão seguras ou tão inseguras quanto uma chamada de Function , eu simplesmente usei Function , mantendo o tamanho do código razoável.
Este projeto é o desempenho-como elementos personalizados nativos podem ser, exceto o custo de definição, que é uma operação pontual por cada classe de elemento personalizado exclusivo, portanto irrelevante a longo prazo, e há uma sobrecarga insignificante na lógica inicial, mas sua execução mais rápida poderá ser a mais rápida que você pode ser o mais rápido, e se verifique se você verá a lógica mais rápida que verifica o status que verifica o status que verifica o status que verifica o status que verifica o status que verifica o status que verifica o status que verifica o status que verifica o status que verifica o status que verifica o status que verifica o status que verifica o status.
Você pode verificar a demonstração clássica do DBMonster aqui e ver que ele tem um desempenho bom.
Nada nesta biblioteca está bloqueando e os módulos são resolvidos apenas uma vez , mesmo as importações relativas de caminho.
A lógica é bem simples: se o nome do módulo não tiver sido resolvido e é uma importação relativa, uma solicitação assíncrona será feita e avaliada posteriormente, enquanto se o módulo não for resolvido e for um nome qualificado, será resolvido apenas quando algum código o fornecer.
Tudo isso, além da importação para exigir resolução, é tratado pelo ajudante de recompensa da UCE, propositadamente não junto a este módulo em si, como ele poderia inspirar e ser usado por outros projetos também.
Se você quiser entender mais sobre uce-template e como funciona, consulte esta página.