Un ejemplo práctico del uso de las funciones de render de Vue: crear una cuadrícula tipográfica para un sistema de diseño

En el material, cuya traducción publicamos hoy, hablaremos sobre cómo crear una cuadrícula de impresión para el sistema de diseño utilizando las funciones de renderización Vue . Aquí hay una demostración del proyecto que vamos a revisar aquí. Puedes encontrar su código aquí. El autor de este material dice que utilizó funciones de renderizado debido a que permiten un control mucho más preciso sobre el proceso de creación de código HTML que las plantillas Vue normales. Sin embargo, para su sorpresa, no pudo encontrar ejemplos prácticos de su aplicación. Solo encontró manuales de instrucciones. Espera que este material marque la diferencia para mejor gracias al ejemplo práctico del uso de las funciones de render de Vue.


Funciones de renderizado Vue


Las funciones de render siempre me parecieron algo inusual para Vue. Todo en este marco enfatiza el deseo de simplicidad y la separación de deberes de varias entidades. Pero las funciones de renderizado son una extraña mezcla de HTML y JavaScript, que a menudo es difícil de leer.

Por ejemplo, aquí está el marcado HTML:

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

Para formarlo, necesita la siguiente función:

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

Sospecho que tales construcciones harán que muchos se alejen inmediatamente de las funciones de renderizado. Después de todo, la facilidad de uso es exactamente lo que atrae a los desarrolladores a Vue. Es una pena que muchas personas no vean sus verdaderos méritos detrás de la apariencia antiestética de las funciones de renderizado. La cuestión es que las funciones de render y los componentes funcionales son herramientas interesantes y poderosas. Yo, para demostrar sus capacidades y su verdadero valor, hablaré sobre cómo me ayudaron a resolver el problema real.

Tenga en cuenta que será muy útil abrir una versión demo del proyecto en consideración en una pestaña del navegador adyacente y acceder a ella mientras lee un artículo.

Definición de criterios para un sistema de diseño.


Tenemos un sistema de diseño basado en VuePress . Necesitábamos incluir una nueva página en ella, demostrando varias posibilidades tipográficas de diseño de texto. Así es como se veía el diseño que me dio el diseñador.


Diseño de página

Y aquí hay un ejemplo del código CSS correspondiente a esta página:

 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; } 

Los encabezados están formateados en función de los nombres de las etiquetas. Para formatear otros elementos, se utilizan nombres de clase. Además, hay clases separadas para riqueza y tamaños de fuente.

Antes de comenzar a escribir código, formulé algunas reglas:

  • Dado que el objetivo principal de esta página es la visualización de datos, los datos deben almacenarse en un archivo separado.
  • Para formatear encabezados, se deben usar etiquetas de encabezado semántico (es decir, <h1> , <h2> etc.), su formato no debe basarse en la clase.
  • Las etiquetas de párrafo ( <p> ) con nombres de clase (por ejemplo, <p class="body-text--lg"> ) deben usarse en el cuerpo de la página.
  • Los materiales que consisten en varios elementos deben agruparse envolviéndolos en la <p> raíz <p> , o en otro elemento raíz adecuado que no tenga asignada una clase de estilización. Los elementos secundarios deben estar envueltos en una <span> , que establece el nombre de la clase. Así es como podría verse esta regla:

     <p>  <span class="body-text--lg">Thing 1</span>  <span class="body-text--lg">Thing 2</span> </p> 
  • Los materiales, cuya salida no tiene requisitos especiales, deben incluirse en la <p> , a la que se le asigna el nombre de clase deseado. Los elementos secundarios deben estar encerrados en una <span> :

     <p class="body-text--semibold">  <span>Thing 1</span>  <span>Thing 2</span> </p> 
  • Para cada celda con estilo que se va a diseñar, los nombres de clase se deben escribir solo una vez.

Opciones para resolver el problema.


Antes de comenzar a trabajar, consideré varias opciones para resolver la tarea que tenía ante mí. Aquí está su descripción general.

▍ Escribir manualmente código HTML


Me gusta escribir código HTML manualmente, pero solo cuando nos permite resolver adecuadamente el problema existente. Sin embargo, en mi caso, la escritura manual de código significaría ingresar varios fragmentos de código repetidos en los que existen algunas variaciones. No me gustó Además, esto significaría que los datos no podrían almacenarse en un archivo separado. Como resultado, rechacé este enfoque.

