Fermer

février 4, 2022

L'alternative à la vanille (Partie 2) —


Résumé rapide ↬

Dans cette seconde partie, Noam propose quelques modèles d'utilisation directe de la plateforme web comme alternative à certaines des solutions proposées par les frameworks.

La semaine dernièrenous avons examiné les différents avantages et coûts de l'utilisation des frameworks, en partant du point de vue des problèmes de base qu'ils tentent de résoudre, en nous concentrant sur la programmation déclarative, la liaison de données, la réactivité, les listes et conditionnels. Aujourd'hui, nous verrons si une alternative peut émerger de la plate-forme Web elle-même. liaison de données réactive. Ayant déjà essayé cela auparavant et voyant à quel point cela peut être coûteux, j'ai décidé de travailler avec une ligne directrice dans cette exploration; non pas pour lancer mon propre framework, mais plutôt pour voir si je peux utiliser la plate-forme Web directement d'une manière qui rend les frameworks moins nécessaires. Si vous envisagez de déployer votre propre framework, sachez qu'il existe un ensemble de coûts non abordés dans cet article.

Vanilla Choices

La plate-forme Web fournit déjà un mécanisme de programmation déclaratif prêt à l'emploi : HTML et CSS. Ce mécanisme est mature, bien testé, populaire, largement utilisé et documenté. Cependant, il ne fournit pas de concepts intégrés clairs de liaison de données, de rendu conditionnel et de synchronisation de liste, et la réactivité est un détail subtil réparti sur plusieurs fonctionnalités de plate-forme.

Lorsque je parcours la documentation des frameworks populaires, je trouve les fonctionnalités décrites dans la partie 1 tout de suite. Lorsque je lis la documentation de la plate-forme Web (par exemple, sur MDN), je trouve de nombreux modèles déroutants sur la façon de faire les choses, sans une représentation concluante de la liaison de données, de la synchronisation des listes ou de la réactivité. Je vais essayer de tracer quelques lignes directrices sur la façon d'aborder ces problèmes sur la plate-forme Web, sans nécessiter de framework (en d'autres termes, en passant à la vanille).

Réactivité avec arbre DOM stable et cascade

Revenons exemple d'étiquette d'erreur. Dans ReactJS et SolidJS, nous créons un code déclaratif qui se traduit par un code impératif qui ajoute l'étiquette au DOM ou la supprime. Dans Svelte, ce code est généré.

Mais que se passerait-il si nous n'avions pas du tout ce code, et que nous utilisions plutôt CSS pour masquer et afficher l'étiquette d'erreur ?

  

La réactivité, dans ce cas, est gérée dans le navigateur : le changement de classe de l'application se propage à ses descendants jusqu'à ce que le mécanisme interne du navigateur décide d'afficher ou non l'étiquette.

