Dieser Beitrag beschreibt, wie ich mit Vue-Renderfunktionen ein Typografie-Gitter für ein Designsystem erstellt habe. Hier ist die Demo und der Code. Ich habe Renderfunktionen verwendet, da sie es ermöglichen, HTML mit einem höheren Kontrollgrad als normale Vue-Vorlagen zu erstellen. Überraschenderweise konnte ich bei meiner Websuche nur sehr wenige reale, nicht-tutorial-basierte Anwendungen dafür finden. Ich hoffe, dieser Beitrag füllt diese Lücke und bietet einen hilfreichen und praktischen Anwendungsfall für Vue-Renderfunktionen.
Ich fand Renderfunktionen schon immer etwas untypisch für Vue. Während der Rest des Frameworks Einfachheit und Trennung von Belangen betont, sind Renderfunktionen eine seltsame und oft schwer lesbare Mischung aus HTML und JavaScript.
Um zum Beispiel anzuzeigen
<div class="container">
<p class="my-awesome-class">Some cool text</p>
</div>
…benötigt man
render(createElement) {
return createElement("div", { class: "container" }, [
createElement("p", { class: "my-awesome-class" }, "Some cool text")
])
}
Ich vermute, dass diese Syntax einige Leute abschreckt, da die Benutzerfreundlichkeit einer der Hauptgründe ist, warum man sich überhaupt für Vue entscheidet. Das ist schade, denn Renderfunktionen und funktionale Komponenten sind zu einigen ziemlich coolen, leistungsstarken Dingen fähig. Um ihren Wert zu demonstrieren, zeige ich, wie sie ein echtes Geschäftsproblem für mich gelöst haben.
Kurzer Haftungsausschluss: Es wird sehr hilfreich sein, die Demo in einem anderen Tab geöffnet zu haben, um sie während des gesamten Beitrags nachschlagen zu können.
Festlegen von Kriterien für ein Designsystem
Mein Team wollte eine Seite in unserem VuePress-basierten Designsystem aufnehmen, die verschiedene Typografie-Optionen zeigt. Dies ist Teil eines Mockups, das ich von unserem Designer erhalten habe.