Si crease la página en cuestión así, habría obtenido algo como lo siguiente:

 <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> 

▍Utilizando patrones Vue tradicionales


En condiciones normales, este enfoque se usa con mayor frecuencia. Sin embargo, eche un vistazo a este ejemplo.


Un ejemplo de uso de plantillas Vue

En la primera columna hay lo siguiente:

  • La <h1> , que se presenta en la forma en que la muestra el navegador.
  • La <p> agrupa a varios <span> hijos con texto. A cada uno de estos elementos se le asigna una clase (pero no se asigna ninguna clase especial a la etiqueta <p> sí).
  • La <p> que no tiene elementos <span> anidados a los que se asigna la clase.

Para implementar todo esto, se requerirían muchas instancias de las directivas v-if y v-if-else . Y esto, lo sé, llevaría al hecho de que el código se volvería muy confuso muy pronto. Además, no me gusta el uso de toda esta lógica condicional en el marcado.

▍Funciones de Render


Como resultado, después de analizar las posibles alternativas, seleccioné las funciones de render. En ellos, usando JavaScript, usando construcciones condicionales, se crean nodos secundarios de otros nodos. Al crear estos nodos secundarios, se tienen en cuenta todos los criterios necesarios. En esta situación, tal solución me pareció perfecta.

Modelo de datos


Como dije, quería almacenar datos tipográficos en un archivo JSON separado. Esto permitiría, si es necesario, realizar cambios en ellos sin tocar el marcado. Estos son los datos.

Cada objeto JSON en el archivo es una descripción de una línea separada:

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

Aquí está el HTML que viene después de procesar este objeto:

 <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> 

Ahora considere un ejemplo más complejo. Las matrices representan grupos de niños. Las propiedades de los objetos de classes , que en sí mismos son objetos, pueden almacenar descripciones de clases. La propiedad base del objeto de classes contiene una descripción de las clases que son comunes a todos los nodos en la celda. Cada clase presente en la propiedad de variants se aplica a un único elemento en el grupo.

 {  "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"] } 

Este objeto se convierte en el siguiente HTML:

 <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 estructura básica del proyecto.


Tenemos un componente principal, TypographyTable.vue , que contiene marcado para formar la tabla. También tenemos un componente secundario, TypographyRow.vue , que es responsable de crear la fila de la tabla y contiene nuestra función de representación.

Al formar filas de tabla, se atraviesa una matriz con datos. Los objetos que describen las filas de la tabla se pasan al componente TypographyRow como propiedades.

 <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> 

Aquí me gustaría señalar una pequeña cosa agradable: los datos tipográficos en una instancia de Vue se pueden representar como una propiedad. Puede acceder a ellos utilizando la construcción $options.typographyData , ya que no cambian y no deberían ser reactivos (gracias a Anton Kosykh ).

Crear un componente funcional


El componente TypographyRow que procesa datos es un componente funcional. Los componentes funcionales son entidades que no tienen estados ni instancias. Esto significa que no tienen this , y que no tienen acceso a los métodos del ciclo de vida del componente Vue.

Aquí está el "esqueleto" de un componente similar con el que comenzaremos a trabajar en nuestro componente:

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

El método de componente de render toma un argumento de context que tiene una propiedad de props . Esta propiedad está sujeta a la desestructuración y se utiliza como segundo argumento.

El primer argumento es createElement . Esta es una función que le dice a Vue qué nodo crear. En aras de la brevedad y la estandarización del código, uso la abreviatura h para createElement . Puedes leer sobre por qué hice esto aquí .

Entonces h toma tres argumentos:

  1. Etiqueta HTML (por ejemplo, div ).
  2. Un objeto de datos que contiene atributos de plantilla (por ejemplo, { class: 'something'} ).
  3. Cadenas de texto (si solo agregamos texto) o nodos secundarios creados con h .

Así es como se ve:

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

Resumamos lo que ya hemos creado. A saber, ahora tenemos lo siguiente:

  1. Un archivo con datos que se planea utilizar para formar la página.
  2. Un componente Vue normal que importa un archivo de datos.
  3. El marco del componente funcional que se encarga de mostrar las filas de la tabla.

