Un exemple pratique d'utilisation des fonctions de rendu de Vue: création d'une grille typographique pour un système de conception

Dans le document dont nous publions la traduction aujourd'hui, nous parlerons de la façon de créer une grille typographique pour le système de conception en utilisant les fonctions de rendu Vue . Voici une démo du projet que nous allons revoir ici. Vous pouvez trouver son code ici. L'auteur de ce document dit qu'il a utilisé des fonctions de rendu car elles permettent un contrôle beaucoup plus précis du processus de création de code HTML que les modèles Vue classiques. Cependant, à sa grande surprise, il n'a pu trouver d'exemples pratiques de leur application. Il n'a rencontré que des manuels d'instructions. Il espère que ce matériau fera une différence pour le mieux grâce à l'exemple pratique d'utilisation des fonctions de rendu de Vue.


Fonctions de rendu Vue


Les fonctions de rendu m'ont toujours semblé quelque chose d'un peu inhabituel pour Vue. Tout dans ce cadre souligne le désir de simplicité et la séparation des fonctions des différentes entités. Mais les fonctions de rendu sont un étrange mélange de HTML et de JavaScript, qui est souvent difficile à lire.

Par exemple, voici le balisage HTML:

<div class="container">   <p class="my-awesome-class">Some cool text</p> </div> 

Pour le former, vous avez besoin de la fonction suivante:

 render(createElement) {  return createElement("div", { class: "container" }, [    createElement("p", { class: "my-awesome-class" }, "Some cool text")  ]) } 

Je soupçonne que de telles constructions feront que beaucoup se détourneront immédiatement des fonctions de rendu. Après tout, la facilité d'utilisation est exactement ce qui attire les développeurs vers Vue. Il est dommage que beaucoup de gens ne voient pas leurs vrais mérites derrière l'apparence disgracieuse des fonctions de rendu. Le fait est que les fonctions de rendu et les composants fonctionnels sont des outils intéressants et puissants. Afin de démontrer leurs capacités et leur véritable valeur, je parlerai de la façon dont ils m'ont aidé à résoudre le vrai problème.

Veuillez noter qu'il sera très utile d'ouvrir une version de démonstration du projet considéré dans un onglet de navigateur adjacent et d'y accéder pendant la lecture d'un article.

Définition de critères pour un système de conception


Nous avons un système de conception basé sur VuePress . Nous devions y inclure une nouvelle page, démontrant diverses possibilités typographiques de conception de texte. Voilà à quoi ressemblait la mise en page que le designer m'a donnée.


Mise en page

Et voici un exemple du code CSS correspondant à cette page:

 h1, h2, h3, h4, h5, h6 {  font-family: "balboa", sans-serif;  font-weight: 300;  margin: 0; } h4 {  font-size: calc(1rem - 2px); } .body-text {  font-family: "proxima-nova", sans-serif; } .body-text--lg {  font-size: calc(1rem + 4px); } .body-text--md {  font-size: 1rem; } .body-text--bold {  font-weight: 700; } .body-text--semibold {  font-weight: 600; } 

Les en-têtes sont formatés en fonction des noms de balises. Pour formater d'autres éléments, des noms de classe sont utilisés. De plus, il existe des classes distinctes pour la richesse et les tailles de police.

Avant de commencer à écrire du code, j'ai formulé quelques règles:

  • Étant donné que le but principal de cette page est la visualisation des données, les données doivent être stockées dans un fichier séparé.
  • Pour formater les en-têtes, des balises d'en-tête sémantiques doivent être utilisées (c'est-à-dire, <h1> , <h2> et ainsi de suite), leur mise en forme ne doit pas être basée sur la classe.
  • Les balises de paragraphe ( <p> ) avec des noms de classe (par exemple, <p class="body-text--lg"> ) doivent être utilisées dans le corps de la page.
  • Les matériaux constitués de divers éléments doivent être regroupés en les enveloppant dans la <p> racine, ou dans un autre élément racine approprié auquel aucune classe de stylisation n'est affectée. Les éléments enfants doivent être enveloppés dans une <span> , qui définit le nom de la classe. Voici à quoi pourrait ressembler cette règle:

     <p>  <span class="body-text--lg">Thing 1</span>  <span class="body-text--lg">Thing 2</span> </p> 
  • Les matériaux, dont la sortie ne présente aucune exigence particulière, doivent être enveloppés dans la <p> , à laquelle est affecté le nom de classe souhaité. Les éléments enfants doivent être placés dans une <span> :

     <p class="body-text--semibold">  <span>Thing 1</span>  <span>Thing 2</span> </p> 
  • Pour chaque cellule stylisée à styliser, les noms de classe doivent être écrits une seule fois.