Und hier ist ein Beispiel für das entsprechende 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;
}
Überschriften werden mit Tag-Namen angesprochen. Andere Elemente verwenden Klassennamen, und es gibt separate Klassen für Gewicht und Größe.
Bevor ich Code schrieb, habe ich einige Grundregeln aufgestellt
- Da dies eigentlich eine Datenvisualisierung ist, sollten die Daten in einer separaten Datei gespeichert werden.
- Überschriften sollten semantische Überschriften-Tags (z. B.
<h1>,<h2>usw.) verwenden, anstatt sich auf eine Klasse verlassen zu müssen. - Textinhalte sollten Absatz-Tags (
<p>) mit dem Klassennamen verwenden (z. B.<p class="body-text--lg">). - Inhaltstypen, die Variationen aufweisen, sollten gruppiert werden, indem sie in das Wurzelelement des Absatzes oder das entsprechende Wurzelelement ohne Styling-Klasse eingehüllt werden. Kinder sollten mit
<span>und dem Klassennamen eingehüllt werden.
<p>
<span class="body-text--lg">Thing 1</span>
<span class="body-text--lg">Thing 2</span>
</p>
- Alle Inhalte, die kein spezielles Styling demonstrieren, sollten ein Absatz-Tag mit dem richtigen Klassennamen und
<span>für alle Kindknoten verwenden.
<p class="body-text--semibold">
<span>Thing 1</span>
<span>Thing 2</span>
</p>
- Klassennamen müssen nur einmal für jede Zelle geschrieben werden, die Styling demonstriert.
Warum Renderfunktionen sinnvoll sind
Ich habe vor Beginn ein paar Optionen in Betracht gezogen
Hardcoding
Ich mag Hardcoding, wenn es angebracht ist, aber mein HTML von Hand zu schreiben hätte bedeutet, verschiedene Kombinationen des Markups zu tippen, was unangenehm und repetitiv erschien. Es bedeutete auch, dass die Daten nicht in einer separaten Datei gespeichert werden konnten, daher schied dieser Ansatz aus.
Hier ist, was ich meine
<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>
Verwendung einer traditionellen Vue-Vorlage
Dies wäre normalerweise die Standardoption. Betrachten Sie jedoch Folgendes
Siehe den Pen
Unterschiedliche Stilbeispiele von Salomone Baquis (@soluhmin)
auf CodePen.
In der ersten Spalte haben wir
– Ein <h1>-Tag, das unverändert gerendert wird.
– Ein <p>-Tag, das einige <span>-Kinder mit Text gruppiert, jedes mit einer Klasse (aber ohne spezielle Klasse auf dem <p>-Tag).
– Ein <p>-Tag mit einer Klasse und ohne Kinder.
Das Ergebnis hätte viele Instanzen von v-if und v-if-else bedeutet, von denen ich wusste, dass sie schnell verwirrend werden würden. Mir gefiel auch die ganze bedingte Logik innerhalb des Markups nicht.
Aus diesen Gründen habe ich Renderfunktionen gewählt. Renderfunktionen verwenden JavaScript, um bedingt Kindknoten basierend auf allen definierten Kriterien zu erstellen, was für diese Situation perfekt erschien.
Datenmodell
Wie ich bereits erwähnte, möchte ich die Typografie-Daten in einer separaten JSON-Datei aufbewahren, damit ich Änderungen später leicht vornehmen kann, ohne das Markup zu berühren. Hier sind die Rohdaten.
Jedes Objekt in der Datei repräsentiert eine andere Zeile.
{
"text": "Heading 1",
"element": "h1", // Root wrapping element.
"properties": "Balboa Light, 30px", // Third column text.
"usage": ["Product title (once on a page)", "Illustration headline"] // Fourth column text. Each item is a child node.
}
Das obige Objekt rendert das folgende 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>
Betrachten wir ein komplexeres Beispiel. Arrays repräsentieren Gruppen von Kindern. Ein classes-Objekt kann Klassen speichern. Die base-Eigenschaft enthält Klassen, die für jeden Knoten in der Zellengruppierung gemeinsam sind. Jede Klasse in variants wird auf ein anderes Element in der Gruppierung angewendet.
{
"text": "Body Text - Large",
"element": "p",
"classes": {
"base": "body-text body-text--lg", // Applied to every child node
"variants": ["body-text--bold", "body-text--regular"] // Looped through, one class applied to each example. Each item in the array is its own node.
},
"properties": "Proxima Nova Bold and Regular, 20px",
"usage": ["Large button title", "Form label", "Large modal text"]
}
Hier ist, wie das gerendert wird
<div class="row">
<!-- Column 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>
<!-- Column 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>
<!-- Column 3 -->
<p class="body-text body-text--md body-text--semibold">Proxima Nova Bold and Regular, 20px</p>
<!-- Column 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>
Die Grundeinrichtung
Wir haben eine Elternkomponente, TypographyTable.vue, die das Markup für das Wrapper-Tabellenelement enthält, und eine Kindkomponente, TypographyRow.vue, die eine Zeile erstellt und unsere Renderfunktion enthält.
Ich iteriere durch die Zeilenkomponente und übergebe die Zeilendaten als Props.
<template>
<section>
<!-- Headers hardcoded for simplicity -->
<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>
<!-- Loop and pass our data as props to each row -->
<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 {
// Our data is static so we don't need to make it reactive
typographyData: TypographyData,
name: "TypographyTable",
components: {
TypographyRow
}
};
</script>
Eine nette Sache, auf die man hinweisen kann: Die Typografie-Daten können eine Eigenschaft auf der Vue-Instanz sein und mit $options.typographyData aufgerufen werden, da sie sich nicht ändert und nicht reaktiv sein muss. (Hutspitze an Anton Kosykh.)
Erstellen einer funktionalen Komponente
Die TypographyRow-Komponente, die Daten übergibt, ist eine funktionale Komponente. Funktionale Komponenten sind zustandslos und instanzlos, was bedeutet, dass sie kein this haben und keinen Zugriff auf Vue-Lebenszyklusmethoden haben.
Die leere Startkomponente sieht so aus
// No <template>
<script>
export default {
name: "TypographyRow",
functional: true, // This property makes the component functional
props: {
rowData: { // A prop with row data
type: Object
}
},
render(createElement, { props }) {
// Markup gets rendered here
}
}
</script>
Die render-Methode nimmt ein context-Argument entgegen, das eine props-Eigenschaft hat, die de-strukturiert und als zweites Argument verwendet wird.
Das erste Argument ist createElement, eine Funktion, die Vue mitteilt, welche Knoten erstellt werden sollen. Der Kürze und Konvention halber werde ich createElement als h abkürzen. Warum ich das tue, können Sie in Sarahs Beitrag nachlesen.
h nimmt drei Argumente entgegen
- Ein HTML-Tag (z. B.
div) - Ein Datenobjekt mit Template-Attributen (z. B.
{ class: 'something'}) - Textzeichenfolgen (wenn wir nur Text hinzufügen) oder Kindknoten, die mit
herstellt wurden
render(h, { props }) {
return h("div", { class: "example-class" }, "Here's my example text")
}
OK, um zusammenzufassen, wo wir bisher stehen: Wir haben Folgendes behandelt:
- eine Datei mit den Daten, die in meiner Visualisierung verwendet werden sollen;
- eine reguläre Vue-Komponente, in der ich die vollständige Datendatei importiere; und
- den Anfang einer funktionalen Komponente, die jede Zeile anzeigen wird.
Um jede Zeile zu erstellen, müssen die Daten aus der JSON-Datei als Argumente für h übergeben werden. Dies könnte alles auf einmal geschehen, aber das beinhaltet viel bedingte Logik und ist verwirrend.
Stattdessen habe ich beschlossen, es in zwei Teilen zu machen
- Die Daten in ein vorhersagbares Format transformieren.
- Die transformierten Daten rendern.
Transformation der gemeinsamen Daten
Ich wollte meine Daten in einem Format haben, das den Argumenten für h entspricht, aber bevor ich das tat, schrieb ich auf, wie ich mir die Struktur vorstellte
// One cell
{
tag: "", // HTML tag of current level
cellClass: "", // Class of current level, null if no class exists for that level
text: "", // Text to be displayed
children: [] // Children each follow this data model, empty array if no child nodes
}
Jedes Objekt repräsentiert eine Zelle, wobei vier Zellen eine Zeile bilden (ein Array).
// One row
[ { cell1 }, { cell2 }, { cell3 }, { cell4 } ]
Der Einstiegspunkt wäre eine Funktion wie
function createRow(data) { // Pass in the full row data and construct each cell
let { text, element, classes = null, properties, usage } = data;
let row = [];
row[0] = createCellData(data) // Transform our data using some shared function
row[1] = createCellData(data)
row[2] = createCellData(data)
row[3] = createCellData(data)
return row;
}
Werfen wir noch einmal einen Blick auf unser Mockup.

