Reactive UI’s with VanillaJS – Teil 1: Rein funktionaler Stil

Avatar of Brandon Smith
Brandon Smith am

DigitalOcean bietet Cloud-Produkte für jede Phase Ihrer Reise. Starten Sie mit 200 $ kostenlosem Guthaben!

Letzten Monat schrieb Chris Coyier einen Beitrag, in dem er die Frage untersuchte: „Wann braucht ein Projekt React?“ Mit anderen Worten, wann überwiegen die Vorteile der Verwendung von React (als Stellvertreter für datengesteuerte Web-Frameworks im Allgemeinen) gegenüber serverseitigen Vorlagen und jQuery, die zusätzliche Komplexität bei der Einrichtung der erforderlichen Werkzeuge, des Build-Prozesses, der Abhängigkeiten usw. aufweisen? Eine Woche später schrieb Sacha Greif einen Gegenbeitrag, in dem er argumentierte, warum man solche Frameworks immer für jede Art von Webprojekt verwenden sollte. Seine Argumente umfassten Zukunftssicherheit, vereinfachten Workflow von Projekt zu Projekt (eine einzige Architektur; keine Notwendigkeit, mehrere Arten von Projektstrukturen zu verfolgen) und verbesserte Benutzererfahrung durch clientseitiges Neu-Rendering, auch wenn sich der Inhalt nicht sehr oft ändert.

In diesem Beitragspaar vertiefe ich mich in einen Mittelweg: Das Schreiben von UI's im reaktiven Stil in reinem JavaScript – keine Frameworks, keine Präprozessoren.

Artikelserie

  1. Rein funktionaler Stil (Du bist hier!)
  2. Klassenbasierte Komponenten

Es gibt zwei sehr unterschiedliche Möglichkeiten, React-Komponenten zu schreiben.

  1. Man kann sie als Klassen schreiben. Zustandsbehaftete Objekte mit Lifecycle-Hooks und internen Daten.
  2. Oder man kann sie als Funktionen schreiben. Nur ein Stück HTML, das basierend auf übergebenen Parametern konstruiert und aktualisiert wird.

Ersteres ist oft nützlicher für große, komplexe Anwendungen mit vielen beweglichen Teilen, während letzteres eine elegantere Möglichkeit ist, Informationen anzuzeigen, wenn man nicht viel dynamischen Zustand hat. Wenn Sie jemals eine Vorlagen-Engine wie Handlebars oder Swig verwendet haben, ähnelt deren Syntax dem funktionsbasierten React-Code.

In diesem Beitragspaar ist unser Zielanwendungsfall Websites, die ansonsten statisch sein könnten, aber von JavaScript-basiertem Rendering profitieren würden, wenn nicht der Overhead für die Einrichtung eines Frameworks wie React wäre. Blogs, Foren usw. Daher konzentriert sich dieser erste Beitrag auf den funktionalen Ansatz zum Schreiben einer komponentenbasierte UI, da dieser für diesen Szenariotyp praktischer ist. Der zweite Beitrag wird eher ein Experiment sein; ich werde die Grenzen ausreizen, wie weit wir Dinge ohne Framework bringen können, und versuchen, das klassenbasierte Komponentenmuster von React mit nur Vanilla JavaScript so genau wie möglich zu replizieren, wahrscheinlich auf Kosten einiger Praktikabilität.

Über funktionale Programmierung

Funktionale Programmierung hat in den letzten Jahren enorm an Popularität gewonnen, hauptsächlich angetrieben durch Clojure, Python und React. Eine vollständige Erklärung der funktionalen Programmierung liegt außerhalb des Rahmens dieses Beitrags, aber der Teil, der für uns jetzt relevant ist, ist das Konzept von Werten, die Funktionen anderer Werte sind.

Nehmen wir an, Ihr Code muss das Konzept eines Rechtecks darstellen. Ein Rechteck hat Breite und Höhe, aber auch Fläche, Umfang und andere Attribute. Zuerst könnte man denken, ein Rechteck mit dem folgenden Objekt darzustellen

var rectangle = {
  width: 2,
  height: 3,
  area: 6,
  perimeter: 10
};

