Um exemplo prático de uso das funções de renderização do Vue: criação de uma grade tipográfica para um sistema de design

No material, cuja tradução estamos publicando hoje, falaremos sobre como criar uma grade de impressão para o sistema de design usando as funções de renderização do Vue . Aqui está uma demonstração do projeto que vamos revisar aqui. Você pode encontrar seu código aqui. O autor deste material diz que ele usou funções de renderização porque elas permitem um controle muito mais preciso sobre o processo de criação de código HTML do que os modelos regulares do Vue. No entanto, para sua surpresa, ele não conseguiu encontrar exemplos práticos de sua aplicação. Ele encontrou apenas manuais de instruções. Ele espera que este material faça a diferença para melhor, graças ao exemplo prático de uso das funções de renderização do Vue aqui.


Funções de renderização do Vue


As funções de renderização sempre me pareciam algo incomum para o Vue. Tudo nessa estrutura enfatiza o desejo de simplicidade e a separação de deveres de várias entidades. Mas as funções de renderização são uma estranha mistura de HTML e JavaScript, que geralmente é difícil de ler.

Por exemplo, aqui está a marcação HTML:

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

Para formar, você precisa da seguinte função:

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

Suspeito que tais construções façam com que muitos se afastem imediatamente das funções de renderização. Afinal, a facilidade de uso é exatamente o que atrai os desenvolvedores ao Vue. É uma pena que muitas pessoas não vejam seus verdadeiros méritos por trás da aparência desagradável das funções de renderização. O fato é que funções de renderização e componentes funcionais são ferramentas interessantes e poderosas. Eu, para demonstrar suas capacidades e seu verdadeiro valor, falarei sobre como eles me ajudaram a resolver o problema real.

Observe que será muito útil abrir uma versão demo do projeto em consideração na guia do navegador vizinho e acessá-la enquanto lê o artigo.

Definindo critérios para um sistema de design


Temos um sistema de design baseado no VuePress . Precisávamos incluir uma nova página, demonstrando várias possibilidades tipográficas do design do texto. Foi assim que o layout que o designer me deu parecia.


Layout da página

E aqui está um exemplo do código CSS correspondente 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; } 

Os cabeçalhos são formatados com base nos nomes das tags. Para formatar outros elementos, nomes de classe são usados. Além disso, existem classes separadas para riqueza e tamanhos de fonte.

Antes de começar a escrever o código, formulei algumas regras:

  • Como o principal objetivo desta página é a visualização de dados, os dados devem ser armazenados em um arquivo separado.
  • Para formatar cabeçalhos, as tags de cabeçalho semântico devem ser usadas (ou seja, <h1> , <h2> e assim por diante), sua formatação não deve ser baseada na classe.
  • Tags de parágrafo ( <p> ) com nomes de classe (por exemplo, <p class="body-text--lg"> ) devem ser usadas no corpo da página.
  • Os materiais que consistem em vários elementos devem ser agrupados envolvendo-os na <p> raiz <p> ou em outro elemento raiz adequado que não tenha uma classe de estilização atribuída. Os elementos filhos devem ser agrupados em uma <span> , que define o nome da classe. Veja como essa regra pode ser:

     <p>  <span class="body-text--lg">Thing 1</span>  <span class="body-text--lg">Thing 2</span> </p> 
  • Os materiais cuja saída não há requisitos especiais devem ser agrupados na <p> , à qual é atribuído o nome da classe desejada. Os elementos filhos devem estar entre uma <span> :

     <p class="body-text--semibold">  <span>Thing 1</span>  <span>Thing 2</span> </p> 
  • Para que cada célula estilizada seja estilizada, os nomes das classes devem ser gravados apenas uma vez.

Opções para resolver o problema


Antes de começar o trabalho, considerei várias opções para resolver a tarefa definida diante de mim. Aqui está a visão geral deles.

WritingManualmente escrevendo código HTML


Eu gosto de escrever código HTML manualmente, mas somente quando ele nos permite resolver adequadamente o problema existente. Entretanto, no meu caso, a escrita manual de código significaria inserir vários fragmentos de código repetidos nos quais algumas variações estão presentes. Eu não gostei Além disso, isso significa que os dados não podem ser armazenados em um arquivo separado. Como resultado, recusei essa abordagem.

