Connaissance des éléments éclairés et des composants Web basés sur celui-ci

À un moment donné, j'ai dû me familiariser de toute urgence avec les composants Web et trouver un moyen de les développer facilement. J'ai l'intention d'écrire une série d'articles qui
organiser en quelque sorte la connaissance des composants Web, des éléments lit et donner une brève introduction à cette technologie pour les autres. Je ne suis pas un expert de cette technologie et j'accepterai volontiers tout commentaire.


lit-element est un wrapper (modèle de base) pour les composants Web natifs. Il implémente de nombreuses méthodes pratiques qui ne figurent pas dans la spécification. En raison de sa proximité avec l'implémentation native, lit-element affiche de très bons résultats dans divers benchmarks par rapport à d'autres approches (au 02/06/2019).


Avantages que j'observe en utilisant lit-element comme classe de base de composants Web:


  1. Cette technologie implémente déjà la deuxième version et «est tombée malade des maladies infantiles», qui sont propres aux instruments qui viennent d'apparaître.
  2. L'assemblage peut être réalisé à la fois en polymère et en webpack, dactylographié, rollup, etc., cela vous permet d'intégrer l'élément éclairé dans n'importe quel projet moderne sans aucun problème.
  3. L'élément lit possède un système très pratique de travail avec les propriétés en termes de saisie, de lancement et de conversion de valeurs.
  4. lit-element implémente presque la même logique que la réaction, c'est-à-dire il fournit le minimum - un modèle unique pour construire des composants et son rendu et ne limite pas le développeur dans le choix d'un écosystème et de bibliothèques supplémentaires.

Créez un simple composant Web sur un élément lit. Passons à la documentation. Nous avons besoin des éléments suivants:


  1. Ajouter le package npm avec l'élément lit à notre assemblage

    npm install --save lit-element 
  2. Créez notre composant.

Par exemple, nous devons créer un composant Web initialisé dans la balise my-component . Pour ce faire, créez le fichier js my-component.js et définissez son modèle de base:


 //       lit-element import { } from ''; //      class MyComponent { } //      customElements.define(); 

Tout d'abord, nous importons notre modèle de base:


 import { LitElement, html } from 'lit-element'; // LitElement -    ()   - // html -  lit-html,     ,  //    html    

Deuxièmement, créez le composant Web lui-même à l'aide de LitElement


 //   ,    //  LitElement    HTMLElement class MyComponent extends LitElement { //    LitElement   //      constructor  connectedCallback //           //    ,       // shadowDOM   {mode: 'open'} render() { return html`<p>Hello World!</p>` } } 

Et la dernière chose est d'enregistrer le composant Web dans le navigateur


 customElements.define('my-component', MyComponent); 

En conséquence, nous obtenons ce qui suit:


 import { LitElement, html } from 'lit-element'; class MyComponent extends LitElement { render() { return html`<p>Hello World!</p>` } } customElements.define('my-component', MyComponent); 

Si vous excluez la nécessité de connecter my-component.js au html, alors c'est tout. Le composant le plus simple est prêt.


Je propose de ne pas réinventer la roue et de prendre l'assemblage fini de l'élément-build-roll-rollup. Suivez les instructions:


 git clone https://github.com/PolymerLabs/lit-element-build-rollup.git cd lit-element-build-rollup npm install npm run build npm run start 

Une fois toutes les commandes terminées, nous allons sur la page du navigateur http: // localhost: 5000 / .


Si nous jetons un coup d'œil en html, nous verrons que webcomponents-loader.js est devant la balise de fermeture. Il s'agit d'un ensemble de polyfills pour les composants Web, et pour un fonctionnement multi-navigateur du composant Web, il est souhaitable que ce polyfill soit présent. Regardons le tableau des navigateurs qui implémentent toutes les normes pour travailler avec les composants Web, il dit qu'EDGE n'implémente toujours pas complètement les normes (je ne parle pas d'IE11, qui doit toujours être pris en charge).



