使用Vue渲染功能的实际示例:为设计系统创建印刷网格

在我们今天发布的翻译材料中,我们将讨论如何使用Vue渲染功能为设计系统创建打印网格。 这是项目演示,我们将在这里进行审查。 可以在此处找到其代码。 该材料的作者说他之所以使用渲染功能,是因为与常规的Vue模板相比,它们可以更精确地控制创建HTML代码的过程。 但是,令他惊讶的是,他找不到实际应用的实例。 他只遇到了使用说明书。 他希望通过使用Vue渲染功能的实际示例,使这种材料能够更好地发挥作用。


Vue渲染功能


在我看来,渲染功能始终对Vue来说有点不寻常。 此框架中的所有内容都强调了对简单性和各个实体职责分离的渴望。 但是渲染功能是HTML和JavaScript的奇怪结合,通常很难阅读。

例如,这是HTML标记:

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

要形成它,您需要以下功能:

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

我怀疑这样的构造会使许多人立即放弃渲染功能。 毕竟,易用性正是吸引开发人员使用Vue的原因。 如果许多人没有看到渲染功能难看的外观背后的真实价值,这是一个遗憾。 问题是渲染功能和功能组件是有趣且功能强大的工具。 为了证明他们的能力和真正的价值,我将谈论他们如何帮助我解决实际问题。

请注意,在相邻的浏览器选项卡中打开正在考虑的项目的演示版本,并在阅读文章时对其进行访问将非常有用。

定义设计系统的标准


我们有一个基于VuePress的设计系统。 我们需要在其中添加一个新页面,以演示文本设计的各种印刷可能性。 这就是设计师给我的布局外观。


页面布局

以下是与此页面对应的CSS代码的示例:

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

标头是根据标签名称格式化的。 要格式化其他元素,请使用类名称。 此外,还有用于丰富性和字体大小的单独类。

在开始编写代码之前,我制定了一些规则:

  • 由于此页面的主要目的是数据可视化,因此数据应存储在单独的文件中。
  • 要格式化标题,应使用语义标头标记(即<h1><h2>等),其格式不应基于该类。
  • 在页面的正文中应使用带有类名(例如, <p class="body-text--lg"> )的段落标签( <p> )。
  • 由各种元素组成的材料应通过将它们包装在根<p>中或未分配样式化类的另一个合适的根元素中进行分组。 子元素必须包装在<span> ,该<span>用于设置类名称。 这条规则可能是这样的:

     <p>  <span class="body-text--lg">Thing 1</span>  <span class="body-text--lg">Thing 2</span> </p> 
  • 没有特殊要求的输出材料应包装在<p> ,该<p>已分配了所需的类名称。 子元素必须包含在<span>

     <p class="body-text--semibold">  <span>Thing 1</span>  <span>Thing 2</span> </p> 
  • 对于要样式化的每个样式化的单元格,类名只能被写入一次。

解决问题的选项


在开始工作之前,我考虑了几种解决摆在我面前的任务的选项。 这是他们的概述。

▍手动编写HTML代码


我喜欢手动编写HTML代码,但前提是它可以让我们充分解决现有问题。 但是,在我的情况下,手动编写代码意味着要输入各种重复的代码片段,其中存在一些变化。 我不喜欢 此外,这意味着无法将数据存储在单独的文件中。 结果,我拒绝了这种方法。

如果我像这样创建有问题的页面,我将得到以下内容:

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

traditional使用传统的Vue模式


在正常情况下,这种方法最常用。 但是,看看这个例子。


使用Vue模板的示例

在第一列中有以下内容:

  • <h1> ,以浏览器显示它的形式显示。
  • <p>将多个<span>子级与文本分组。 为这些元素中的每个元素分配了一个类(但没有为<p>标记本身分配任何特殊的类)。
  • <p> ,该<p>没有嵌套该类的<span>元素。

要实现所有这些,将需要v-ifv-if-else指令的许多实例。 我知道,这将导致这样的事实,即代码很快就会变得非常混乱。 另外,我不喜欢在标记中使用所有这些条件逻辑。

▍渲染功能


结果,在分析了可能的替代方法之后,我选择了渲染函数。 在它们中,使用JavaScript,使用条件构造来创建其他节点的子节点。 创建这些子节点时,将考虑所有必要条件。 在这种情况下,这种解决方案对我来说似乎很完美。

资料模型


正如我所说,我想将印刷数据存储在一个单独的JSON文件中。 如有必要,这将允许在不更改标记的情况下对其进行更改。 这些是数据。

文件中的每个JSON对象都是单独一行的描述:

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

这是处理此对象后出现的HTML:

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

现在考虑一个更复杂的示例。 数组代表子组。 classes对象的属性本身就是对象,可以存储类描述。 classes对象的base属性包含对单元中所有节点通用的类的描述。 variants属性中存在的每个类都应用于组中的单个元素。

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

该对象变成以下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> 