Die erste Spalte hat Stilvariationen, aber die anderen scheinen demselben Muster zu folgen, also beginnen wir mit diesen.
Wiederum ist das gewünschte Modell für jede Zelle
{
tag: "",
cellClass: "",
text: "",
children: []
}
Dies gibt uns eine baumartige Struktur für jede Zelle, da einige Zellen Gruppen von Kindern haben. Verwenden wir zwei Funktionen, um die Zellen zu erstellen.
createNodenimmt jede unserer gewünschten Eigenschaften als Argumente entgegen.createCellwickeltcreateNodeein, damit wir prüfen können, ob der Text, den wir übergeben, ein Array ist. Wenn ja, bauen wir ein Array von Kindknoten auf.
// Model for each cell
function createCellData(tag, text) {
let children;
// Base classes that get applied to every root cell tag
const nodeClass = "body-text body-text--md body-text--semibold";
// If the text that we're passing in as an array, create child elements that are wrapped in spans.
if (Array.isArray(text)) {
children = text.map(child => createNode("span", null, child, children));
}
return createNode(tag, nodeClass, text, children);
}
// Model for each node
function createNode(tag, nodeClass, text, children = []) {
return {
tag: tag,
cellClass: nodeClass,
text: children.length ? null : text,
children: children
};
}
Jetzt können wir so etwas tun
function createRow(data) {
let { text, element, classes = null, properties, usage } = data;
let row = [];
row[0] = ""
row[1] = createCellData("p", ?????) // Need to pass in class names as text
row[2] = createCellData("p", properties) // Third column
row[3] = createCellData("p", usage) // Fourth column
return row;
}
Wir übergeben properties und usage an die dritte und vierte Spalte als Textargumente. Die zweite Spalte ist jedoch etwas anders; dort zeigen wir die Klassennamen an, die in der Datendatei wie folgt gespeichert sind
"classes": {
"base": "body-text body-text--lg",
"variants": ["body-text--bold", "body-text--regular"]
},
Außerdem, denken Sie daran, dass Überschriften *keine* Klassen haben, also wollen wir für diese Zeilen die Überschriften-Tag-Namen anzeigen (z. B. h1, h2 usw.).
Lassen Sie uns einige Hilfsfunktionen erstellen, um diese Daten in ein Format zu parsen, das wir für unser Textargument verwenden können.
// Pass in the base tag and class names as arguments
function displayClasses(element, classes) {
// If there are no classes, return the base tag (appropriate for headings)
return getClasses(classes) ? getClasses(classes) : element;
}
// Return the node class as a string (if there's one class), an array (if there are multiple classes), or null (if there are none.)
// Ex. "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) {
// Concatenate each variant with the base classes
return variants.map(variant => base.concat(`${variant}`));
}
return base;
}
return classes;
}
Jetzt können wir das tun
function createRow(data) {
let { text, element, classes = null, properties, usage } = data;
let row = [];
row[0] = ""
row[1] = createCellData("p", displayClasses(element, classes)) // Second column
row[2] = createCellData("p", properties) // Third column
row[3] = createCellData("p", usage) // Fourth column
return row;
}
Transformation der Demo-Daten
Damit bleibt die erste Spalte, die die Stile demonstriert. Diese Spalte ist anders, weil wir jeder Zelle neue Tags und Klassen zuweisen, anstatt die Klassen-Kombination zu verwenden, die vom Rest der Spalten verwendet wird.
<p class="body-text body-text--md body-text--semibold">
Anstatt zu versuchen, dies in createCellData oder createNodeData zu tun, erstellen wir eine weitere Funktion, die über diesen grundlegenden Transformationsfunktionen liegt und einige der neuen Logik verarbeitet.
function createDemoCellData(data) {
let children;
const classes = getClasses(data.classes);
// In cases where we're showing off multiple classes, we need to create children and apply each class to each child.
if (Array.isArray(classes)) {
children = classes.map(child =>
// We can use "data.text" since each node in a cell grouping has the same text
createNode("span", child, data.text, children)
);
}
// Handle cases where we only have one class
if (typeof classes === "string") {
return createNode("p", classes, data.text, children);
}
// Handle cases where we have no classes (ie. headings)
return createNode(data.element, null, data.text, children);
}
Nun haben wir die Zeilendaten in einem normalisierten Format, das wir unserer Renderfunktion übergeben können
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
}
Rendern der Daten
Hier ist, wie wir die Daten tatsächlich rendern, um sie anzuzeigen
// Access our data in the "props" object
const rowData = props.rowData;
// Pass it into our entry transformation function
const row = createRow(rowData);
// Create a root "div" node and handle each cell
return h("div", { class: "row" }, row.map(cell => renderCells(cell)));
// Traverse cell values
function renderCells(data) {
// Handle cells with multiple child nodes
if (data.children.length) {
return renderCell(
data.tag, // Use the base cell tag
{ // Attributes in here
class: {
group: true, // Add a class of "group" since there are multiple nodes
[data.cellClass]: data.cellClass // If the cell class isn't null, apply it to the node
}
},
// The node content
data.children.map(child => {
return renderCell(
child.tag,
{ class: child.cellClass },
child.text
);
})
);
}
// If there are no children, render the base cell
return renderCell(data.tag, { class: data.cellClass }, data.text);
}
// A wrapper function around "h" to improve readability
function renderCell(tag, classArgs, text) {
return h(tag, classArgs, text);
}
Und wir erhalten unser Endprodukt! Hier ist erneut der Quellcode.
Zusammenfassung
Es ist erwähnenswert, dass dieser Ansatz eine experimentelle Methode zur Lösung eines relativ trivialen Problems darstellt. Ich bin sicher, viele Leute werden argumentieren, dass diese Lösung unnötig kompliziert und überkonstruiert ist. Ich würde wahrscheinlich zustimmen.
Trotz der anfänglichen Kosten sind die Daten nun vollständig von der Präsentation getrennt. Wenn mein Designteam nun Zeilen hinzufügt oder entfernt, muss ich nicht mehr in unübersichtlichem HTML wühlen – ich aktualisiere nur ein paar Eigenschaften in der JSON-Datei.
Lohnt es sich? Wie bei allem in der Programmierung, denke ich, es hängt davon ab. Ich sage nur, dieser Comic-Strip war mir im Hinterkopf, als ich daran arbeitete