Se eu tivesse criado a página em questão dessa maneira, teria algo como o seguinte:

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

SingUsando padrões tradicionais do Vue


Sob condições normais, essa abordagem é usada com mais frequência. No entanto, dê uma olhada neste exemplo.


Um exemplo de uso de modelos do Vue

Na primeira coluna, há o seguinte:

  • A <h1> , apresentada na forma em que o navegador a exibe.
  • A <p> agrupa vários filhos <span> com texto. A cada um desses elementos é atribuída uma classe (mas nenhuma classe especial é atribuída à própria tag <p> ).
  • A <p> que não possui elementos <span> aninhados aos quais a classe está atribuída.

Para implementar tudo isso, muitas instâncias das diretivas v-if e v-if-else seriam necessárias. E isso, eu sei, levaria ao fato de que o código se tornaria muito confuso muito em breve. Além disso, não gosto do uso de toda essa lógica condicional na marcação.

Funções de renderização


Como resultado, depois de analisar as possíveis alternativas, selecionei as funções de renderização. Neles, usando JavaScript, usando construções condicionais, nós filhos de outros nós são criados. Ao criar esses nós filhos, todos os critérios necessários são levados em consideração. Nessa situação, essa solução parecia perfeita para mim.

Modelo de dados


Como eu disse, queria armazenar dados tipográficos em um arquivo JSON separado. Isso permitiria, se necessário, fazer alterações neles sem tocar na marcação. Estes são os dados.

Cada objeto JSON no arquivo é uma descrição de uma linha separada:

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

Aqui está o HTML que vem após o processamento deste 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> 

Agora considere um exemplo mais complexo. Matrizes representam grupos de crianças. As propriedades dos objetos de classes , que são objetos, podem armazenar descrições de classe. A propriedade base do objeto de classes contém uma descrição das classes comuns a todos os nós na célula. Cada classe presente na propriedade variants é aplicada a um único elemento no 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 transforma no seguinte 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> 

A estrutura básica do projeto


Temos um componente pai, TypographyTable.vue , que contém marcação para formar a tabela. Também temos um componente filho, TypographyRow.vue , responsável pela criação da linha da tabela e contém nossa função de renderização.

Ao formar linhas de tabela, uma matriz com dados é percorrida. Os objetos que descrevem as linhas da tabela são passados ​​para o componente TypographyRow como propriedades.

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

Aqui, gostaria de observar uma ninharia agradável: os dados tipográficos em uma instância do Vue podem ser representados como uma propriedade. Você pode acessá-los usando a construção $options.typographyData , pois eles não mudam e não devem ser reativos (graças a Anton Kosykh ).

Criando um componente funcional


O componente TypographyRow que processa dados é um componente funcional. Componentes funcionais são entidades que não possuem estados e instâncias. Isso significa que eles não têm this e que não têm acesso aos métodos de ciclo de vida do componente Vue.

Aqui está o "esqueleto" de um componente semelhante com o qual começaremos a trabalhar em nosso componente:

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

O método do componente de render usa um argumento de context que possui uma propriedade props . Esta propriedade está sujeita a desestruturação e é usada como o segundo argumento.

O primeiro argumento é createElement . Essa é uma função que informa ao Vue qual nó criar. Por uma questão de brevidade e padronização do código, eu uso a abreviação h para createElement . Você pode ler sobre por que eu fiz isso aqui .

Então h leva três argumentos:

  1. Tag HTML (por exemplo, div ).
  2. Um objeto de dados contendo atributos de modelo (por exemplo, { class: 'something'} ).
  3. Sequências de texto (se adicionarmos apenas texto) ou nós filhos criados usando h .

Aqui está o que parece:

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

Vamos resumir o que já criamos. Nomeadamente, agora temos o seguinte:

  1. Um arquivo com dados planejados para serem usados ​​na formação da página.
  2. Um componente regular do Vue que importa um arquivo de dados.
  3. A estrutura do componente funcional que é responsável por exibir as linhas da tabela.

