(譯)使用渲染函數構建一個設計系統的排版佈局

談談你對Vue函數式組件的理解?javascript

本身先想一分鐘css

譯者注:英語和文筆有限,不對之處歡迎留言斧正!原文地址:t.cn/AipuKWqvhtml

這篇文章介紹了我是如何使用Vue渲染函數爲設計系統構建網格佈局的。這裏是相關的demo代碼。我之因此選擇使用渲染函數是由於它比常規的Vue模板語法在可控性方面表現的更增強大。然而關於渲染函數的介紹,我在網上並未找到太多的文章,這讓我很驚訝。我但願這篇文章能彌補這方面的不足,而且提供一個有用和實用的使用Vue渲染函數的用例。vue

過去我老是以爲渲染函數太過於抽象,甚至有點不合時宜。雖然框架的其他部分強調簡單性和關注點分離,但渲染函數是一種奇怪的,一般難以閱讀的HTML和JavaScript混合。java

例如,下面的代碼:node

<div class="container">
  <p class="my-awesome-class">Some cool text</p>
</div>
複製代碼

使用渲染函數你須要:git

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

我懷疑這種語法會讓一些人頭大,由於簡單易上手是咱們一開始學習Vue的關鍵因素。這很遺憾,由於渲染函數和函數式組件可以提供一些很是酷,且功能強大的東西。好吧,讓咱們看下它是怎樣幫助我解決實際問題的。github

Tips: 在新標籤頁中打開示例代碼對照本文閱讀效果更佳哦~ 編程

1. 定義一個設計標準

個人團隊但願在咱們的VuePress驅動設計系統中包含一個頁面,展現不一樣的排版選項。下面的截圖是我從設計師那裏得到的效果圖的一部分。json

這裏是一些相應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~h6)元素,其餘項目用的是類名,而且還有設置字體加粗和大小的單獨類。