Implémentation de 2 options pour ce polyfill:


  1. webcomponents-bundle.js - cette version contient toutes les options possibles pour le polyfill, elles sont toutes lancées, mais chaque polyfill ne fonctionnera que sur la base des signes détectés.
  2. webcomponents-loader.js est un chargeur de démarrage minimal qui, en fonction des symptômes détectés, charge les polyfills nécessaires

Je vous demande également de prêter attention à un autre polyfill - custom-elements-es5-adapter.js . Selon la spécification, seules les classes ES6 peuvent être ajoutées à customElements.define natif. Pour de meilleures performances, le code ES6 doit être transmis uniquement aux navigateurs qui le prennent en charge, et ES5 à tout le monde. Il n'est pas toujours possible de le faire, par conséquent, pour une meilleure compatibilité entre les navigateurs, il est recommandé de convertir tout le code ES6 en ES5. Mais dans ce cas, les composants Web sur ES5 ne pourront pas fonctionner dans les navigateurs. Pour résoudre ce problème, il existe custom-elements-es5-adapter.js.


./src/my-element.js fichier ./src/my-element.js


 import {html, LitElement, property} from 'lit-element'; class MyElement extends LitElement { // @property - ,    babel  ts //         //  ,   @property({type: String}) myProp = 'stuff'; render() { return html` <p>Hello World</p> ${this.myProp} `; } } customElements.define('my-element', MyElement); 

Le moteur de modèle lit-html peut traiter une chaîne différemment. Je vais vous donner plusieurs options:


 //  : html`<div>Hi</div>` // : html`<div>${this.disabled ? 'Off' : 'On'}</div>` // : html`<x-foo .bar="${this.bar}"></x-foo>` // : html`<div class="${this.color} special"></div>` //   boolean,  checked === false, //        HTML: html`<input type="checkbox" ?checked=${checked}>` //  : html`<button @click="${this._clickHandler}"></button>` 

Conseils pour optimiser la fonction render ():


  • ne doit pas changer l'état d'un élément,
  • ne devrait pas avoir d'effets secondaires,
  • ne devrait dépendre que des propriétés de l'élément,
  • devrait retourner le même résultat lors de la transmission des mêmes valeurs.

Ne mettez pas à jour le DOM en dehors de la fonction render ().


Lit-html est responsable du rendu de l'élément lit - c'est une façon déclarative de décrire comment le composant Web doit être affiché. lit-html garantit des mises à jour rapides en ne modifiant que les parties du DOM qui doivent être modifiées.


Presque tout ce code était dans un exemple simple, mais le décorateur @property été ajouté pour la propriété myProp . Ce décorateur indique que nous attendons un attribut nommé myprop dans notre my-element . Si aucun attribut de ce type n'est défini, la valeur de chaîne est définie sur stuff par défaut.


 <!--  myProp  ,       -   'stuff' --> <my-element></my-element> <!--  myprop         lowerCamelCase .. myProp   -      'else' --> <my-element myprop="else"></my-element> 

lit-element propose 2 façons de travailler avec une property :


  1. Par le décorateur.
  2. Via un getter statique properties .

La première option permet de spécifier chaque propriété séparément:


 @property({type: String}) prop1 = ''; @property({type: Number}) prop2 = 0; @property({type: Boolean}) prop3 = false; @property({type: Array}) prop4 = []; @property({type: Object}) prop5 = {}; 

La seconde consiste à tout spécifier au même endroit, mais dans ce cas, si la propriété a une valeur par défaut, elle doit être écrite dans la méthode du constructeur de classe:


 static get properties() { return { prop1: {type: String}, prop2: {type: Number}, prop3: {type: Boolean}, prop4: {type: Array}, prop5: {type: Object} }; } constructor() { this.prop1 = ''; this.prop2 = 0; this.prop3 = false; this.prop4 = []; this.prop5 = {}; } 

L'API pour travailler avec les propriétés dans lit-element est assez étendue:


  • attribut : si une propriété peut devenir un attribut observable. S'il est false , l'attribut sera exclu de l'observation; aucun getter ne sera créé pour lui. Si true ou l' attribute absent, la propriété spécifiée dans le getter au format lowerCamelCase correspondra à l'attribut au format de chaîne. Si une chaîne est spécifiée, par exemple my-prop , alors elle correspondra au même nom dans les attributs.
  • convertisseur : contient une description de la façon de convertir une valeur de / vers un attribut / propriété. La valeur peut être une fonction qui fonctionne pour sérialiser et désérialiser la valeur, ou elle peut être un objet avec les clés fromAttribute et toAttribute , ces clés contiennent des fonctions distinctes pour convertir les valeurs. Par défaut, la propriété contient une conversion vers les types de base Boolean , String , Number , Object et Array . Les règles de conversion sont répertoriées ici .
  • type : indique l'un des types de base que cette propriété contiendra. Il est utilisé comme «indice» pour le convertisseur sur le type que la propriété doit contenir.
  • refléter : indique si l'attribut doit être associé à la propriété ( true ) et modifié selon les règles de type et de converter .
  • hasChanged : chaque propriété l'a, contient une fonction qui détermine s'il y a un changement entre l'ancienne et la nouvelle valeur, renvoie respectivement un Boolean . Si true , il commence à mettre à jour l'élément.
  • noAccessor : cette propriété accepte un Boolean et par défaut false . Il interdit la génération de getters et setters pour chaque propriété pour y accéder depuis la classe. Cela n'annule pas la conversion.

Prenons un exemple hypothétique: nous allons écrire un composant Web qui contient un paramètre qui contient une chaîne, ce mot doit être dessiné à l'écran, dans lequel chaque lettre est plus grande que la précédente.


 <!-- index.html --> <ladder-of-letters letters=""></ladder-of-letters> 

 //ladder-of-letters.js import {html, LitElement, property} from 'lit-element'; class LadderOfLetters extends LitElement { @property({ type: Array, converter: { fromAttribute: (val) => { // console.log('in fromAttribute', val); return val.split(''); } }, hasChanged: (value, oldValue) => { if(value === undefined || oldValue === undefined) { return false; } // console.log('in hasChanged', value, oldValue.join('')); return value !== oldValue; }, reflect: true }) letters = []; changeLetter() { this.letters = ['','','','','']; } render() { // console.log('in render', this.letters); //    ,    //        return html` <div>${this.letters.map((i, idx) => html`<span style="font-size: ${idx + 2}em">${i}</span>`)}</div> // @click     ,     //   'click'    <button @click=${this.changeLetter}>  ''</button> `; } } customElements.define('ladder-of-letters', LadderOfLetters); 

à la fin on obtient:



lorsque le bouton a été cliqué, la propriété a été modifiée, ce qui a provoqué la vérification en premier, puis a été envoyée pour être redessinée.



et en utilisant la reflect nous pouvons également voir les changements html



Si vous modifiez cet attribut avec du code en dehors de ce composant Web, nous provoquerons également un nouveau dessin du composant Web.


Considérez maintenant le style du composant. Nous avons 2 façons de styliser l'élément lit:


  1. Styliser en ajoutant une balise de style à la méthode de rendu

     render() { return html` <style> p { color: green; } </style> <p>Hello World</p> `; } 


  2. Via des styles getter statiques

     import {html, LitElement, css} from 'lit-element'; class MyElement extends LitElement { static get styles() { return [ css` p { color: red; } ` ]; } render() { return html` <p>Hello World</p> `; } } customElements.define('my-element', MyElement); 


En conséquence, nous obtenons qu'une balise avec des styles n'est pas créée, mais est écrite ( >= Chrome 73 ) dans le Shadow DOM élément conformément à la spécification . Cela améliore les performances avec un grand nombre d'éléments, car lors de l'enregistrement d'un nouveau composant, il sait déjà quelles propriétés ses styles déterminent; elles n'ont pas besoin d'être enregistrées à chaque fois et recomptées.




De plus, si cette spécification n'est pas prise en charge, une balise de style régulière est créée dans le composant.




De plus, n'oubliez pas que de cette manière, nous pouvons également séparer les styles qui seront ajoutés et calculés sur la page. Par exemple, pour utiliser des requêtes multimédias non pas en CSS, mais en JS et implémenter uniquement le style souhaité, par exemple (c'est sauvage, mais cela doit l'être):


 static get styles() { const mobileStyle = css`p { color: red; }`; const desktopStyle = css`p { color: green; }`; return [ window.matchMedia("(min-width: 400px)").matches ? desktopStyle : mobileStyle ]; } 

En conséquence, nous verrons cela si l'utilisateur s'est connecté à un appareil avec une largeur d'écran de plus de 400 pixels.



Et c'est si l'utilisateur a visité le site à partir d'un appareil d'une largeur inférieure à 400 pixels.



Mon avis: il n'y a pratiquement pas de cas adéquat lorsqu'un utilisateur, travaillant sur un appareil mobile, fait soudain face à un moniteur à part entière avec une largeur d'écran de 1920 pixels. Ajoutez à cela le chargement paresseux des composants. En conséquence, nous obtenons un front très optimisé avec un rendu rapide des composants. Le seul problème est la difficulté à supporter.


Maintenant, je propose de me familiariser avec les méthodes du cycle de vie de l'élément lit:


  • render () : implémente une description de l'élément DOM en utilisant lit-html . Idéalement, la fonction de render est une fonction pure qui utilise uniquement les propriétés actuelles de l'élément. La méthode render() est appelée par la fonction update() .
  • shouldUpdate (changedProperties) : implémenté s'il est nécessaire de contrôler la mise à jour et le rendu, lorsque les propriétés ont été modifiées ou que requestUpdate() appelée. L'argument de la fonction changedProperties est une Map contenant les clés des propriétés modifiées. Par défaut, cette méthode renvoie toujours true , mais la logique de la méthode peut être modifiée pour contrôler la mise à jour du composant.
  • performUpdate () : implémenté pour contrôler le temps de mise à jour, par exemple, pour s'intégrer au planificateur.
  • update (changedProperties) : cette méthode appelle render() . Il met également à jour les attributs d'un élément en fonction de la valeur de la propriété. La définition des propriétés à l'intérieur de cette méthode ne provoquera pas une autre mise à jour.
  • firstUpdated (changedProperties) : appelé après la première mise à jour de l'élément DOM immédiatement avant l'appel à updated() . Cette méthode peut être utile pour capturer des liens vers des nœuds statiques visualisés avec lesquels vous devez travailler directement, par exemple, dans updated() .
  • updated (changedProperties) : appelé chaque fois que le DOM d'un élément est mis à jour et affiché. Une implémentation pour effectuer des tâches après la mise à jour via l'API DOM, par exemple, en se concentrant sur un élément.
  • requestUpdate (name, oldValue) : appelle une demande de mise à jour asynchrone pour un élément. Cela doit être appelé lorsque l'élément doit être mis à jour en fonction d'un état non provoqué par la définition de la propriété.
  • createRenderRoot () : crée par défaut une racine fantôme pour l'élément. Si l'utilisation du Shadow DOM n'est pas nécessaire, la méthode doit renvoyer this .

Comment l'élément se met-il à jour:


  • La propriété reçoit une nouvelle valeur.
  • Si la hasChanged(value, oldValue) renvoie false , l'élément n'est pas mis à jour. Sinon, une mise à jour est planifiée en appelant requestUpdate() .
  • requestUpdate () : met à jour l'élément après la microtâche (à la fin de la boucle d'événement et avant le prochain rafraîchissement).
  • performUpdate () : la mise à jour est en cours et se poursuit avec le reste de l'API de mise à jour.
  • shouldUpdate (changedProperties) : la mise à jour continue si true retourné.
  • firstUpdated (changedProperties) : appelé lorsque l'élément est mis à jour pour la première fois, immédiatement avant d'appeler updated() .
  • update (changedProperties) : met à jour l'élément. La modification des propriétés dans cette méthode ne provoque pas une autre mise à jour.
    • render () : renvoie un modèle lit-html pour le rendu d'un élément dans le DOM. La modification des propriétés dans cette méthode ne provoque pas une autre mise à jour.

  • updated (changedProperties) : appelé chaque fois qu'un élément est mis à jour.