Aber es würde sich schnell herausstellen, dass es ein Problem gibt. Was passiert, wenn sich die Breite ändert? Jetzt müssen wir auch Fläche und Umfang ändern, sonst wären sie falsch. Es ist möglich, widersprüchliche Werte zu haben, bei denen man nicht einfach einen ändern kann, ohne die Möglichkeit zu haben, etwas anderes zu aktualisieren. Dies wird als mehrere Wahrheitsquellen bezeichnet.

Im Rechteckbeispiel ist die Lösung im Stil der funktionalen Programmierung, area und perimeter in Funktionen eines Rechtecks zu verwandeln

var rectangle = {
  width: 2,
  height: 3
};

function area(rect) {
  return rect.width * rect.height;
}

function perimeter(rect) {
  return rect.width * 2 + rect.height * 2;
}

area(rectangle); // = 6
perimeter(rectangle); // = 10

Auf diese Weise müssen wir nichts anderes manuell ändern, wenn sich width oder height ändern. Die area und perimeter sind einfach korrekt. Dies wird als einzige Wahrheitsquelle bezeichnet.

Diese Idee ist wirkungsvoll, wenn Sie das Rechteck durch die Daten ersetzen, die Ihre Anwendung möglicherweise hat, und die Fläche und den Umfang durch HTML. Wenn Sie Ihr HTML zu einer Funktion Ihrer Daten machen können, dann müssen Sie sich nur noch um die Änderung der Daten kümmern – nicht um das DOM –, und die Art und Weise, wie es auf der Seite gerendert wird, wird implizit sein.

UI-Komponenten als Funktionen

Wir möchten unser HTML zu einer Funktion unserer Daten machen. Nehmen wir das Beispiel eines Blogbeitrags

var blogPost = {
  author: 'Brandon Smith',
  title: 'A CSS Trick',
  body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.'
};

function PostPage(postData) {
  return  '<div class="page">' +
            '<div class="header">' + 
              'Home' +
              'About' +
              'Contact' +
            '</div>' + 
            '<div class="post">' + 
              '<h1>' + postData.title + '</h1>' + 
              '<h3>By ' + postData.author + '</h3>' +
              '<p>' + postData.body + '</p>' +
            '</div>' +
          '</div>';
}

document.querySelector('body').innerHTML = PostPage(blogPost);

Okay. Wir haben eine Funktion eines Beitrags-Objekts erstellt, die einen HTML-String zurückgibt, der unseren Blogbeitrag rendert. Es ist jedoch noch nicht wirklich „komponentisiert“. Es ist alles eine große Sache. Was ist, wenn wir auch alle unsere Blogbeiträge nacheinander auf der Homepage rendern wollen? Was ist, wenn wir diese Kopfzeile auf verschiedenen Seiten wiederverwenden wollen? Glücklicherweise ist es sehr einfach, Funktionen aus anderen Funktionen aufzubauen. Dies wird als Komponieren von Funktionen bezeichnet

var blogPost = {
  author: 'Brandon Smith',
  title: 'A CSS Trick',
  body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.'
};

function Header() {
  return '<div class="header">' + 
            'Home' +
            'About' +
            'Contact' +
          '</div>';
}

function BlogPost(postData) {
  return '<div class="post">' + 
            '<h1>' + postData.title + '</h1>' + 
            '<h3>By ' + postData.author + '</h3>' +
            '<p>' + postData.body + '</p>' +
          '</div>';
}

function PostPage(postData) {
  return  '<div class="page">' +
            Header() +
            BlogPost(postData) +
          '</div>';
}

function HomePage() {
  return '<div class="page">' +
            Header() +
            '<h1>Welcome to my blog!</h1>' +
            '<p>It\'s about lorem ipsum dolor sit amet, consectetur ad...</p>' +
          '</div>';
}

document.querySelector('body').innerHTML = PostPage(blogPost);

Das ist viel schöner. Wir mussten die Kopfzeile nicht für die Homepage duplizieren; wir haben eine einzige Wahrheitsquelle für diesen HTML-Code. Wenn wir einen Beitrag in einem anderen Kontext anzeigen möchten, könnten wir dies einfach tun.

Schönere Syntax mit Template-Literalen