Options pour résoudre le problème


Avant de commencer le travail, j'ai envisagé plusieurs options pour résoudre l'ensemble de tâches qui m'était proposé. Voici leur aperçu.

WritingÉcrire manuellement du code HTML


J'aime écrire du code HTML manuellement, mais seulement quand cela nous permet de résoudre adéquatement le problème existant. Cependant, dans mon cas, l'écriture manuelle de code impliquerait la saisie de divers fragments de code répétitifs dans lesquels certaines variations sont présentes. Je n'aimais pas ça. De plus, cela signifierait que les données ne pourraient pas être stockées dans un fichier séparé. En conséquence, j'ai refusé cette approche.

Si j'avais créé la page en question comme ça, j'aurais eu quelque chose comme ceci:

 <div class="row">  <h1>Heading 1</h1>  <p class="body-text body-text--md body-text--semibold">h1</p>  <p class="body-text body-text--md body-text--semibold">Balboa Light, 30px</p>  <p class="group body-text body-text--md body-text--semibold">    <span>Product title (once on a page)</span>    <span>Illustration headline</span>  </p> </div> 

▍Utilisation des modèles Vue traditionnels


Dans des conditions normales, cette approche est utilisée le plus souvent. Cependant, jetez un œil à cet exemple.


Un exemple d'utilisation de modèles Vue