Pour comprendre toutes les nuances du cycle de vie des composants, je vous conseille de consulter la documentation .


Au travail, j'ai un projet sur Adobe Experience Manager (AEM), dans sa création, l'utilisateur peut faire glisser et déposer des composants sur la page, et selon l'idéologie AEM, ce composant contient une balise de script qui contient tout ce qui est nécessaire pour implémenter la logique de ce composant. Mais en fait, cette approche a engendré de nombreuses ressources bloquantes et des difficultés avec la mise en place du front dans ce système. Pour implémenter le front, les composants Web ont été choisis comme un moyen de ne pas modifier le rendu côté serveur (ce qu'il a très bien fait), ainsi que d'enrichir l'ancienne implémentation avec une nouvelle approche en douceur, au niveau du bit. À mon avis, il existe plusieurs options pour implémenter le chargement des composants Web pour ce système: collecter un bundle (il peut devenir très volumineux) ou le diviser en morceaux (beaucoup de petits fichiers, un chargement dynamique est nécessaire), ou utiliser l'approche actuelle avec l'incorporation d'un script dans chaque un composant qui est rendu côté serveur (je ne veux vraiment pas y revenir). À mon avis, la première et la troisième option ne sont pas une option. Pour le second, vous avez besoin d'un chargeur de démarrage dynamique, comme dans le gabarit. Mais pour l'élément éclairé dans la "boîte", cela n'est pas fourni. Il y a eu une tentative de la part des développeurs d'éléments éclairés de créer un chargeur dynamique , mais c'est une expérience, et il n'est pas recommandé de l'utiliser en production. De la part des développeurs d'éléments éclairés, il y a un problème dans le référentiel de spécifications des composants Web avec une proposition d'ajouter à la spécification la possibilité de charger dynamiquement les js nécessaires pour le composant Web en fonction du balisage html sur la page. Et, à mon avis, cet outil natif est une très bonne idée qui vous permettra de créer un point d'initialisation des composants web et de simplement l'ajouter à toutes les pages du site.