Okay, aber all diese Pluszeichen sind schrecklich. Sie sind mühsam zu tippen und machen es schwerer zu lesen, was passiert. Es muss einen besseren Weg geben, oder? Nun, die Leute vom W3C sind Ihnen weit voraus. Sie haben Template-Literale erfunden – die, obwohl sie noch relativ neu sind, ziemlich gute Browserunterstützung zu diesem Zeitpunkt haben. Wickeln Sie Ihre Zeichenkette einfach in Backticks anstelle von Anführungszeichen, und sie erhält ein paar zusätzliche Superkräfte.

Die erste Superkraft ist die Fähigkeit, sich über mehrere Zeilen zu erstrecken. Unser BlogPost-Komponente kann also werden

// ...

function BlogPost(postData) {
  return `<div class="post">
            <h1>` + postData.title + `</h1>
            <h3>By ` + postData.author + `</h3>
            <p>` + postData.body + `</p>
          </div>`;
}

// ...

Das ist schön. Aber die andere Kraft ist noch schöner: Variablensubstitution. Variablen (oder beliebige JavaScript-Ausdrücke, einschließlich Funktionsaufrufe!) können direkt in die Zeichenkette eingefügt werden, wenn sie in ${ } eingeschlossen sind.

// ...

function BlogPost(postData) {
  return `<div class="post">
            <h1>${postData.title}</h1>
            <h3>By ${postData.author}</h3>
            <p>${postData.body}</p>
          </div>`;
}

// ...

Viel besser. Es sieht jetzt fast aus wie JSX. Sehen wir uns unser vollständiges Beispiel noch einmal an, mit Template-Literalen

var blogPost = {
  author: 'Brandon Smith',
  title: 'A CSS Trick',
  body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.'
};

function Header() {
  return `<div class="header">
            Home
            About
            Contact
          </div>`;
}

function BlogPost(postData) {
  return `<div class="post">
            <h1>${postData.title}</h1>
            <h3>By ${postData.author}</h3>
            <p>${postData.body}</p>
          </div>`;
}

function PostPage(postData) {
  return  `<div class="page">
            ${Header()}
            ${BlogPost(postData)}
          </div>`;
}

function HomePage() {
  return `<div class="page">
            ${Header()}
            <h1>Welcome to my blog!</h1>
            <p>It's about lorem ipsum dolor sit amet, consectetur ad...</p>
          </div>`;
}

document.querySelector('body').innerHTML = PostPage(blogPost);

Mehr als nur Lücken füllen

Wir können also Variablen und sogar andere Komponenten über Funktionen einfüllen, aber manchmal ist eine komplexere Rendering-Logik notwendig. Manchmal müssen wir über Daten iterieren oder auf eine Bedingung reagieren. Lassen Sie uns einige JavaScript-Sprachfeatures durchgehen, die es einfacher machen, komplexeres Rendering in einem funktionalen Stil durchzuführen.

Der ternäre Operator

Wir beginnen mit der einfachsten Logik: if-else. Natürlich könnten wir, da unsere UI-Komponenten nur Funktionen sind, ein tatsächliches if-else verwenden, wenn wir wollten. Sehen wir uns an, wie das aussehen würde

var blogPost = {
  isSponsored: true,
  author: 'Brandon Smith',
  title: 'A CSS Trick',
  body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.'
};

function BlogPost(postData) {
  var badgeElement;
  if(postData.isSponsored) {
    badgeElement = `<img src="badge.png">`;
  } else {
    badgeElement = '';
  }

  return `<div class="post">
            <h1>${postData.title} ${badgeElement}</h1>
            <h3>By ${postData.author}</h3>
            <p>${postData.body}</p>
          </div>`;
}

Das ist… nicht ideal. Es fügt eine ganze Menge Zeilen für etwas hinzu, das nicht so kompliziert ist, und es trennt einen Teil unseres Rendering-Codes von seinem Platz im Rest des HTML. Das liegt daran, dass eine klassische if-else-Anweisung entscheidet, welche Codezeilen ausgeführt werden, anstatt welchen Wert ausgewertet werden soll. Dies ist eine wichtige Unterscheidung zu verstehen. Sie können nur einen Ausdruck in ein Template-Literal einfügen, nicht eine Reihe von Anweisungen.

Der ternäre Operator ist wie ein if-else, aber für einen Ausdruck statt für eine Reihe von Anweisungen