Vielleicht ist das eine Antwort. Ich würde mich sehr über all eure (konstruktiven) Gedanken und Vorschläge freuen, oder ob ihr andere Wege ausprobiert habt, um eine ähnliche Aufgabe zu bewältigen.
Tolle Ausarbeitung – ich habe mich immer gefragt, warum man Renderfunktionen verwenden sollte, aber das erklärt es sehr gut.
Übrigens gibt es einen Tippfehler – ein fehlendes
"bei der KlasseDanke! Das ist behoben.
Guter Artikel! Ich denke, du hast die Dinge mit dem Satz "Lohnt es sich? Wie bei allem in der Programmierung, denke ich, es hängt davon ab." ziemlich gut zusammengefasst.
Schöne Datenhandhabung.
Der Hauptgrund, warum ich Renderfunktionen im Moment vermeide, ist, dass sich die Syntax mit Vue 3 ändern wird.
Toller Artikel.
Als Alternative könnten Sie es relativ einfach mit
v-if,v-forund<component :is="element">implementieren. Es ist vielleicht nicht ganz so "sauber", aber schauen Sie sich das an: https://codepen.io/anon/pen/YbmozpGute Arbeit! Ich denke definitiv, dass dieser Ansatz seine Vorteile hat (er ist sicherlich weniger umständlich).
Wie ich im Artikel darlege, habe ich ursprünglich davon abgesehen, dies zu tun, weil ich finde, dass diese Art von enger Kopplung von Markup und Logik verwirrend wird, besonders wenn sich die Anforderungen ändern. Ich habe an einer Produktionsanwendung gearbeitet, bei der Leute versuchten, alle möglichen bedingten Anweisungen in das HTML einzufügen, und das wurde ziemlich schnell unübersichtlich.
Der JS/funktionale Ansatz erfordert wahrscheinlich genauso viel Wartungsaufwand, aber ich ziehe es persönlich vor, Dinge getrennt zu halten (das erklärt vielleicht auch, warum ich mich nie in React verliebt habe).
Danke, dass du das zusammengestellt hast! Es ist toll, eine alternative Implementierung zu sehen.