Dans la première colonne, il y a ce qui suit:

  • La <h1> , présentée sous la forme dans laquelle le navigateur l'affiche.
  • La <p> regroupant plusieurs enfants <span> avec du texte. Une classe est affectée à chacun de ces éléments (mais aucune classe spéciale n'est affectée à la <p> elle-même).
  • La <p> qui n'a pas d'éléments <span> imbriqués auxquels la classe est affectée.

Pour implémenter tout cela, de nombreuses instances de directives v-if et v-if-else seraient nécessaires. Et cela, je le sais, conduirait au fait que le code deviendrait très déroutant très bientôt. De plus, je n'aime pas l'utilisation de toute cette logique conditionnelle dans le balisage.

▍Fonctions de rendu


En conséquence, après avoir analysé les alternatives possibles, j'ai sélectionné les fonctions de rendu. En eux, en utilisant JavaScript, en utilisant des constructions conditionnelles, des nœuds enfants d'autres nœuds sont créés. Lors de la création de ces nœuds enfants, tous les critères nécessaires sont pris en compte. Dans cette situation, une telle solution me semblait parfaite.

Modèle de données


Comme je l'ai dit, je voulais stocker les données typographiques dans un fichier JSON distinct. Cela permettrait, si nécessaire, d'y apporter des modifications sans toucher au balisage. Ce sont les données.

Chaque objet JSON du fichier est une description d'une ligne distincte:

 {  "text": "Heading 1",  "element": "h1", //  .  "properties": "Balboa Light, 30px", //   .  "usage": ["Product title (once on a page)", "Illustration headline"] //   .   -   . } 

Voici le code HTML qui vient après le traitement de cet objet:

 <div class="row">  <h1>Heading 1</h1>  <p class="body-text body-text--md body-text--semibold">h1</p>  <p class="body-text body-text--md body-text--semibold">Balboa Light, 30px</p>  <p class="group body-text body-text--md body-text--semibold">    <span>Product title (once on a page)</span>    <span>Illustration headline</span>  </p> </div> 

Considérons maintenant un exemple plus complexe. Les tableaux représentent des groupes d'enfants. Les propriétés des objets classes , qui sont eux-mêmes des objets, peuvent stocker des descriptions de classe. La propriété de base de l'objet classes contient une description des classes communes à tous les nœuds de la cellule. Chaque classe présente dans la propriété variants est appliquée à un seul élément du groupe.

 {  "text": "Body Text - Large",  "element": "p",  "classes": {    "base": "body-text body-text--lg", //     .    "variants": ["body-text--bold", "body-text--regular"] //    ,       .        .  },  "properties": "Proxima Nova Bold and Regular, 20px",  "usage": ["Large button title", "Form label", "Large modal text"] } 

Cet objet se transforme en HTML suivant:

 <div class="row">  <!--  1 -->  <p class="group">    <span class="body-text body-text--lg body-text--bold">Body Text - Large</span>    <span class="body-text body-text--lg body-text--regular">Body Text - Large</span>  </p>  <!--  2 -->  <p class="group body-text body-text--md body-text--semibold">    <span>body-text body-text--lg body-text--bold</span>    <span>body-text body-text--lg body-text--regular</span>  </p>  <!--  3 -->  <p class="body-text body-text--md body-text--semibold">Proxima Nova Bold and Regular, 20px</p>  <!--  4 -->  <p class="group body-text body-text--md body-text--semibold">    <span>Large button title</span>    <span>Form label</span>    <span>Large modal text</span>  </p> </div> 

La structure de base du projet


Nous avons un composant parent, TypographyTable.vue , qui contient un balisage pour former la table. Nous avons également un composant enfant, TypographyRow.vue , qui est responsable de la création de la ligne du tableau et contient notre fonction de rendu.

Lors de la formation de lignes de table, un tableau contenant des données est parcouru. Les objets qui décrivent les lignes de la table sont passés au composant TypographyRow tant que propriétés.

 <template>  <section>    <!--         -->    <div class="row">      <p class="body-text body-text--lg-bold heading">Hierarchy</p>      <p class="body-text body-text--lg-bold heading">Element/Class</p>      <p class="body-text body-text--lg-bold heading">Properties</p>      <p class="body-text body-text--lg-bold heading">Usage</p>    </div>     <!--             -->    <typography-row      v-for="(rowData, index) in $options.typographyData"      :key="index"      :row-data="rowData"    />  </section> </template> <script> import TypographyData from "@/data/typography.json"; import TypographyRow from "./TypographyRow"; export default {  //     ,        typographyData: TypographyData,  name: "TypographyTable",  components: {    TypographyRow }; </script> 

Ici, je voudrais noter une petite chose sympa: les données typographiques dans une instance de Vue peuvent être représentées comme une propriété. Vous pouvez y accéder en utilisant la construction $options.typographyData , car ils ne changent pas et ne doivent pas être réactifs (merci à Anton Kosykh ).

Création d'un composant fonctionnel


Le composant TypographyRow qui traite les données est un composant fonctionnel. Les composants fonctionnels sont des entités qui n'ont ni états ni instances. Cela signifie qu'ils ne l'ont pas et qu'ils n'ont pas accès aux méthodes de cycle de vie du composant Vue.

Voici le «squelette» d'un composant similaire avec lequel nous allons commencer à travailler sur notre composant:

 //  <template> <script> export default {  name: "TypographyRow",  functional: true, //       props: {    rowData: { //          type: Object     },  render(createElement, { props }) {    //    } </script> 

La méthode du composant de render prend un argument de context qui a une propriété props . Cette propriété est sujette à la déstructuration et est utilisée comme deuxième argument.

Le premier argument est createElement . Il s'agit d'une fonction qui indique à Vue quel nœud créer. Par souci de brièveté et de normalisation du code, j'utilise l'abréviation h pour createElement . Vous pouvez lire pourquoi je l'ai fait ici .

Donc, h prend trois arguments:

  1. Balise HTML (par exemple div ).
  2. Un objet de données contenant des attributs de modèle (par exemple, { class: 'something'} ).
  3. Chaînes de texte (si nous ajoutons simplement du texte) ou nœuds enfants créés à l'aide de h .

Voici à quoi ça ressemble:

 render(h, { props }) {  return h("div", { class: "example-class" }, "Here's my example text") } 

Résumons ce que nous avons déjà créé. À savoir, nous avons maintenant les éléments suivants:

  1. Un fichier avec des données qui est prévu pour être utilisé dans la formation de la page.
  2. Un composant Vue standard qui importe un fichier de données.
  3. Le cadre du composant fonctionnel qui est responsable de l'affichage des lignes de la table.

Pour créer des lignes de table, les données du format JSON doivent être passées en argument à h . Vous pouvez transférer toutes ces données en une seule fois, mais avec cette approche, vous aurez besoin d'une grande quantité de logique conditionnelle, ce qui dégradera la lisibilité du code. Au lieu de cela, j'ai décidé de faire ceci:

  1. Transformez les données en un format standardisé.
  2. Affichez les données transformées.

Transformation des données


Je souhaite que mes données soient présentées dans un format correspondant aux arguments acceptés par h . Mais avant de les convertir, j'ai prévu quelle structure ils devraient avoir dans le fichier JSON:

 //   {  tag: "", // HTML-    cellClass: "", //   .       -   null  text: "", // ,     children: [] //   ,     .     ,    . } 

Chaque objet représente une cellule du tableau. Quatre cellules forment chaque ligne du tableau (elles sont rassemblées dans un tableau):

 //   [ { cell1 }, { cell2 }, { cell3 }, { cell4 } ] 

Le point d'entrée peut être une fonction comme celle-ci:

 function createRow(data) { //      ,         let { text, element, classes = null, properties, usage } = data;  let row = [];  row[0] = createCellData(data) //         row[1] = createCellData(data)  row[2] = createCellData(data)  row[3] = createCellData(data)  return row; } 

Regardons à nouveau la disposition.


Mise en page

Vous pouvez voir que dans la première colonne, les éléments sont de style différent. Et dans les colonnes restantes, le même formatage est utilisé. Commençons donc par ça.
Permettez-moi de vous rappeler que je voudrais utiliser la structure JSON suivante comme modèle pour décrire chaque cellule:

 {  tag: "",  cellClass: "",  text: "",  children: [] } 

Avec cette approche, une structure arborescente sera utilisée pour décrire chaque cellule. C'est précisément parce que certaines cellules contiennent des groupes d'enfants. Nous utilisons les deux fonctions suivantes pour créer des cellules:

  • La fonction createNode prend chacune des propriétés qui nous intéressent comme argument.
  • La fonction createCell joue le rôle d'un wrapper autour de createNode , avec son aide, nous vérifions si le text l'argument text tableau. Si c'est le cas, nous créons un tableau d'enfants.

 //    function createCellData(tag, text) {  let children;  //  ,         const nodeClass = "body-text body-text--md body-text--semibold";  //   text   -   ,    span.  if (Array.isArray(text)) {    children = text.map(child => createNode("span", null, child, children));   return createNode(tag, nodeClass, text, children); } //    function createNode(tag, nodeClass, text, children = []) {  return {    tag: tag,    cellClass: nodeClass,    text: children.length ? null : text,    children: children  }; } 

Maintenant, nous pouvons faire quelque chose comme ça:

 function createRow(data) {  let { text, element, classes = null, properties, usage } = data;  let row = [];  row[0] = ""  row[1] = createCellData("p", ?????) //         row[2] = createCellData("p", properties) //    row[3] = createCellData("p", usage) //    return row; } 

Lors de la formation des troisième et quatrième colonnes, nous transmettons les properties et l' usage comme arguments de texte. Cependant, la deuxième colonne est différente des troisième et quatrième. Ici, nous affichons les noms des classes, qui, dans les données source, sont stockées sous cette forme:

 "classes": {  "base": "body-text body-text--lg",  "variants": ["body-text--bold", "body-text--regular"] }, 

De plus, n'oublions pas que lorsque vous travaillez avec des en-têtes, les classes ne sont pas utilisées. Par conséquent, nous devons créer des noms de balises d'en-tête pour les lignes correspondantes (c'est-à-dire h1 , h2 , etc.).

Nous créons des fonctions auxiliaires qui nous permettent de convertir ces données dans un format qui facilite leur utilisation comme argument de text .

 //          function displayClasses(element, classes) {  //   ,     (   )  return getClasses(classes) ? getClasses(classes) : element; } //       (    )     (   ),   null (  ,   ) // : "body-text body-text--sm" or ["body-text body-text--sm body-text--bold", "body-text body-text--sm body-text--italic"] function getClasses(classes) {  if (classes) {    const { base, variants = null } = classes;    if (variants) {      //             return variants.map(variant => base.concat(`${variant}`));       return base;   return classes; } 

Maintenant, nous pouvons faire ce qui suit:

 function createRow(data) {  let { text, element, classes = null, properties, usage } = data;  let row = [];  row[0] = ""  row[1] = createCellData("p", displayClasses(element, classes)) //    row[2] = createCellData("p", properties) //    row[3] = createCellData("p", usage) //    return row; } 

Transformation des données utilisées pour démontrer les styles


Nous devons décider quoi faire avec la première colonne du tableau, montrant des exemples d'application de styles. Cette colonne est différente des autres. Ici, nous appliquons de nouvelles balises et classes à chaque cellule au lieu d'utiliser la combinaison de classes utilisée par les colonnes restantes:

 <p class="body-text body-text--md body-text--semibold"> 

Au lieu d'essayer d'implémenter cette fonctionnalité dans createCellData ou createNodeData , je propose de créer une nouvelle fonction qui tirera parti des capacités de ces fonctions de base qui effectuent la transformation des données. Il mettra en œuvre un nouveau mécanisme de traitement des données:

 function createDemoCellData(data) {  let children;  const classes = getClasses(data.classes);  //   ,       ,               if (Array.isArray(classes)) {    children = classes.map(child =>      //    "data.text"      ,   ,            createNode("span", child, data.text, children)    );   //   ,       if (typeof classes === "string") {    return createNode("p", classes, data.text, children);   //  ,    ( )  return createNode(data.element, null, data.text, children); } 

Maintenant, les données de chaîne sont réduites à un format normalisé et peuvent être passées à la fonction de rendu:

 function createRow(data) {  let { text, element, classes = null, properties, usage } = data  let row = []  row[0] = createDemoCellData(data)  row[1] = createCellData("p", displayClasses(element, classes))  row[2] = createCellData("p", properties)  row[3] = createCellData("p", usage)  return row } 

Rendu des données


Voici comment rendre les données affichées sur la page:

 //   ,    "props" const rowData = props.rowData; //   ,     const row = createRow(rowData); //    "div"     return h("div", { class: "row" }, row.map(cell => renderCells(cell))); //    function renderCells(data) {  //  ,      if (data.children.length) {    return renderCell(      data.tag, //          { //            class: {          group: true, //    ""               [data.cellClass]: data.cellClass //      ,                  },      //        data.children.map(child => {        return renderCell(          child.tag,          { class: child.cellClass },          child.text        );      })    );   //     -     return renderCell(data.tag, { class: data.cellClass }, data.text); } // -  "h"     function renderCell(tag, classArgs, text) {  return h(tag, classArgs, text); } 

Maintenant, tout est prêt ! Ici encore, le code source.

Résumé


Il vaut la peine de dire que l'approche considérée ici est une manière expérimentale de résoudre un problème plutôt trivial. Je suis sûr que beaucoup diront que cette solution est excessivement compliquée et surchargée d'excès d'ingénierie. Je serai peut-être d'accord avec cela.

Malgré le fait que le développement de ce projet ait pris beaucoup de temps, les données sont désormais complètement séparées de la présentation. Maintenant, si nos concepteurs viennent ajouter des lignes au tableau ou en supprimer des lignes existantes, je n'ai pas besoin de ratisser le code HTML déroutant . Pour ce faire, il me suffira de modifier plusieurs propriétés dans le fichier JSON.

Le résultat vaut-il l'effort? Je pense qu'il faut regarder les circonstances. Ceci, cependant, est très caractéristique de la programmation. Je veux dire que dans ma tête, dans le processus de travail sur ce projet, l'image suivante est constamment apparue.


C'est peut-être la réponse à ma question de savoir si ce projet vaut l'effort consacré à son développement.

Chers lecteurs! ? ?

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


All Articles