var wantsToGo = true;
var response = wantsToGo ? 'Yes' : 'No'; // response = 'Yes'

wantsToGo = false;
response = wantsToGo ? 'Yes' : 'No'; // response = 'No'

Er hat die Form [Bedingung] ? [WertWennWahr] : [WertWennFalsch]. Der Blogbeitragsbeispiel oben wird also zu

var blogPost = {
  isSponsored: true,
  author: 'Brandon Smith',
  title: 'A CSS Trick',
  body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.'
};

function BlogPost(postData) {
  return `<div class="post">
            <h1>
              ${postData.title} ${postData.isSponsored ? '<img src="badge.png">' : ''}
            </h1>
            <h3>By ${postData.author}</h3>
            <p>${postData.body}</p>
          </div>`;
}

Viel besser.

Array.map()

Nun zu den Schleifen. Wann immer wir ein Array von Daten haben, das wir rendern wollen, müssen wir über diese Werte iterieren, um das entsprechende HTML zu generieren. Aber wenn wir eine for-Schleife verwenden würden, würden wir auf das gleiche Problem stoßen wie bei der if-else-Anweisung oben. Eine for-Schleife evaluiert keinen Wert, sie führt eine Reihe von Anweisungen auf eine bestimmte Weise aus. Glücklicherweise hat ES6 einige sehr hilfreiche Methoden zum Array-Typ hinzugefügt, die diesem spezifischen Bedarf dienen.

Array.map() ist eine Array-Methode, die ein einzelnes Argument nimmt, nämlich eine Callback-Funktion. Sie durchläuft das Array, auf dem sie aufgerufen wird (ähnlich wie Array.forEach()), und ruft den bereitgestellten Callback einmal für jedes Element auf, wobei das Array-Element als Argument übergeben wird. Der Unterschied zu Array.forEach() besteht darin, dass der Callback einen Wert zurückgeben soll – vermutlich einen, der auf dem entsprechenden Element im Array basiert –, und der gesamte Ausdruck gibt das neue Array aller von der Callback-Funktion zurückgegebenen Elemente zurück. Zum Beispiel

var myArray = [ 'zero', 'one', 'two', 'three' ];

// evaluates to [ 'ZERO', 'ONE', 'TWO', 'THREE' ]
var capitalizedArray = myArray.map(function(item) {
  return item.toUpperCase();
});

Sie können vielleicht erraten, warum das so nützlich für das ist, was wir tun. Früher haben wir das Konzept eines Wertes, der eine Funktion eines anderen Wertes ist, etabliert. Array.map() ermöglicht es uns, ein ganzes Array zu erhalten, bei dem jedes Element eine Funktion des entsprechenden Elements in einem anderen Array ist. Nehmen wir an, wir haben ein Array von Blogbeiträgen, die wir anzeigen möchten

function BlogPost(postData) {
  return `<div class="post">
            <h1>${postData.title}</h1>
            <h3>By ${postData.author}</h3>
            <p>${postData.body}</p>
          </div>`;
}

function BlogPostList(posts) {
  return `<div class="blog-post-list">
            ${posts.map(BlogPost).join('')}
          </div>`
}

var allPosts = [
  {
    author: 'Brandon Smith',
    title: 'A CSS Trick',
    body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.'
  },
  {
    author: 'Chris Coyier',
    title: 'Another CSS Trick',
    body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.'
  },
  {
    author: 'Bob Saget',
    title: 'A Home Video',
    body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.'
  }
]

document.querySelector('body').innerHTML = BlogPostList(allPosts);

Jedes Objekt, das die Informationen für einen einzelnen Blogbeitrag enthält, wird nacheinander an die BlogPost-Funktion übergeben, und die zurückgegebenen HTML-Strings werden in ein neues Array eingefügt. Wir rufen dann einfach join() auf diesem neuen Array auf, um das Array von Strings zu einem einzigen String zu kombinieren (getrennt durch einen leeren String), und fertig. Keine for-Schleifen, nur eine Liste von Objekten, die in eine Liste von HTML-Elementen umgewandelt werden.

Neu-Rendering

