(译)使用渲染函数构建一个设计系统的排版布局
栏目: JavaScript · 发布时间: 6年前
内容简介:谈谈你对Vue函数式组件的理解?自己先想一分钟译者注:英语和文笔有限,不对之处欢迎留言斧正!原文地址:t.cn/AipuKWqv
谈谈你对Vue函数式组件的理解?
自己先想一分钟
译者注:英语和文笔有限,不对之处欢迎留言斧正!原文地址:t.cn/AipuKWqv
这篇文章介绍了我是如何使用Vue渲染函数为设计系统构建网格布局的。这里是相关的demo和代码。我之所以选择使用渲染函数是因为它比常规的Vue模板语法在可控性方面表现的更加强大。然而关于渲染函数的介绍,我在网上并未找到太多的文章,这让我很惊讶。我希望这篇文章能弥补这方面的不足,并且提供一个有用和实用的使用Vue渲染函数的用例。
过去我总是觉得渲染函数太过于抽象,甚至有点不合时宜。虽然框架的其余部分强调简单性和关注点分离,但渲染函数是一种奇怪的,通常难以阅读的HTML和JavaScript混合。
例如,下面的代码:
<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的关键因素。这很遗憾,因为渲染函数和函数式组件能够提供一些非常酷,且功能强大的东西。好吧,让我们看下它是怎样帮助我解决实际问题的。
Tips: 在新标签页中打开示例代码对照本文阅读效果更佳哦~
1. 定义一个设计标准
我的团队希望在我们的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~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-if 和 v-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 有三个参数:
div
{class: 'something'}
h
render(h, { props }) {
return h("div", { class: "example-class" }, "Here's my example text")
}
复制代码
好了,简单概括下我们目前为止做了哪些事情吧:
- 一个用于呈现数据可视化的JSON文件
- 一个我正在导入完整数据的常规Vue组件和
- 一个用于展示每一行数据的函数式组件雏形
要创建每一行,需要将JSON文件中的数据对象作为参数传给 h 。这可以一次完成但这样做会涉及到很多的条件逻辑判断,并且令人困惑。所以接下来,我决定分两部分来做:
- 将数据格式化,即便于观察的格式
- 然后再渲染转换后的数据
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;
}
复制代码
我们将 properties 和 usage 作为参数传递给第三和第四列。但是第二列有点不同;画 ????? 那里,我们将展示存储在数据文件中的类名,如:
"classes": {
"base": "body-text body-text--lg",
"variants": ["body-text--bold", "body-text--regular"]
},
复制代码
另外,请记住标题没有类,因此我们要显示这些行的标题标记名称(例如 h1 , h2 等)。
让我们创建一个辅助函数来将这些数据解析成我们可以用于文本参数的格式:
// 参数分别是标签名和类名
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 ,备注 掘金 即可。
以上所述就是小编给大家介绍的《(译)使用渲染函数构建一个设计系统的排版布局》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- Bootstrap学习之三:使用排版
- R语言图表排版之一页多图
- 以 Markdown 撰写文稿,以 LaTeX 排版
- Android9编程七:ConstraintLayout 排版
- 中英文排版规范化 API
- [译]《Smashing》: 用 CSS 形状打造高级排版
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。