Para criar linhas da tabela, os dados do formato JSON devem ser passados ​​como argumento para h . Você pode transferir todos esses dados de uma só vez, mas com essa abordagem, você precisará de uma grande quantidade de lógica condicional, o que degradará a compreensibilidade do código. Em vez disso, decidi fazer o seguinte:

  1. Transforme dados em um formato padronizado.
  2. Exibir dados transformados.

Transformação de dados


Gostaria que meus dados fossem apresentados em um formato que corresponda aos argumentos aceitos por h . Mas antes de convertê-los, planejei qual estrutura eles deveriam ter no arquivo JSON:

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

Cada objeto representa uma célula da tabela. Quatro células formam cada linha da tabela (elas são coletadas em uma matriz):

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

O ponto de entrada pode ser uma função como a seguinte:

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

Vamos olhar para o layout novamente.


Layout da página

Você pode ver que na primeira coluna os elementos são estilizados de maneira diferente. E nas colunas restantes a mesma formatação é usada. Então, vamos começar com isso.
Deixe-me lembrá-lo de que gostaria de usar a seguinte estrutura JSON como modelo para descrever cada célula:

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

Com essa abordagem, uma estrutura semelhante a uma árvore será usada para descrever cada célula. Isto é precisamente porque algumas células contêm grupos de crianças. Usamos as duas funções a seguir para criar células:

  • A função createNode usa cada uma das propriedades em que estamos interessados ​​como argumento.
  • A função createCell desempenha o papel de um wrapper em torno do createNode , com sua ajuda, verificamos se o text do argumento text matriz. Nesse caso, criamos uma matriz de filhos.

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

Agora podemos fazer algo assim:

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

Ao formar a terceira e quarta colunas, passamos properties e usage como argumentos de texto. No entanto, a segunda coluna é diferente da terceira e quarta. Aqui, exibimos os nomes das classes, que, nos dados de origem, são armazenadas neste formato:

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

Além disso, não devemos esquecer que, ao trabalhar com cabeçalhos, as classes não são usadas. Portanto, precisamos criar nomes de tags de cabeçalho para as linhas correspondentes (ou seja, h1 , h2 e assim por diante).

Criamos funções auxiliares que nos permitem converter esses dados em um formato que facilita seu 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; } 

Agora podemos fazer o seguinte:

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

Transformação de dados usados ​​para demonstrar estilos


Precisamos decidir o que fazer com a primeira coluna da tabela, mostrando exemplos da aplicação de estilos. Esta coluna é diferente das outras. Aqui, aplicamos novas tags e classes a cada célula, em vez de usar a combinação de classes usada pelas colunas restantes:

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

Em vez de tentar implementar essa funcionalidade em createCellData ou createNodeData , proponho a criação de uma nova função que aproveitará os recursos dessas funções básicas que executam a transformação de dados. Ele implementará um novo mecanismo de processamento de dados:

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

Agora, os dados da string são reduzidos para um formato normalizado e podem ser passados ​​para a função de renderização:

 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 } 

Renderização de dados


Veja como renderizar os dados exibidos na 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); } 

Agora está tudo pronto ! Aqui , novamente, o código fonte.

Sumário


Vale dizer que a abordagem aqui considerada é uma maneira experimental de resolver um problema bastante trivial. Estou certo de que muitos dirão que esta solução é excessivamente complicada e sobrecarregada com excessos de engenharia. Talvez eu concorde com isso.

Apesar de o desenvolvimento deste projeto ter demorado muito, os dados agora estão completamente separados da apresentação. Agora, se nossos designers entrarem para adicionar algumas linhas à tabela ou remover quaisquer linhas existentes dela, não preciso usar o código HTML confuso . Para fazer isso, basta alterar várias propriedades no arquivo JSON.

O resultado vale o esforço? Eu acho que é necessário olhar para as circunstâncias. Isso, no entanto, é muito característico da programação. Quero dizer que, na minha cabeça, no processo de trabalhar neste projeto, a figura a seguir apareceu constantemente.


Talvez essa seja a resposta para minha pergunta sobre se esse projeto vale o esforço despendido em seu desenvolvimento.

Caros leitores! ? ?

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


All Articles