Wir können nun implizit HTML für gegebene Daten generieren, auf eine Weise, die wiederverwendbar und komponierbar ist, alles im Browser. Aber wie aktualisieren wir, wenn sich die Daten ändern? Woher wissen wir überhaupt, wann wir ein Update auslösen sollen? Dieses Thema ist heute eines der komplexesten und am heißesten diskutierten in der JavaScript-Framework-Community. Das effiziente Aktualisieren großer Mengen von DOM-Änderungen ist ein erstaunlich schwieriges Problem, an dem Ingenieure bei Facebook und Google jahrelang gearbeitet haben.

Glücklicherweise ist unsere sprichwörtliche Website nur ein Blog. Der Inhalt ändert sich so ziemlich nur, wenn wir uns einen anderen Blogbeitrag ansehen. Es gibt nicht viele Interaktionen zu erkennen, wir müssen unsere DOM-Operationen nicht optimieren. Wenn wir einen neuen Blogbeitrag laden, können wir einfach das DOM zerschlagen und neu aufbauen.

document.querySelector('body').innerHTML = PostPage(postData);

Wir könnten dies ein wenig schöner machen, indem wir es in eine Funktion verpacken

function update() {
  document.querySelector('body').innerHTML = PostPage(postData);
}

Jetzt können wir, wann immer wir einen neuen Blogbeitrag laden, einfach update() aufrufen und er erscheint. Wenn unsere Anwendung kompliziert genug wäre, dass sie häufig neu gerendert werden müsste – vielleicht ein paar Mal pro Sekunde in bestimmten Situationen – würde sie sehr schnell ruckeln. Sie könnten komplexe Logik schreiben, um herauszufinden, welche Teile der Seite bei einer bestimmten Datenänderung wirklich aktualisiert werden müssen und nur diese aktualisieren, aber das ist der Punkt, an dem Sie einfach ein Framework verwenden sollten.

Nicht nur für Inhalte

An diesem Punkt wurde fast unser gesamter Rendering-Code verwendet, um den tatsächlichen HTML- und Textinhalt innerhalb der Elemente zu bestimmen, aber wir müssen nicht dort aufhören. Da wir nur einen HTML-String erstellen, ist alles darin möglich. CSS-Klassen?

function BlogPost(postData) {
  return `<div class="post ${postData.isSponsored ? 'sponsored-post' : ''}">
            <h1>
              ${postData.title} ${postData.isSponsored ? '<img src="badge.png">' : ''}
            </h1>
            <h3>By ${postData.author}</h3>
            <p>${postData.body}</p>
          </div>`;
}

Check. HTML-Attribute?

function BlogPost(postData) {
  return `<div class="post ${postData.isSponsored ? 'sponsored-post' : ''}">
            <input type="checkbox" ${postData.isSponsored ? 'checked' : ''}>
            <h1>
              ${postData.title} ${postData.isSponsored ? '<img src="badge.png">' : ''}
            </h1>
            <h3>By ${postData.author}</h3>
            <p>${postData.body}</p>
          </div>`;
}

Check. Fühlen Sie sich frei, hier wirklich kreativ zu werden. Denken Sie über Ihre Daten nach und darüber, wie alle verschiedenen Aspekte davon in Markup dargestellt werden sollten, und schreiben Sie Ausdrücke, die das eine in das andere verwandeln.

Zusammenfassung

Hoffentlich gibt Ihnen dieser Beitrag ein gutes Werkzeugset für das Schreiben einfacher reaktiver, datengesteuerter Weboberflächen ohne den Overhead von Werkzeugen oder Frameworks. Dieser Code ist weitaus einfacher zu schreiben und zu warten als jQuery-Spaghetti, und es gibt absolut keine Hürde, ihn jetzt zu verwenden. Alles, worüber wir hier gesprochen haben, ist kostenlos in allen einigermaßen modernen Browsern enthalten, nicht einmal eine Bibliothek.

Teil 2 konzentriert sich auf klassenbasierte, zustandsbehaftete Komponenten, die in das Gebiet des zu komplizierten, um es vernünftigerweise in VanillaJS zu tun, vordringen werden. Aber beim Teufel, wir werden es trotzdem versuchen, und es wird interessant werden.

Artikelserie

  1. Rein funktionaler Stil (Du bist hier!)
  2. Klassenbasierte Komponenten