项目的基本结构


我们有一个父组件TypographyTable.vue ,其中包含用于形成表格的标记。 我们还有一个子组件TypographyRow.vue ,它负责创建表行并包含我们的render函数。

形成表行时,将遍历带有数据的数组。 描述表行的对象作为属性传递给TypographyRow组件。

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

在这里,我要指出一个令人愉快的地方:Vue实例中的印刷数据可以表示为属性。 您可以使用$options.typographyData构造访问它们,因为它们不会更改并且不应是被动的(感谢Anton Kosykh )。

创建功能组件


处理数据的TypographyRow组件是功能组件。 功能组件是没有状态和实例的实体。 这意味着他们没有this ,并且他们无权访问Vue组件生命周期方法。

这是相似组件的“骨架”,我们将开始使用该组件进行工作:

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

render组件方法采用具有props属性的context参数。 此属性可以进行解构,并用作第二个参数。

第一个参数是createElement 。 这是一个告诉Vue创建节点的功能。 为了代码的简洁和标准化,我使用createElement的简写形式。 您可以在这里阅读有关我为什么这样做的信息

因此, h需要三个参数:

  1. HTML标记(例如div )。
  2. 包含模板属性的数据对象(例如{ class: 'something'} )。
  3. 文本字符串(如果我们仅添加文本)或使用h创建的子节点。

看起来是这样的:

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

让我们总结一下我们已经创建的内容。 即,我们现在有以下内容:

  1. 文件,该文件包含计划在页面结构中使用的数据。
  2. 导入数据文件的常规Vue组件。
  3. 负责显示表中各行的功能组件的框架。

要创建表行,必须将JSON格式的数据作为参数传递给h 。 您可以一次性传输所有此类数据,但是使用这种方法,您将需要大量的条件逻辑,这将降低代码的可理解性。 相反,我决定这样做:

  1. 将数据转换为标准格式。
  2. 显示转换后的数据。

数据转换


我希望以与h接受的参数匹配的格式显示数据。 但是在转换它们之前,我计划了它们在JSON文件中应具有的结构:

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

每个对象代表表的一个单元格。 表格的每一行由四个单元格组成(它们收集在一个数组中):

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

输入点可能是如下函数:

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

让我们再次看一下布局。


页面布局

您可以在第一列中看到元素的样式不同。 在其余的列中,使用相同的格式。 因此,让我们从此开始。
让我提醒您,我想使用以下JSON结构作为描述每个单元格的模型:

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

通过这种方法,树状结构将用于描述每个单元。 正是因为某些单元格包含子组。 我们使用以下两个函数来创建单元格:

  • createNode函数将我们感兴趣的每个属性用作参数。
  • createCell函数充当createNode的包装器,借助它的帮助,我们可以检查参数text数组。 如果是这样,我们将创建一系列子代。

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

现在我们可以做这样的事情:

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

在形成第三和第四列时,我们将propertiesusage作为文本参数传递。 但是,第二列不同于第三列和第四列。 在这里,我们显示类的名称,这些名称在源数据中以这种形式存储:

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

另外,我们不要忘记在使用标头时不使用类。 因此,我们需要为相应的行(例如h1h2等)创建标题标签名称。

我们创建辅助功能,使我们能够将此数据转换为便于将其用作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; } 

现在,我们可以执行以下操作:

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

用于演示样式的数据转换


我们需要决定如何处理表格的第一列,以显示样式应用程序的示例。 此列与其他列不同。 在这里,我们将新的标记和类应用于每个单元,而不是使用其余列使用的类的组合:

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

我建议createCellData创建一个新功能,以利用这些执行数据转换的基本功能的功能,而不是尝试在createCellDatacreateNodeData实现此功能。 它将实现新的数据处理机制:

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

现在,字符串数据被简化为规范化格式,并且可以传递给render函数:

 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 } 

数据渲染


以下是呈现页面上显示的数据的方法:

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

现在一切就绪还是这里的源代码。

总结


值得一提的是,这里考虑的方法是解决一个相当琐碎的问题的实验方法。 我敢肯定,很多人会说这种解决方案是不合理的复杂,并且由于工程过剩而过载。 也许我会同意这一点。

尽管该项目的开发花费了很多时间,但现在数据与演示完全分开了。 现在,如果我们的设计师进来在表中添加一些行,或从表中删除任何现有的行,那么我就不必弄乱令人困惑的 HTML代码。 为此,对于我来说更改JSON文件中的几个属性就足够了。

结果值得付出努力吗? 我认为有必要看一下情况。 但是,这是编程的特征。 我想说的是,在我从事这个项目的过程中,以下图片不断出现。


也许这是我关于这个项目是否值得花在其开发上的努力的答案。

亲爱的读者们!您可以对此处讨论的项目表达什么想法和建议?您以什么方式解决了此类问题?

Source: https://habr.com/ru/post/zh-CN458494/


All Articles