Cette technique présente plusieurs avantages :[19659016]La taille du bundle est de zéro.

  • Il n'y a aucune étape de construction.
  • La propagation des modifications est optimisée et bien testée, dans le code du navigateur natif, et évite les opérations DOM coûteuses et inutiles comme append et . supprimer.
  • Les sélecteurs sont stables. Dans ce cas, vous pouvez compter sur la présence de l'élément label. Vous pouvez lui appliquer des animations sans vous fier à des constructions compliquées telles que des "groupes de transition". Vous pouvez y faire référence en JavaScript.
  • Si l'étiquette est affichée ou masquée, vous pouvez voir la raison dans le panneau de style des outils de développement, qui vous montre toute la cascade, la chaîne de règles qui s'est terminée dans l'étiquette étant visible (ou masquée).
  • Même si vous lisez ceci et choisissez de continuer à travailler avec des frameworks, l'idée de garder le DOM stable et de changer d'état avec CSS est puissante. Considérez où cela pourrait vous être utile. saisir. Traditionnellement, l'utilisateur remplissait le formulaire et cliquait sur un bouton "Soumettre", et le code côté serveur gérait la réponse. Les formulaires étaient la version d'application multipage de la liaison de données et de l'interactivité. Pas étonnant que les éléments HTML avec les noms de base input et output soient des éléments de formulaire.

    En raison de leur large utilisation et de leur longue histoire, les API de formulaire ont accumulé plusieurs pépites cachées qui font utiles pour les problèmes qui ne sont pas traditionnellement considérés comme étant résolus par des formulaires. est accessible par son nom (en utilisant form.elements). De plus, le formulaire associé à un élément est accessible (grâce à l'attribut form). Cela inclut non seulement les éléments d'entrée, mais également d'autres éléments de formulaire tels que outputtextarea et fieldsetqui permettent un accès imbriqué aux éléments d'une arborescence.

    Dans l'exemple d'étiquette d'erreur de la section précédente, nous avons montré comment afficher et masquer de manière réactive le message d'erreur. Voici comment nous mettons à jour le texte du message d'erreur dans React (et de la même manière dans SolidJS) :

    const [errorMessage, setErrorMessage] = useState(null);
    return 
    

    Lorsque nous avons un DOM stable et des formes d'arborescence et des éléments de formulaire stables, nous pouvons faire ce qui suit :

    Cela semble assez verbeux dans sa forme brute, mais c'est aussi très stable, direct et extrêmement performant.

    Formulaires d'entrée

    Habituellement, lorsque nous construisons un SPA, nous avons une sorte d'API de type JSON avec lequel nous travaillons pour mettre à jour notre serveur, ou quel que soit le modèle que nous utilisons.

    Ceci serait un exemple familier (écrit en Typescript pour plus de lisibilité) :

    interface
      identifiant : chaîne ;
      nom : chaîne ;
      e-mail : chaîne ;
      abonné : booléen ;
    }
    
    fonction mise à jourContact(contact : Contact) { … }
    

    Il est courant dans le code de framework de générer cet objet Contact en sélectionnant des éléments d'entrée et en construisant l'objet pièce par pièce. Avec une utilisation appropriée des formulaires, il existe une alternative concise :

    En utilisant des entrées masquées et la classe utile FormDatanous pouvons transformer de manière transparente les valeurs entre l'entrée DOM et les fonctions JavaScript.

    Combiner les formulaires et la réactivité

    En combinant la stabilité du sélecteur haute performance de formulaires et la réactivité CSS, nous pouvons obtenir une logique d'interface utilisateur plus complexe :

    Notez dans cet exemple qu'il n'y a pas d'utilisation de classes – nous développons le comportement du DOM et du style à partir des données des formulaires, plutôt qu'en modifiant manuellement les classes d'éléments.

    Je n'aime pas abuser des classes CSS. en tant que sélecteurs JavaScript. Je pense qu'ils devraient être utilisés pour regrouper des éléments de style similaire, et non comme un mécanisme fourre-tout pour modifier les styles de composants. les fonctionnalités sont stables. Cela signifie beaucoup moins de JavaScript, beaucoup moins d'incompatibilités de versions de framework et pas de "build".

  • Les formulaires sont accessibles par défaut. Si votre application utilise correctement les formulaires, les attributs ARIA, les "plugins d'accessibilité" et les audits de dernière minute sont beaucoup moins nécessaires. Les formulaires se prêtent à la navigation au clavier, aux lecteurs d'écran et à d'autres technologies d'assistance.
  • Les formulaires sont dotés de fonctionnalités intégrées de validation des entrées : validation par modèle regex, réactivité aux formulaires non valides et valides en CSS, gestion des éléments obligatoires par rapport aux facultatifs, et Suite. Vous n'avez pas besoin de quelque chose qui ressemble à un formulaire pour profiter de ces fonctionnalités.
  • L'événement submit des formulaires est extrêmement utile. Par exemple, il permet d'attraper une touche "Entrée" même lorsqu'il n'y a pas de bouton d'envoi, et il permet de différencier plusieurs boutons d'envoi par l'attribut submitter (comme nous le verrons dans l'exemple TODO plus tard).
  • Les éléments sont associés par défaut à leur formulaire contenant mais peuvent être associés à tout autre formulaire du document à l'aide de l'attribut form. Cela nous permet de jouer avec l'association de formulaires sans créer de dépendance à l'arborescence DOM.
  • L'utilisation des sélecteurs stables aide à l'automatisation des tests de l'interface utilisateur : nous pouvons utiliser l'API imbriquée comme un moyen stable de se connecter au DOM, quelle que soit sa disposition. et hiérarchie. La hiérarchie form > (fieldsets) > element peut servir de squelette interactif de votre document.
  • ChaCha et HTML Template

    Les frameworks fournissent leur propre façon d'exprimer des listes observables. Aujourd'hui, de nombreux développeurs s'appuient également sur des bibliothèques non-framework qui fournissent ce type de fonctionnalités, telles que MobX.

    Le principal problème avec les listes observables à usage général est qu'elles sont à usage général. Cela ajoute de la commodité au coût des performances et nécessite également des outils de développement spéciaux pour déboguer les actions compliquées que ces bibliothèques effectuent en arrière-plan. le choix du framework d'interface utilisateur, mais l'utilisation de l'alternative n'est peut-être pas plus compliquée et cela peut éviter certains des pièges qui se produisent lorsque vous essayez de lancer votre propre modèle. – également connu sous le nom de Changes Channel – est un flux bidirectionnel dont le but est de notifier les changements dans la direction intention et la direction observe.

    • Dans la direction. sens intentionnell'interface utilisateur notifie le modèle des modifications voulues par l'utilisateur.
    • Dans le sens observerle modèle notifie à l'interface utilisateur les modifications apportées au modèle et doivent être affichés pour e e utilisateur.

    C'est peut-être un drôle de nom, mais ce n'est pas un modèle compliqué ou nouveau. Les flux bidirectionnels sont utilisés partout sur le Web et dans les logiciels (par exemple, MessagePort). Dans ce cas, nous créons un flux bidirectionnel qui a un objectif particulier : signaler les modifications réelles du modèle à l'interface utilisateur et les intentions du modèle.

    L'interface de ChaCha peut généralement être dérivée de la spécification de l'application, sans aucune Code de l'interface utilisateur.

    Par exemple, une application qui vous permet d'ajouter et de supprimer des contacts et qui charge la liste initiale à partir d'un serveur (avec une option d'actualisation) pourrait avoir un ChaCha qui ressemble à ceci :

    interface Contact {
      identifiant : chaîne ;
      nom : chaîne ;
      e-mail : chaîne ;
    }
    // Sens "Observer"
    interface ContactListModelObserver {
      surAjouter(contact : Contact);
      onRemove(contact: Contact);
      onUpdate(contact: Contact);
    }
    // Sens "Intention"
    interface ContactListModel {
      ajouter(contact : contact );
      supprimer (contact : contact );
      rechargeDepuisServeur();
    }
    

    Notez que toutes les fonctions des deux interfaces sont vides et ne reçoivent que des objets simples. C'est intentionnel. ChaCha est construit comme un canal avec deux ports pour envoyer des messages, ce qui lui permet de fonctionner dans un EventSourceun HTML MessageChannelun service worker ou tout autre protocole.

    L'avantage des ChaCha est qu'ils sont faciles à tester : vous envoyez des actions et attendez des appels spécifiques à l'observateur en retour.

    L'élément de modèle HTML pour les éléments de liste

    Les modèles HTML sont des éléments spéciaux présents dans le DOM mais ne s'affiche pas. Leur but est de générer des éléments dynamiques.

    Lorsque nous utilisons un élément templatenous pouvons éviter tout le code passe-partout consistant à créer des éléments et à les remplir en JavaScript.

    Ce qui suit ajoutera un nom. à une liste à l'aide d'un modèle :

    En utilisant l'élément template pour les éléments de liste, nous pouvons voir l'élément de liste dans notre code HTML d'origine – il n'est pas "rendu" à l'aide de JSX ou d'un autre langage. Votre fichier HTML contient maintenant tout le code HTML de l'application – les parties statiques font partie du DOM rendu et les parties dynamiques sont exprimées dans des modèles, prêts à être clonés et ajoutés au document lorsque le temps vient.

    Mettre tout ensemble : TodoMVC

    TodoMVC est une spécification d'application d'une liste TODO qui a été utilisée pour présenter les différents frameworks. Le modèle TodoMVC est livré avec du HTML et du CSS prêts à l'emploi pour vous aider à vous concentrer sur le framework.

    Vous pouvez jouer avec le résultat dans le référentiel GitHub et le code source complet. est disponible.

    Commencez avec une ChaCha dérivée d'une spécification

    Nous allons commencer par la spécification et l'utiliser pour créer l'interface ChaCha :

    interface Task {
       titre : chaîne ;
       terminé : booléen ;
    }
    
    interface TaskModelObserver {
       onAdd(clé : nombre, valeur : tâche );
       onUpdate(clé : nombre, valeur : tâche );
       onRemove(clé : numéro);
       onCountChange(count : {actif : nombre, terminé : nombre} );
    }
    
    interface Modèle de tâche {
       constructeur (observateur : TaskModelObserver );
       createTask(tâche : tâche) : void ;
       updateTask(clé : numéro, tâche : Tâche) : void ;
       deleteTask(clé : numéro): void ;
       clearCompleted() : void ;
       markAll(completed: boolean): void;
    }
    

    Les fonctions du modèle de tâche sont dérivées directement de la spécification et de ce que l'utilisateur peut faire (effacer les tâches terminées, marquer toutes comme terminées ou actives, obtenir les décomptes actifs et terminés).

    Notez qu'il suit les directives. de ChaCha :

    • Il existe deux interfaces, une agissante et une observatrice.
    • Tous les types de paramètres sont des objets primitifs ou simples (facilement traduits en JSON).
    • Toutes les fonctions renvoient void.[19659021]L'implémentation de TodoMVC utilise localStorage comme arrière-plan.

      Le modèle est très simple et peu pertinent pour la discussion sur le cadre de l'interface utilisateur. Il enregistre dans localStorage si nécessaire et déclenche des rappels de modification à l'observateur lorsque quelque chose change, soit à la suite d'une action de l'utilisateur, soit lorsque le modèle est chargé à partir de localStorage pour la première fois.[19659100]HTML simplifié et orienté formulaire

      Ensuite, je vais prendre le modèle TodoMVC et le modifier pour qu'il soit orienté formulaire – une hiérarchie de formulaires, avec des éléments d'entrée et de sortie représentant des données pouvant être modifiées avec JavaScript.[19659007]Comment puis-je savoir si quelque chose doit être un élément de formulaire ? En règle générale, s'il est lié aux données du modèle, il doit s'agir d'un élément de formulaire.

      Le fichier HTML complet est disponible, mais voici sa partie principale :

      todos

      Ce code HTML comprend les éléments suivants :

      • Nous avons un formulaire principalavec toutes les entrées et tous les boutons globaux, et un nouveau formulaire pour créer une nouvelle tâche. Notez que nous associons les éléments au formulaire à l'aide de l'attribut formpour éviter d'imbriquer les éléments dans le formulaire.
      • L'élément template représente un élément de liste, et son élément racine est une autre forme qui représente les données interactives liées à une tâche particulière. Ce formulaire serait répété en clonant le contenu du modèle lorsque des tâches sont ajoutées.
      • Les entrées masquées représentent des données qui ne sont pas directement affichées mais qui sont utilisées pour le style et la sélection.

      Notez à quel point ce DOM est concis. Il n'a pas de classes réparties sur ses éléments. Il comprend tous les éléments nécessaires à l'application, disposés dans une hiérarchie raisonnable. Grâce aux éléments d'entrée masqués, vous pouvez déjà avoir une bonne idée de ce qui pourrait changer ultérieurement dans le document.

      Ce HTML ne sait pas comment il va être stylé ni exactement à quelles données il est lié. Laissez le CSS et JavaScript travailler pour votre HTML, plutôt que votre HTML fonctionne pour un mécanisme de style particulier. Cela faciliterait grandement la modification des conceptions au fur et à mesure.

      JavaScript minimal du contrôleur

      Maintenant que nous avons la majeure partie de la réactivité dans CSS et que nous avons la gestion des listes dans le modèle, il ne reste que le code du contrôleur — le ruban adhésif qui tient tout ensemble. Dans cette petite application, le JavaScript du contrôleur est environ 40 lignes.

      Voici une version, avec une explication pour chaque partie :

      import TaskListModel from './model.js' ;
      
      modèle const = new TaskListModel (nouvelle classe {
      

      Ci-dessus, nous créons un nouveau modèle.

      onAdd(key, value) {
         const newItem = document.querySelector('.todo-list template').content.cloneNode(true).firstElementChild;
         newItem.name = `task-${key}` ;
         const save = () => model.updateTask(key, Object.fromEntries(new FormData(newItem)));
         newItem.elements.completed.addEventListener('modifier', enregistrer);
         newItem.addEventListener('soumettre', enregistrer);
         newItem.elements.title.addEventListener('dblclick', ({target}) => target.removeAttribute('readonly'));
         newItem.elements.title.addEventListener('blur', ({target}) => target.setAttribute('readonly', ''));
         newItem.elements.destroy.addEventListener('click', () => model.deleteTask(key));
         this.onUpdate(key, value, newItem);
         document.querySelector('.todo-list').appendChild(newItem);
      }
      

      Lorsqu'un élément est ajouté au modèle, nous créons l'élément de liste correspondant dans l'interface utilisateur.

      Ci-dessus, nous clonons le contenu de l'élément templateattribuons les écouteurs d'événement pour un élément particulier et ajoutez le nouvel élément à la liste.

      Notez que cette fonction, ainsi que onUpdateonRemove et onCountChangesont des rappels qui vont à appeler depuis le modèle.

      onUpdate(key, {title, filled}, form = document.forms[`task-${key}`]) {
         form.elements.completed.checked = !!terminé ;
         form.elements.title.value = titre ;
         form.elements.title.blur();
      }
      

      Lorsqu'un élément est mis à jour, nous définissons ses valeurs completed et titlepuis blur (pour quitter le mode d'édition).

      onRemove( clé) { document.forms[`task-${key}`].remove(); }
      

      Lorsqu'un élément est supprimé du modèle, nous supprimons l'élément de liste correspondant de la vue.

      onCountChange({actif, terminé}) {
         document.forms.main.elements.completedCount.value = terminé ;
         document.forms.main.elements.toggleAll.checked = actif === 0 ;
         document.forms.main.elements.totalCount.value = actif + terminé ;
         document.forms.main.elements.activeCount.innerHTML = `${active} item${active === 1 ? '' : 's'} gauche`;
      }
      

      Dans le code ci-dessus, lorsque le nombre d'éléments terminés ou actifs change, nous définissons les entrées appropriées pour déclencher les réactions CSS, et nous formatons la sortie qui affiche le nombre.

      const updateFilter = () => filter .value = location.hash.substr(2);
      window.addEventListener('hashchange', updateFilter);
      window.addEventListener('load', updateFilter);
      

      Et nous mettons à jour le filtre à partir du fragment hash (et au démarrage). Tout ce que nous faisons ci-dessus est de définir la valeur d'un élément de formulaire – CSS gère le reste. : vrai});

      Ici, nous nous assurons de ne pas recharger la page lorsqu'un formulaire est soumis. C'est la ligne qui transforme cette application en SPA.

      document.forms.newTask.addEventListener('submit', ({target : {elements : {title}}}) =>
          model.createTask({titre : titre.value}) );
      document.forms.main.elements.toggleAll.addEventListener('change', ({cible : {coché}})=>
          model.markAll(coché));
      document.forms.main.elements.clearCompleted.addEventListener('click', () =>
          model.clearCompleted());
      

      Et cela gère les actions principales (création, tout marquer, effacement terminé).

      Réactivité avec CSS

      Le fichier CSS complet est à votre disposition pour consultation.

      Des poignées CSS une grande partie des exigences du cahier des charges (avec quelques modifications pour favoriser l'accessibilité). Regardons quelques exemples.

      Selon la spécification, le bouton "X" (destroy) est affiché uniquement au survol. J'ai également ajouté un bit d'accessibilité pour le rendre visible lorsque la tâche est ciblée :

      .task:not(:hover, :focus-within) button[name="destroy"] { opacity : 0 }
      

      Le lien filter obtient une bordure rouge lorsqu'il s'agit de l'actuel :

      .todoapp input[name="filter"][value=""] ~ footer a[href$="#/"],
      nav a:cible {
         couleur de bordure : #CE4646 ;
      }
      

      Notez que nous pouvons utiliser le href de l'élément de lien comme sélecteur d'attribut partiel – pas besoin de JavaScript qui vérifie le filtre actuel et définit une classe selected sur l'élément approprié .

      Nous utilisons également le sélecteur  :ciblece qui nous évite d'avoir à nous soucier d'ajouter ou non des filtres.

      Le style d'affichage et d'édition de l'entrée titre change. basé sur son mode lecture seule :

      .task input[name="title"]:read-only {
      …
      }
      
      .task input[name="title"]:not(:read-only) {
      …
      }
      

      Le filtrage (c'est-à-dire afficher uniquement les tâches actives et terminées) s'effectue à l'aide d'un sélecteur :

      input[name="filter"][value="active"] ~ * .task
            :is(input[name="completed"]:coché, input[name="completed"]:coché ~ *),
      entrée[name="filter"][value="completed"] ~ * .tâche
           :is(input[name="completed"]:not(:checked), input[name="completed"]:not(:checked) ~ *) {
         affichage : aucun ;
      }
      

      Le code ci-dessus peut sembler un peu verbeux, et il est probablement plus facile à lire avec un préprocesseur CSS tel que Sass. Mais ce qu'il fait est simple : si le filtre est actif et que la case à cocher terminé est cochée, ou vice versa, nous masquons la case à cocher et ses frères et sœurs.

      J'ai choisi de implémentez ce filtre simple dans CSS pour montrer jusqu'où cela peut aller, mais s'il commence à devenir poilu, il serait tout à fait logique de le déplacer dans le modèle à la place. des moyens pratiques d'accomplir des tâches complexes, et ils présentent des avantages au-delà des aspects techniques, tels que l'alignement d'un groupe de développeurs sur un style et un modèle particuliers. La plate-forme Web offre de nombreux choix, et l'adoption d'un cadre met tout le monde au moins partiellement sur la même page pour certains de ces choix. Il y a de la valeur là-dedans. De plus, il y a quelque chose à dire sur l'élégance de la programmation déclarative, et la grande fonctionnalité de la composantisation n'est pas quelque chose que j'ai abordé dans cet article. moins d'expérience de développeur. Permettez-vous d'être curieux avec ces modèles, même si vous décidez de les choisir tout en utilisant un framework. Cela déclenche une réaction en chaîne pour faciliter les choses.

    • Appuyez sur CSS pour la réactivité au lieu de JavaScript, lorsque vous le pouvez.
    • Utilisez des éléments de formulaire comme principal moyen de représenter des données interactives.
    • Utilisez le HTML . élément template au lieu des modèles générés par JavaScript.
    • Utilisez un flux bidirectionnel de modifications comme interface avec votre modèle.

    Remerciements particuliers aux personnes suivantes pour les révisions techniques : Yehonatan Daniv, Tom Bigelajzen, Benjamin Greenbaum, Nick Ribal, Louis Lazaris

    Smashing Editorial(vf, il, al)




    Source link