在寫代碼以前,咱們先作一些約定:

  • 因爲這其實是一個數據可視化展現,所以數據應該存儲在單獨的文件中

  • 標題應使用語義化標題標籤(例如<h1>, <h2>等)不依賴類

  • 正文內容應該使用帶有類名的段落(<p>)標籤(例如<p class="body-text--lg">

  • 變更的內容類型使用段落標籤或不帶樣式類的根元素組合在一塊兒,孩子元素應該用 <span>和類名包裹,例如:

    <p>
     <span class="body-text--lg">Thing 1</span>
     <span class="body-text--lg">Thing 2</span>
    </p>
    複製代碼
  • 沒有特殊樣式的內容都應使用帶有正確類名的段落標籤和<span> 標籤組合在一塊兒,例如:

    <p class="body-text--semibold">
      <span>Thing 1</span>
      <span>Thing 2</span>
    </p>
    複製代碼
  • 對於每一個要展現樣式的單元格,只須要編寫一次類名

2. 爲什麼渲染函數有存在的意義

開始作以前我考慮了幾點:

2.1 硬編碼

我喜歡在適當的時候使用硬編碼,由於手工編寫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>
複製代碼

2.2 使用傳統的Vue模板

點擊查看Codeopen代碼。這一般是首選方案,但請注意:

第一列,咱們有:

  • 一個按原樣渲染的<h1> 標籤
  • 一個<p>標籤,它將一些帶有文本的子標籤<span>組合在一塊兒,每一個 span 標籤上都有一個類(可是在 p 標籤上沒有特殊的類)
  • 一個帶有類但沒有子節點的 <p> 標籤

這也就意味着咱們須要加不少的 v-ifv-if-else 去判斷,並且加着加着邏輯就會變得很混亂,我也不太喜歡在HTML中加這些邏輯判斷,這讓代碼很難閱讀。

基於這個緣由,我選擇了渲染函數。渲染函數是使用Javascript基於現有的邏輯規則有條件的添加子節點,對於我這種業務處理彷佛是完美的解決方案。

3. 數據模型

正如我以前提到的,我但願將數據存放在一個單獨的JSON文件中,這樣我之後只需改JSON文件無需修改HTML了。這裏是原始數據

文件中的每一個對象表明一行:

{
  "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 對象用來存儲類選擇器。其中 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">
  <!-- 第一列 -->
  <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>
  <!-- 第二列 -->
  <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>
  <!-- 第三列 -->
  <p class="body-text body-text--md body-text--semibold">Proxima Nova Bold and Regular, 20px</p>
  <!-- 第四列 -->
  <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>
複製代碼

4. 基礎設置

假設咱們有一個用來包裝表格容器的父組件 TypographyTable.vue,以及一個用來建立行和包含咱們渲染函數的子組件 TypographyRow.vue。而後我循環遍歷行,每行的數據經過 props 傳遞:

<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>  
    <!-- 循環遍歷將咱們的數據做爲props傳給每一行 -->
    <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訪問。這樣它既不會改變也不會成爲響應式數據。

5. 使用函數式組件

我將 TypographyRow.vue 組件改形成一個函數式組件。這意味函數式組件無狀態 (沒有響應式數據),也沒有實例 (沒有 this 上下文),而且沒法訪問任何Vue生命週期方法。

一個空的函數式組件大致上是這個樣子:

// No <template>
<script> export default { name: "TypographyRow", functional: true, // 這個屬性表示它是一個函數式組件 props: { rowData: { // 行數據 type: Object } }, render(createElement, { props }) { // 渲染行的邏輯代碼在這裏 } } </script>
複製代碼

其中 render 方法接受一個 context 上下文參數,咱們可經過解構獲取單獨的 props 屬性。

第一個參數 createElement,咱們都知道是一個Vue提供的用來建立虛擬DOM的函數。按照國際慣例,我把 createElement 縮寫成 h,關於爲何這麼作,你能夠讀一下Sarsh's post的文章。

h 有三個參數:

  1. 一個html標籤(例如:div
  2. 一個具備模板屬性的數據對象(例如:{class: 'something'}
  3. 文本字符串(若是咱們只是添加文本)或者是使用 h 建立的子節點
render(h, { props }) {
  return h("div", { class: "example-class" }, "Here's my example text")
}
複製代碼

好了,簡單歸納下咱們目前爲止作了哪些事情吧:

  • 一個用於呈現數據可視化的JSON文件
  • 一個我正在導入完整數據的常規Vue組件和
  • 一個用於展現每一行數據的函數式組件雛形

要建立每一行,須要將JSON文件中的數據對象做爲參數傳給h。這能夠一次完成但這樣作會涉及到不少的條件邏輯判斷,而且使人困惑。因此接下來,我決定分兩部分來作:

  1. 將數據格式化,即使於觀察的格式
  2. 而後再渲染轉換後的數據

6. 轉換普通數據

我但願個人數據結構跟 h 要求的參數匹配,因此轉換以前我說下我想要的結構:

// 一個單元格
{
  tag: "", // 當前級別的HTML標籤
  cellClass: "", // 當前級別的類名,若是類名不存在則爲空
  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;
}
複製代碼

讓咱們再回頭看看咱們的設計圖:

第一列有樣式變化,其他的列彷佛遵循相同的模式,因此讓咱們先從其他列開始。

一樣的,我想要的每一個單元的格式是:

{
  tag: "",
  cellClass: "", 
  text: "", 
  children: []
}
複製代碼

這有點像樹形結構,由於有的單元格下面還有子節點。讓咱們使用兩個函數來建立單元格:

  • createNode 將每一個所需的屬性做爲參數
  • createCell 包裝 createNode,這樣咱們就能夠檢查咱們傳入的文本是不是一個數組。若是是,咱們構建一個子節點數組
// 每一個單元格的模型
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等)。

讓咱們建立一個輔助函數來將這些數據解析成咱們能夠用於文本參數的格式:

// 參數分別是標籤名和類名
function displayClasses(element, classes) {
  // 若是沒有類,就返回基本標籤(適用於標題)
  return getClasses(classes) ? getClasses(classes) : element;
}

// 若是 `classes` 是一個字符串,返回一個類
// 若是是一個數組,返回多個類
// 若是是null, 返回自己
// 例如: "body-text body-text--sm" 或
// ["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;
}
複製代碼

7. 轉換演示數據

還剩下第一列的示例樣式沒處理了。這一列須要展現效果,因此咱們須要爲其賦予新的標籤和類名,不能用上面的方法了。

<p class="body-text body-text--md body-text--semibold">
複製代碼

讓咱們從新建立一個專門處理這塊邏輯的方法吧:

function createDemoCellData(data) {
  let children;
  const classes = getClasses(data.classes);
  // 處理多個類的狀況
  if (Array.isArray(classes)) {
    children = classes.map(child =>
      // 咱們可使用`data.text`是由於元數據中每一個對象都有一個 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);
}
複製代碼

如今咱們有一個標準化格式的行數據了,咱們能夠將其傳遞給渲染函數了。

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
}
複製代碼

8. 渲染數據

下面這纔是咱們渲染數據方式的最終邏輯代碼:

// 訪問`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, // 設置類名爲 `group`,由於它下面有多個子節點
          [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);
}
複製代碼

咱們獲得了咱們最終想要的產品! 源碼看這裏

9. 總結

值得指出的是,這種方法表明瞭解決相對簡單問題的一種實驗性方法。我相信不少人會爭辯說這個解決方案是不必的複雜的以及過分設計的。我可能會認可這一點。

然而,儘管前期花費了不少時間,但數據如今已徹底跟頁面解耦。如今,若是個人設計團隊再想添加或刪除行,我沒必要深刻研究那一坨又一坨凌亂的HTML — 我只要更新JSON文件中的幾個屬性就好了。

這值得嗎?就像編程中的其餘事情同樣,它取決於最佳實踐,一圖勝千言吧:

  • 顧客:你能給我加點鹽嗎?
  • (20分鐘後)
  • 顧客:都過二十分鐘了!
  • 廚師:我說過了——我知道!我正在開發一個能夠給你加任意調味品的系統。從長遠來看這會節省更多時間!

圖片來源: xkcd.com/974

也許這是一個答案,我很樂意聽到你全部的(建設性)的想法和建議。或者你是否嘗試過其餘方式完成相似的任務。

最後,下面是我維護的一個Q羣,歡迎掃碼進羣哦,讓咱們一塊兒交流學習吧。也能夠加我我的微信:G911214255 ,備註 掘金 便可。

Q1769617251
相關文章
相關標籤/搜索