Para crear filas de tabla, los datos del formato JSON se deben pasar como argumento a h . Puede transferir todos esos datos de una sola vez, pero con este enfoque necesitará una gran cantidad de lógica condicional, lo que degradará la comprensión del código. En cambio, decidí hacer esto:

  1. Transforme los datos a un formato estandarizado.
  2. Mostrar datos transformados.

Transformación de datos


Me gustaría que mis datos se presenten en un formato que coincida con los argumentos aceptados por h . Pero antes de convertirlos, planeé qué estructura deberían tener en el archivo JSON:

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

Cada objeto representa una celda de la tabla. Cuatro celdas forman cada fila de la tabla (se recopilan en una matriz):

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

El punto de entrada puede ser una función como la siguiente:

 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; } 

Miremos el diseño nuevamente.


Diseño de página

Puede ver que en la primera columna los elementos tienen un estilo diferente. Y en las columnas restantes se usa el mismo formato. Entonces comencemos con esto.
Permítame recordarle que me gustaría usar la siguiente estructura JSON como modelo para describir cada celda:

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

Con este enfoque, se utilizará una estructura en forma de árbol para describir cada celda. Esto es precisamente porque algunas células contienen grupos de niños. Utilizamos las siguientes dos funciones para crear celdas:

  • La función createNode toma cada una de las propiedades que nos interesan como argumento.
  • La función createCell desempeña el papel de un contenedor alrededor de createNode , con su ayuda comprobamos si el text del argumento text matriz. Si es así, creamos una variedad de niños.

 //    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  }; } 

Ahora podemos hacer algo como esto:

 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; } 

Al formar las columnas tercera y cuarta, pasamos properties y usage como argumentos de texto. Sin embargo, la segunda columna es diferente de la tercera y cuarta. Aquí mostramos los nombres de las clases, que, en los datos de origen, se almacenan de esta forma:

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

Además, no olvidemos que cuando se trabaja con encabezados, las clases no se usan. Por lo tanto, necesitamos crear nombres de etiquetas de encabezado para las líneas correspondientes (es decir, h1 , h2 , etc.).

Creamos funciones auxiliares que nos permiten convertir estos datos en un formato que facilite su uso como argumento 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; } 

Ahora podemos hacer lo siguiente:

 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; } 

Transformación de datos utilizados para demostrar estilos.


Necesitamos decidir qué hacer con la primera columna de la tabla, que muestra ejemplos de la aplicación de estilos. Esta columna es diferente de las demás. Aquí aplicamos nuevas etiquetas y clases a cada celda en lugar de usar la combinación de clases utilizada por las columnas restantes:

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

En lugar de intentar implementar esta funcionalidad en createCellData o createNodeData , propongo crear una nueva función que aproveche las capacidades de estas funciones básicas que realizan la transformación de datos. Implementará un nuevo mecanismo de procesamiento de datos:

 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); } 

Ahora los datos de la cadena se reducen a un formato normalizado y se pueden pasar a la función de representación:

 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 } 

Procesamiento de datos


Aquí se explica cómo representar los datos que se muestran en la página:

 //   ,    "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); } 

¡Ahora todo está listo ! Aquí , nuevamente, el código fuente.

Resumen


Vale la pena decir que el enfoque considerado aquí es una forma experimental de resolver un problema bastante trivial. Estoy seguro de que muchos dirán que esta solución es excesivamente complicada y está sobrecargada con excesos de ingeniería. Quizás esté de acuerdo con eso.

A pesar de que el desarrollo de este proyecto tomó mucho tiempo, los datos ahora están completamente separados de la presentación. Ahora, si nuestros diseñadores entran para agregar algunas filas a la tabla, o eliminar cualquier fila existente de ella, no tengo que rastrillar el código HTML confuso . Para hacer esto, será suficiente para mí cambiar varias propiedades en el archivo JSON.

¿Vale la pena el resultado? Creo que es necesario mirar las circunstancias. Esto, sin embargo, es muy característico de la programación. Quiero decir que en mi cabeza, en el proceso de trabajar en este proyecto, apareció constantemente la siguiente imagen.


Quizás esta sea la respuesta a mi pregunta sobre si este proyecto vale la pena el esfuerzo dedicado a su desarrollo.

Estimados lectores! ? ?

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


All Articles