Pour charger dynamiquement des composants Web d'éléments éclairés de manière dynamique avec les types PolymerLabs, un élément divisé a été développé. Il s'agit d'une solution expérimentale. Cela fonctionne de la manière suivante:


  • Pour créer un SplitElement, vous écrivez deux définitions d'élément dans deux modules.
  • L'un d'eux est un stub, qui définit les parties chargées d'un élément: il s'agit généralement du nom et des propriétés. Les propriétés doivent être définies avec un stub afin que lit-element puisse générer des attributs observables en temps opportun pour appeler customElements.define() .
  • Le stub doit également avoir une méthode de chargement asynchrone statique qui renvoie une classe d'implémentation.
  • Une autre classe est "l'implémentation", qui contient tout le reste.
  • Le constructeur SplitElement charge la classe d'implémentation et exécute upgrade() .

Exemple de talon:


 import {SplitElement, property} from '../split-element.js'; export class MyElement extends SplitElement { // MyElement    load   //      connectedCallback()   static async load() { //        //      MyElement return (await import('./my-element-impl.js')).MyElementImpl; } //      //   - @property() message: string; } customElements.define('my-element', MyElement); 

Exemple d'implémentation:


 import {MyElement} from './my-element.js'; import {html} from '../split-element.js'; // MyElementImpl  render    - export class MyElementImpl extends MyElement { render() { return html` <h1>I've been upgraded</h1> My message is ${this.message}. `; } } 

Exemple de SplitElement sur ES6:


 import {LitElement, html} from 'lit-element'; export * from 'lit-element'; //    LitElement  SplitElement //       export class SplitElement extends LitElement { static load; static _resolveLoaded; static _rejectLoaded; static _loadedPromise; static implClass; static loaded() { if (!this.hasOwnProperty('_loadedPromise')) { this._loadedPromise = new Promise((resolve, reject) => { this._resolveLoaded = resolve; this._rejectLoaded = reject; }); } return this._loadedPromise; } //      - //      static _upgrade(element, klass) { SplitElement._upgradingElement = element; Object.setPrototypeOf(element, klass.prototype); new klass(); SplitElement._upgradingElement = undefined; element.requestUpdate(); if (element.isConnected) { element.connectedCallback(); } } static _upgradingElement; constructor() { if (SplitElement._upgradingElement !== undefined) { return SplitElement._upgradingElement; } super(); const ctor = this.constructor; if (ctor.hasOwnProperty('implClass')) { //   ,   ctor._upgrade(this, ctor.implClass); } else { //    if (typeof ctor.load !== 'function') { throw new Error('A SplitElement must have a static `load` method'); } (async () => { ctor.implClass = await ctor.load(); ctor._upgrade(this, ctor.implClass); })(); } } //       render() { return html``; } } 

Si vous utilisez toujours l'assemblage suggéré ci-dessus dans Rollup, assurez-vous de définir babel pour pouvoir gérer les importations dynamiques


 npm install @babel/plugin-syntax-dynamic-import 

Et dans les paramètres .babelrc, ajoutez


 { "plugins": ["@babel/plugin-syntax-dynamic-import"] } 

Ici, j'ai fait un petit exemple de la mise en œuvre de composants Web avec un chargement retardé: https://github.com/malay76a/elbrus-split-litelement-web-components


J'ai essayé d'appliquer l'approche du chargement dynamique des composants Web, je suis arrivé à la conclusion suivante: l'outil fonctionne assez bien, vous devez collecter toutes les définitions des composants Web dans un seul fichier et connecter la description du composant lui-même via des morceaux séparément. Sans http2, cette approche ne fonctionne pas, car Un très grand pool de petits fichiers décrivant les composants est formé. Sur la base du principe de conception atomique , l'importation d'atomes doit être déterminée dans le corps, mais le corps doit déjà être connecté en tant que composant distinct. L'un des goulots d'étranglement est que l'utilisateur recevra un grand nombre de définitions d'éléments utilisateur dans le navigateur qui seront initialisées d'une manière ou d'une autre dans le navigateur et l'état initial sera déterminé. Une telle solution est redondante. L'une des options pour une solution simple pour le chargeur de composants est l'algorithme suivant:


  1. charger les utilitaires requis,
  2. charger des polyfills,
  3. assembler des éléments personnalisés à partir de DOM léger:
    1. tous les éléments DOM contenant un trait d'union dans le nom de la balise sont sélectionnés
    2. la liste est filtrée et une liste est formée des premiers éléments.
  4. :
    1. Intersection Observer,
    2. +- 100px import.
    1. 3 shadowDOM,
    2. , shadowDOM , , import JS.


- lit-element open-wc.org . webpack rollup, - storybook, IDE.


:


  1. Let's Build Web Components! Part 5: LitElement
  2. Web Component Essentials
  3. A night experimenting with Lit-HTML…
  4. LitElement To Do App
  5. LitElement app tutorial part 1: Getting started
  6. LitElement tutorial part 2: Templating, properties, and events
  7. LitElement tutorial part 3: State management with Redux
  8. LitElement tutorial part 4: Navigation and code splitting
  9. LitElement tutorial part 5: PWA and offline
  10. Lit-html workshop
  11. Awesome lit-html

Source: https://habr.com/ru/post/fr445438/


All Articles