https://a.storyblok.com/f/270183/1368x665/7ed91865d4/implement-your-own-ssr.png

Wie Sie Ihren eigenen SSR-Server für Webkomponenten implementieren

Zuletzt aktualisiert am July 30, 2024

Lesedauer: 11 Minuten

Server Side Rendering (SSR) ist heute ein brandaktuelles Thema. Was hat es mit den React Server Components auf sich, die all diese Buzz-Words mit sich bringen, die "ich einfach in meinem Projekt implementieren muss"... In diesem Artikel werden wir unseren eigenen Server bauen, um die Mechanik hinter dem SSR-Paradigma und seine möglichen Erweiterungen zu lernen. Wenn Sie verstehen, wie SSR funktioniert, können Sie aktuelle SSR-Lösungen an Ihre Bedürfnisse anpassen. Zum Beispiel könnten Sie SSR-Webkomponenten in einer Nuxt Server benötigen.

Bei Vonage haben wir ein öffentliches Projekt namens Entwickler-Portal. Dabei handelt es sich um eine Dokumentations-Website, die sich nicht hinter einer Anmeldeseite verbirgt (d.h. öffentlich ist) und hauptsächlich Inhalte enthält. Wir wollen auch, dass der Inhalt suchmaschinenoptimiert (SEO) ist, was ihn zu einem guten Kandidaten für SSR macht.

Das Entwicklerportal ist in Vue geschrieben und wird mit Nuxt. Nuxt ermöglicht SSR über sein Universal Rendering Mechanismus. Wir mussten Nuxt in die Lage versetzen, auch die SSR Web-Komponenten des Design-Systems. So begann unsere Reise zur Entwicklung eines SSR-Mechanismus für Webkomponenten.

Was ist SSR?

Kurz gesagt, SSR ist der Prozess, bei dem unsere Anwendung auf einem Server läuft und einfaches HTML an den Client zurückgibt.

Der Vue-Code wird in unserem Portal auf einem node.js (Nuxt)-Server gerendert. Die Ausgabe des Renderings ist HTML (mit eventuell eingefügtem CSS). Dieses HTML (+CSS) wird an den Browser gesendet und dort angezeigt - ohne jegliches JavaScript. So bekommt der Nutzer die Website sehr schnell zu sehen.

Außerdem wird das Layout der Website so angezeigt, wie es mit JavaScript sein sollte, um starke Layoutverschiebungen zu vermeiden, die dadurch entstehen, dass Komponenten plötzlich Inhalte erhalten und sich ausdehnen, sobald JavaScript einsetzt.

Beachten Sie, dass Bots (z. B. Suchmaschinen-Crawler) JavaScript in der Regel nicht sehen. Wenn Sie also dieses Bündel an inhaltsreichem HTML sofort einbinden, kann das Wunder für Ihr Suchmaschinen-Ranking bewirken.

Während Formulare, Links, Videos usw. in nicht-komplexen Beispielen funktionieren sollten, haben wir ohne JavaScript keine erweiterte Interaktivität. Der Benutzer sieht also die Website, kann aber nicht mit nicht-nativen Funktionen interagieren.

Dokumentation ist ein klassisches Beispiel dafür, wo SSR wirklich gebraucht wird. Sie zeigt dem Benutzer hauptsächlich Text und Bilder, und die Interaktivität besteht hauptsächlich im Scrollen, um mehr von dem Text und den Bildern zu sehen. Diese "dünne" Ansichtsebene von Text und Bildern kann man als die dehydrierte Version unserer Anwendung bezeichnet werden.

Was ist, wenn wir mit der Seite interagieren müssen? Dann müssen wir hydratisieren unsere Komponenten hydrieren. Hydratisierung ist eine vermarktbare Bezeichnung für "unser JavaScript laden". Sobald JavaScript geladen ist, erhalten wir unsere Funktionalität.

Im Wesentlichen hilft uns SSR, unsere statischen Inhalte schneller zu laden, damit die Nutzer sie konsumieren, aber nicht mit ihnen interagieren können. Außerdem trägt es zu unserem SEO-Ranking bei.

Wie baut man einen eigenen SSR-Server?

Das erste, was ich den meisten Leuten empfehle, ist: Bauen Sie nicht Ihren eigenen SSR-Server.

In diesem Artikel werden wir unseren eigenen Server bauen, um die Mechanismen hinter dem SSR-Paradigma und seine möglichen Erweiterungen kennenzulernen. Wenn Sie verstehen, wie SSR funktioniert, können Sie die aktuellen SSR-Lösungen an Ihre Bedürfnisse anpassen. Zum Beispiel könnten Sie SSR-Webkomponenten in einer Nuxt Server benötigen.

Nachdem wir nun die Nützlichkeit des Aufbaus eines SSR-Servers (oder das Fehlen eines solchen ;) ) verstanden haben, wollen wir einen zu Lernzwecken bauen.

Ein SSR-Server ist im Wesentlichen ein HTTP-Server, der eine Anfrage vom Client erhält und anhand dieser Anfrage eine Vorlage parst und HTML an den Client zurückgibt.

Hier ist eine Illustration des Prozesses:

A client requests a page from the SSR server. The SSR server calls a rendering function with the URL parameters. The rendering function returns serializable HTML content. The SSR server serves it to the client.SSR architecture

Auf dieser Grundlage können wir die Bausteine unseres Servers definieren:

  1. Ein HTTP-Server, der Routen verarbeitet

  2. Eine Rendering-Funktion

Einrichten des HTTP-Servers

Der HTTP-Server ist ein ziemlicher Standard. Er bedient statische Dateien und analysiert die an ihn gesendeten Routen (wie Homepage):

import http from 'http';
import fs from 'fs';
import path from 'path';

import * as routes from './routes/index.mjs';
const CONTENT_TYPES = {
    '.js': 'text/javascript',
    '.mjs': 'text/javascript',
    '.css': 'text/css',
    '.png': 'image/png',
    '.jpg': 'image/png',
    '.gif': 'image/png',
    '.ico': 'image/png',
};

const server = http.createServer(async (req, res) => {
    function returnFileContent(filePath, contentType) {
        fs.readFile(filePath, (err, content) => {
            if (err) {
                if (err.code === 'ENOENT') {
                    res.writeHead(404);
                    res.end('File not found');
                } else {
                    res.writeHead(500);
                    res.end(`Server Error: ${err.code}`);
                }
            } else {
                res.writeHead(200, { 'Content-Type': contentType });
                res.end(content, 'utf-8');
            }
          });
    }

    let filePath = '.' + req.url;
    if (filePath === './') {
        filePath = 'HomePage';
    }

    const extname = path.extname(filePath);
    let contentType = CONTENT_TYPES[extname] ?? 'text/html';

    if (contentType === 'text/html') {
      res.writeHead(200, { 'Content-Type': contentType });
      res.end(await routes[filePath].template, 'utf-8');
    } else {
        returnFileContent(filePath, contentType);
    }
});

const PORT = 3000;
server.listen(PORT, () => {
    console.log(`Server running on http://localhost:${PORT}/`);
});

Das Routing-System

Unser Server importiert eine Routen Objekt. Dieses Routen Objekt enthält eine Hashtabelle für Routen. Jede Route hat eine Vorlage Methode, die gültiges HTML zurückgibt. Sie wird wie folgt verwendet:

res.end(await routes[filePath].template, 'utf-8');

In unserem Projekt werden wir eine Routen Ordner, der die Datei index.mjs Datei enthält.

Neben der Indexdatei werden wir eine startseite Ordner, in dem die Homepage-Route gespeichert wird. Das sieht dann so aus:

The routes folder consists of an index barrel file and a home-page folder.Routes folder structureHomepage enthält ihre eigene index.mjs Datei:

The template index file exposes a `template` method that is used as the rendering function in the server.Homepage template index fileDieses HomePage-Objekt wird auch aus der Datei routes/index.mjs Datei exportiert:

exportiere * aus './home-page/index.mjs'

Jetzt müssen wir nur noch implementieren getHomePageTemplate in implementieren. home-page.template.mjs:

export function getHomePageTemplate() {
    return `
        <div>Hello World</div>
    `;
}

Schließlich müssen wir die Route in unserem Server verwenden, also werden wir die Hauptdatei index.mjs ändern:

import http from 'http';
import fs from 'fs';
import path from 'path';

import * as routes from './routes/index.mjs';
const CONTENT_TYPES = {
    '.js': 'text/javascript',
    '.css': 'text/css',
    '.png': 'image/png',
    '.jpg': 'image/png',
    '.gif': 'image/png',
};

function returnFileContent(filePath, contentType) {
    fs.readFile(filePath, (err, content) => {
        if (err) {
            if (err.code === 'ENOENT') {
                res.writeHead(404);
                res.end('File not found');
            } else {
                res.writeHead(500);
                res.end(`Server Error: ${err.code}`);
            }
        } else {
            res.writeHead(200, { 'Content-Type': contentType });
            res.end(content, 'utf-8');
        }
      });
}
const server = http.createServer((req, res) => {
    let filePath = '.' + req.url;
    if (filePath === './') {
        filePath = 'HomePage';
    }

    const extname = path.extname(filePath);
    let contentType = CONTENT_TYPES[extname] ?? 'text/html';

    if (contentType === 'text/html') {
      res.writeHead(200, { 'Content-Type': contentType });
      res.end(routes[filePath].template(), 'utf-8');
    } else {
        returnFileContent(filePath, contentType);
    }
});

const PORT = 3000;
server.listen(PORT, () => {
    console.log(`Server running on http://localhost:${PORT}/`);
});

Hier importieren wir die Routen (Zeile 5) und verwenden sie, wenn wir zurückkehren text/html (Zeile 41).

Die Ergebnisse sind verblüffend!

A browser showing the Hellow World served from the SSR server side-by-side with the dev tools open on the Elements panel showing the div with "Hello World!" under the body tag.The results of the served content in the browser

Wir fügen eine bessere Vorlage hinzu

Diese Vorlage ist ziemlich langweilig... lassen Sie uns etwas Pikantes bringen. Hierfür verwende ich die Lebendige Gestaltungssystem. Vivid Komponenten sind reine Webkomponenten. Wir werden sie verwenden, um unsere Vorlage aufzupeppen und sie serverseitig zu rendern.

In Vivid's Seite für Schaltflächenkomponentenkönnen wir das Beispiel des Aussehens nehmen, das vier verschiedene Schaltflächen zeigt:

Samples of the Vivid button component with 4 varying appearances. Below them the relevant code snippet shows.The button code sample from the Vivid documentation

Wir können unsere Vorlage ersetzen in home-page.template.mjs: durch den Beispielcode:

export function getHomePageTemplate() {
    return `
        <vwc-button label="ghost" appearance="ghost"></vwc-button>
        <vwc-button label="ghost-light" appearance="ghost-light"></vwc-button>
        <vwc-button label="filled" appearance="filled"></vwc-button>
        <vwc-button label="outlined" appearance="outlined"></vwc-button>
    `;
}

Und das Ergebnis ist hier:

A browser showing a blank page served from the SSR server side-by-side with the dev tools open on the Elements panel showing the four vwc-button elements in the DOM.The results of the served content in the browser after adding the Vivid buttons to the template

Eine leere Seite neben einem nicht ganz leeren Textkörper. Wo sind die Komponenten aus dem Codebeispiel?

Sie werden nicht geladen, weil wir JS und CSS laden müssen.

Wie lädt man CSS und JavaScript?

Dies ist normalerweise eine triviale Frage, aber wie wird sie in einem SSR-Server gestellt?

Wählen wir die einfachste Möglichkeit, nämlich die Verwendung eines CDN. Sie können Vivid-Komponenten mit dieser Konvention importieren:

https://unpkg.com/@vonage/vivid@latest/{pathToFile}

Auf diese Weise können wir unseren Code in die Vorlage importieren:

export function getHomePageTemplate() {
    return `
        <style>
            @import "https://unpkg.com/@vonage/vivid@latest/styles/tokens/theme-light.css";
            @import "https://unpkg.com/@vonage/vivid@latest/styles/core/all.css";
            @import "https://unpkg.com/@vonage/vivid@latest/styles/fonts/spezia-variable.css";
        </style>
        <vwc-button label="ghost" appearance="ghost"></vwc-button>
        <vwc-button label="ghost-light" appearance="ghost-light"></vwc-button>
        <vwc-button label="filled" appearance="filled"></vwc-button>
        <vwc-button label="outlined" appearance="outlined"></vwc-button>
        <script type="module" src="https://unpkg.com/@vonage/vivid@latest/button"></script>
    `;
}

Wenn wir unseren Client testen, werden wir unsere Komponenten sehen. Naja... irgendwie schon:

A browser showing the four unstyled buttons served from the SSR server side-by-side with the dev tools open on the Elements panel showing the CSS imports and the four vwc-button elements in the DOM.The results of the served content in the browser after importing the Vivid library client-side

Eine Sache, die wir tun müssen, um die Lebendige Komponenten funktionieren, ist das Hinzufügen der vvd-root Klasse zu dem Element hinzufügen, das sie umhüllt (normalerweise der Body...).

Lassen Sie uns einen Wrapper für unsere Vorlage definieren:

export function getHomePageTemplate() {
    return `
        <style>
            @import "https://unpkg.com/@vonage/vivid@latest/styles/tokens/theme-light.css";
            @import "https://unpkg.com/@vonage/vivid@latest/styles/core/all.css";
            @import "https://unpkg.com/@vonage/vivid@latest/styles/fonts/spezia-variable.css";

            #buttons-wrapper {
                min-width: 50px;
                min-height: 50px;
                background-color: crimson;
            }
        </style>
        <div id="buttons-wrapper" class="vvd-root">
            <vwc-button label="ghost" appearance="ghost"></vwc-button>
            <vwc-button label="ghost-light" appearance="ghost-light"></vwc-button>
            <vwc-button label="filled" appearance="filled"></vwc-button>
            <vwc-button label="outlined" appearance="outlined"></vwc-button>
        </div>
        <script type="module" src="https://unpkg.com/@vonage/vivid@latest/button"></script>
    `;
}

Hier ist das Ergebnis:

An animated gif of the browser loading the components. It shows the components load after a few milliseconds, causing a shift in the page's layout.An animated Gif showing the page load of the code above

Die Schaltflächen funktionieren also, aber... Können Sie das Problem erkennen?

Der HTML-Code wird geladen - wie wir am Wrapping-Div sehen können - und dann werden die Schaltflächen gerendert, sobald das JS einsetzt, was zu einer großen Layout-Verschiebung führt. Stellen Sie sich vor, dass dies in einer größeren App mit viel mehr Komponenten passiert.

Das ist nicht gut...

Wie können wir diesen Flash verhindern? Lassen Sie uns die Komponenten auf dem Server rendern!

Erstellen der Rendering-Funktion

Anstatt JS auf der Client-Seite zu laden, können wir die Komponenten auf dem Server rendern und ein vollständiges HTML senden. Wir müssen also einen Weg finden, unsere Komponenten auf dem Server so zu rendern, als ob sie in einem Browser wären.

Jedes Framework hat eine andere Rendering-Methode.

Webkomponenten werden nativ vom Browser gerendert. Webkomponenten bringen auch die Idee des Schatten-DOM mit sich. Im Wesentlichen ist das Schatten-DOM ein Dokumentfragment, in das Sie HTML und CSS einfügen können. Zu diesem Zweck erstellt der Browser eine Schattenwurzel innerhalb unserer Komponente:

A snippet from the chrome dev tools Elements panel showing a vwc-button tag with a shadowroot as its child.Shadow Root under the button

Alles außerhalb der Schattenwurzel ist "im Licht", während der Rest im Schatten liegt. Der Vorteil eines shadowDOMs ist, dass es die Stile kapselt. Stile im Inneren wirken sich nicht auf etwas außerhalb aus und (fast vollständig) andersherum.

Das heißt, wenn wir unsere Vorlage nehmen und sie als innerHTML eines div setzen, sollten wir gerenderte Komponenten erhalten. Lassen Sie uns das im Browser ausprobieren:

const div = document.createElement('div');
div.innerHTML = `
<style>
            @import "https://unpkg.com/@vonage/vivid@latest/styles/tokens/theme-light.css";
            @import "https://unpkg.com/@vonage/vivid@latest/styles/core/all.css";
            @import "https://unpkg.com/@vonage/vivid@latest/styles/fonts/spezia-variable.css";

            #buttons-wrapper {
                min-width: 50px;
                min-height: 50px;
                background-color: crimson;
            }
        </style>
        <div id="buttons-wrapper" class="vvd-root">
            <vwc-button label="ghost" appearance="ghost"></vwc-button>
            <vwc-button label="ghost-light" appearance="ghost-light"></vwc-button>
            <vwc-button label="filled" appearance="filled"></vwc-button>
            <vwc-button label="outlined" appearance="outlined"></vwc-button>
        </div>
        <script type="module" src="https://unpkg.com/@vonage/vivid@latest/button"></script>
`;
document.body.appendChild(div);

Wenn Sie diesen Code in Ihren Browser einfügen, sollten Sie das purpurrote Div ohne die Schaltfläche sehen, da das JS nicht importiert wird.

Trotzdem - wenn Sie das JS vorher importiert hätten, hätte es funktioniert:

const script = document.createElement('script');
script.type = 'module';
script.src = 'https://unpkg.com/@vonage/vivid@latest/button';
const div = document.createElement('div');
div.innerHTML = `
<style>
            @import "https://unpkg.com/@vonage/vivid@latest/styles/tokens/theme-light.css";
            @import "https://unpkg.com/@vonage/vivid@latest/styles/core/all.css";
            @import "https://unpkg.com/@vonage/vivid@latest/styles/fonts/spezia-variable.css";

            #buttons-wrapper {
                min-width: 50px;
                min-height: 50px;
                background-color: crimson;
            }
        </style>
        <div id="buttons-wrapper" class="vvd-root">
            <vwc-button label="ghost" appearance="ghost"></vwc-button>
            <vwc-button label="ghost-light" appearance="ghost-light"></vwc-button>
            <vwc-button label="filled" appearance="filled"></vwc-button>
            <vwc-button label="outlined" appearance="outlined"></vwc-button>
        </div>
`;
document.body.appendChild(div);
document.body.appendChild(script);

Wie auf Google.com getestet:

It shows the four buttons at the bottom of the page.The google home page with the button HTML snippet added to it

Die Sache ist die, dass die Elemente document, body und HTML nicht nativ auf der Serverseite existieren. Also...

Wie kann man HTML auf einem Server rendern?

Gute Frage! Gut, dass Sie fragen.

Es gibt verschiedene Möglichkeiten, HTML auf der Serverseite zu rendern.

Da Vivid seine Komponenten mit jsdom testet, wissen wir, dass es unsere Komponenten ohne Browser darstellen kann.

Wenn wir also eine JSDOM-Umgebung auf unserem Server erstellen, können wir unseren Code zum Rendern unserer Komponenten verwenden.

Das ist dank des allmächtigen NPM ganz einfach!

npm i global-jsdom/register jsdom wird hinzufügen jsdom - eine Bibliothek, die die DOM-API des Browsers in der Server-Laufzeitumgebung nachahmt, so dass sie Markup wie im Browser erstellen kann. global-jsdom/register stellt die Browser-API global zur Verfügung, damit wir sie in unserem Code verwenden können. So können wir unsere Komponenten serverseitig rendern.

Ändern wir den Code unserer Vorlage ein wenig, um das zu nutzen:

import 'global-jsdom/register';
import '@vonage/vivid/button';

export function getHomePageTemplate() {
    const template = `
        <style>
            @import "https://unpkg.com/@vonage/vivid@latest/styles/tokens/theme-light.css";
            @import "https://unpkg.com/@vonage/vivid@latest/styles/core/all.css";
            @import "https://unpkg.com/@vonage/vivid@latest/styles/fonts/spezia-variable.css";

            #buttons-wrapper {
                min-width: 50px;
                min-height: 50px;
                background-color: crimson;
            }
        </style>
        <div id="buttons-wrapper" class="vvd-root">
            <vwc-button label="ghost" appearance="ghost"></vwc-button>
            <vwc-button label="ghost-light" appearance="ghost-light"></vwc-button>
            <vwc-button label="filled" appearance="filled"></vwc-button>
            <vwc-button label="outlined" appearance="outlined"></vwc-button>
        </div>
    `;

    const div = document.createElement('div');
    div.innerHTML = template;
    document.body.appendChild(div);
    return div.innerHTML;
}

Wir importieren global-jsdom/register. Beachten Sie, dass wir den Import von @vonage/vivid/button Paket serverseitig importieren, damit die Webkomponente als eine solche gerendert werden kann.

Wir lassen jsdom unser Template rendern, indem wir es einfach zum DOM hinzufügen und sein innerHTML. Das sieht dann so aus:

A browser showing only the red background without the buttons side-by-side with the dev tools open on the Elements panel showing the CSS imports and the four vwc-button elements in the DOM without a shadowroot.The results of the served content in the browser after serving the HTML from the server.

OH NEIN! Keine Schaltflächen in der Ansicht! Sie sind tatsächlich im DOM. Wir können auch die Eingabe im hellen DOM innerhalb jeder Schaltfläche sehen (sie ist da, um die Formularzuordnung zu lösen).

Der Grund, warum wir nichts sehen, ist, dass innerHTML uns nicht den Inhalt des shadowDOMs liefert.

Was wir also versuchen könnten, ist, die shadowDOM jeder Komponente wie folgt zu ermitteln:

function appendOwnShadow(element) {

    const shadowTemplate = ${element.shadowRoot.innerHTML};

    const tmpElement = document.createElement('div');

    tmpElement.innerHTML = shadowTemplate;

    element.appendChild(tmpElement.children[0]);

}

Array.from(div.querySelectorAll(‘vwc-button’))

    .forEach(button => button.appendChild(appendOwnShadow(button)));

Daraus ergibt sich diese Benutzeroberfläche:

The four buttons show, but they are deformed.The four buttons deformedJuhu! Wir können etwas sehen, aber... es ist nicht genau dasselbe, oder?

Wenn wir uns den HTML-Code ansehen, sehen wir, dass die shadowroot bei diesem Gist-Link fehlt.

Dies könnte sich auf jeden Fall auf das Styling der Komponente auswirken, da die Kapselung verloren geht.

Explizites Rendern von Schatten-DOM ohne JavaScript

Zu diesem Zweck definiert die HTML-Spezifikation jetzt einen shadowrootmode Attribut für den Template-Tag. Wenn der Browser auf <template shadowrootmode=”open”>, trifft, weiß er, dass er alles, was in dieser Vorlage enthalten ist, in einem Schatten-DOM wiedergeben muss.

Mit diesem Wissen können wir unseren Code wie folgt ändern:

function appendOwnShadow(element) {

    const shadowTemplate = <template shadowrootmode="open">   ${element.shadowRoot.innerHTML}</template>;

    const tmpElement = document.createElement('div');

    tmpElement.innerHTML = shadowTemplate;

    element.appendChild(tmpElement.children[0]);

}

Array.from(div.querySelectorAll(‘vwc-button’))

    .forEach(button => button.appendChild(appendOwnShadow(button)));

Sie wird nun wie folgt dargestellt:

The four buttons show, and they are as expectedThe four buttons appear correctly

Genau so haben wir es erwartet! Hurra!

Wenn Sie sich das DOM jetzt ansehen, sieht es wie folgt aus:

The four buttons in the DOM, including the shadow-rootThe buttons' HTML snippet from the Elements PanelWie cool ist das denn? Wir haben unsere Webkomponenten serverseitig gerendert und die Layoutverschiebung in unserer App verhindert!

Versuchen wir, unsere Bewerbung aufzupeppen.

Handhabung komplexer Komponenten

Die von uns verwendete Schaltfläche war recht einfach. Versuchen wir, eine Schaltfläche mit einem Symbol darin zu verwenden:

<vwc-button icon="facebook-color" label="ghost" appearance="ghost"></vwc-button>
<vwc-button icon="linkedin-color" label="ghost-light" appearance="ghost-light"></vwc-button>
<vwc-button icon="twitter-color" label="filled" appearance="filled"></vwc-button>
<vwc-button icon="instagram-color" label="outlined" appearance="outlined"></vwc-button>

Und so sieht es im Browser aus:

The buttons appear, but internal icon elements are not renderedThe buttons appear, but internal icon elements are not renderedEs hat sich etwas geändert, aber wir können keine Symbole sehen...

Der HTML-Code innerhalb der Schaltfläche sieht wie folgt aus:

The HTML inside a shadow-root shows that we have a `vwc-icon` inside but it has no shadowroot of its own and that it didn't receive the name attribute.A button's shadow-root innerHTMLWir können sehen vwc-icon genau dort in der Mitte. Wir können hier zwei Probleme sehen:

  1. Das Symbol hat keine Attribute - es weiß also nicht wirklich, wie es sich darstellen soll.

  2. Das Symbol hat keinen Inhalt - vor allem keine Schattenwurzel.

Lösung für das Problem, dass das Icon keine Attribute erhält

Lassen Sie uns das einfachere Problem lösen. Das Symbol erhält seine Attribute von der Schaltflächenkomponente. Die Vorlage wird asynchron gerendert. Das bedeutet, dass wir nach dem Hinzufügen der div zum DOM hinzugefügt haben, erfolgt die eigentliche Aktualisierung nach einer weiteren Iteration der der Ereignisschleife. Wir müssen also den Abschluss des Rendering-Prozesses abwarten.

Zu diesem Zweck können wir die Vorlagenfunktion als asynchron festlegen und einen Ereignisschleifenzyklus abwarten:

import 'global-jsdom/register';
import '@vonage/vivid/button';

function appendOwnShadow(element) {
    const shadowTemplate = `<template shadowrootmode="open">${element.shadowRoot.innerHTML}</template>`;
    const tmpElement = document.createElement('div');
    tmpElement.innerHTML = shadowTemplate;
    element.appendChild(tmpElement.children[0]);
}

export async function getHomePageTemplate() {
    const template = `
        <style>
            @import "https://unpkg.com/@vonage/vivid@latest/styles/tokens/theme-light.css";
            @import "https://unpkg.com/@vonage/vivid@latest/styles/core/all.css";
            @import "https://unpkg.com/@vonage/vivid@latest/styles/fonts/spezia-variable.css";

            #buttons-wrapper {
                min-width: 50px;
                min-height: 50px;
                background-color: crimson;
            }
        </style>
        <div id="buttons-wrapper" class="vvd-root">
            <vwc-button icon="facebook-color" label="ghost" appearance="ghost"></vwc-button>
            <vwc-button icon="linkedin-color" label="ghost-light" appearance="ghost-light"></vwc-button>
            <vwc-button icon="twitter-color" label="filled" appearance="filled"></vwc-button>
            <vwc-button icon="instagram-color" label="outlined" appearance="outlined"></vwc-button>
        </div>
    `;

    const div = document.createElement('div');
    div.innerHTML = template;
    document.body.appendChild(div);
    await new Promise(res => setTimeout(res));
    Array.from(div.querySelectorAll('vwc-button')).forEach(appendOwnShadow);
    return div.innerHTML;
}

Beachten Sie, dass wir die magische await new Promise(res => setTimeout(res)); in Zeile 28 hinzugefügt.

Wenn wir nun einen Blick auf unser HTML werfen, sehen wir, dass das Symbol die Attribute erhält:

A vwc-icon without a shadow root but with the name attribute set with "facebook-color"A snippet showing the vwc-icon inside the button after the change

Laden interner Komponenten

Das zweite Problem - wegen dem wir die Icons nicht sehen - ergibt sich aus der Tatsache, dass wir das HTML der internen Komponenten der Schattenwurzel nicht erhalten.

Eine Möglichkeit, dies zu beheben, wäre, alle Webkomponenten rekursiv zu finden und zu rendern.

Um die Komponenten zu finden, können wir den DOM-Baum wie folgt durchlaufen:

function getAllNestedShadowRootsParents(element) {
    const nestedShadowRoots = [];

    function traverseShadowRoot(node) {
        if (node.shadowRoot) {
            nestedShadowRoots.push(node);
            node.shadowRoot.querySelectorAll('*').forEach(child => {
                traverseShadowRoot(child);
            });
        } else {
            Array.from(node.querySelectorAll('*')).forEach(child => traverseShadowRoot(child));
        }
    }

    traverseShadowRoot(element);
    return Array.from(new Set(nestedShadowRoots));
}

Diese Funktion erhält ein Element (vermutlich unser Wrapping-Div) und findet alle Webkomponenten mit shadowDOM.

Jetzt müssen wir sie nur noch in unserer Vorlagendatei parsen:

Machen wir es so:

import 'global-jsdom/register';
import '@vonage/vivid/button';

function getAllNestedShadowRootsParents(element) {
    const nestedShadowRoots = [];

    function traverseShadowRoot(node) {
        if (node.shadowRoot) {
            nestedShadowRoots.push(node);
            node.shadowRoot.querySelectorAll('*').forEach(child => {
                traverseShadowRoot(child);
            });
        } else {
            Array.from(node.querySelectorAll('*')).forEach(child => traverseShadowRoot(child));
        }
    }

    traverseShadowRoot(element);
    return Array.from(new Set(nestedShadowRoots));
}

function appendOwnShadow(element) {
    const shadowTemplate = `<template shadowrootmode="open">${element.shadowRoot.innerHTML}</template>`;
    const tmpElement = document.createElement('div');
    tmpElement.innerHTML = shadowTemplate;
    element.appendChild(tmpElement.children[0]);
}

export async function getHomePageTemplate() {
    const template = `
        <style>
            @import "https://unpkg.com/@vonage/vivid@latest/styles/tokens/theme-light.css";
            @import "https://unpkg.com/@vonage/vivid@latest/styles/core/all.css";
            @import "https://unpkg.com/@vonage/vivid@latest/styles/fonts/spezia-variable.css";

            #buttons-wrapper {
                min-width: 50px;
                min-height: 50px;
                background-color: crimson;
            }
        </style>
        <div id="buttons-wrapper" class="vvd-root">
        <vwc-button icon="facebook-color" label="ghost" appearance="ghost"></vwc-button>
        <vwc-button icon="linkedin-color" label="ghost-light" appearance="ghost-light"></vwc-button>
        <vwc-button icon="twitter-color" label="filled" appearance="filled"></vwc-button>
        <vwc-button icon="instagram-color" label="outlined" appearance="outlined"></vwc-button>
        </div>
    `;

    const div = document.createElement('div');
    div.innerHTML = template;
    document.body.appendChild(div);
    await new Promise(res => setTimeout(res));
    getAllNestedShadowRootsParents(div).reverse().forEach(appendOwnShadow);
    return div.innerHTML;
}

Beachten Sie die Änderung in Zeile 54 - wir gehen alle Elemente mit shadow DOM in umgekehrter Reihenfolge durch und fügen eine shadowroot Vorlage mit ihrem innerHTML an.

Das Ergebnis ist verblüffend:

The four buttons with their icons rendered correctlyThe four buttons with their icons rendered correctlyWenn Sie bis jetzt durchgehalten haben, gute Arbeit! Sie haben die Grundlagen der SSR verstanden.

Können wir mehr servieren?

Unser einfacher SSR-Server kann weiter optimiert werden. Zum Beispiel sind einige Dinge, wie die CSS und die SVGs der Icons, immer noch von Servern abhängig, die weit entfernt sind. Wir können unserem SSR-Server mehr Logik hinzufügen, um sie abzurufen und in das zurückgegebene HTML einzubinden.

Weitere Ideen können von anderen SSR-Systemen übernommen werden. React-Server-Komponenten verfügen beispielsweise über eine eigene API zum Abrufen und Senden von Anfragen an den Server, der wiederum die Daten anfordert und die erforderliche Ansicht rendert.

Qwik richtet Service Worker ein, die JS im Hintergrund abrufen.

Alle SSR-Frameworks haben viele Optimierungen für Sie vorgenommen, aber sie passen nicht immer zu Ihren Bedürfnissen, daher ist das Wissen, wie sie funktionieren, ein guter Anfang, um sie zu erweitern.

Zusammenfassung

Das war eine tolle Fahrt, nicht wahr?

Der Aufbau eines SSR-Mechanismus ist im Grunde recht einfach, aber er kann immer noch verbessert, angepasst und optimiert werden. Möglicherweise müssen Sie eine große Codebasis pflegen, nur um SSR zu handhaben.

Sie können entweder nextjs (react), Nuxtjs (vue) oder eine andere SSR-Bibliothek verwenden. Wenn Sie Webkomponenten verwenden, können SSR-Bibliotheken wie litssr oder fastssr Ihnen die schwere Arbeit abnehmen.

Eine große Einschränkung bei diesen SSR-Frameworks oder -Bibliotheken ist, dass sie nur für das Framework oder die Bibliothek funktionieren, für die sie gedacht sind.

Unser Anwendungsfall war es, einen SSR-Mechanismus zu entwickeln, der mit Nuxt. Sie können meinen Code also ein SSR-Plugin nennen. Ich hoffe, dieser Artikel hat Ihnen einen Hinweis darauf gegeben, wie Sie mit der Erstellung eines solchen Plugins beginnen können, falls der Bedarf jemals entsteht.

Die Gemeinsamkeit aller SSRs ist, dass es eine Rendering-Funktion gibt. Diese Funktion wird in Ihrer Vorlage verwendet und gibt einen HTML-String zurück, der an den Client gesendet wird (mit Ausnahme von React Server Components, die tatsächlich JSON senden - aber das würde den Rahmen dieses Artikels sprengen).

Ein Teil des HTML wird später hydriert, nachdem das JavaScript asynchron geladen wurde, ohne die Seite zu blockieren. In diesem Artikel haben wir gelernt, wie man das mit Webkomponenten und Schatten-DOM macht.

Wir blockieren die Seite nicht durch das Laden von JS, was uns hilft, den Inhalt schneller bereitzustellen, starke Layoutverschiebungen zu vermeiden und möglicherweise unser SEO-Ranking zu verbessern.

Bitte machen Sie mit bei unserem Vonage Community Slack oder senden Sie uns eine Nachricht auf X, früher bekannt als Twitterund lassen Sie uns wissen, wie wir helfen können!

Teilen Sie:

https://a.storyblok.com/f/270183/400x400/7bf76cb05c/yonatankra.png
Yonatan KraVonage Software Architekt

Yonatan war an einigen großartigen Projekten in der Akademie und in der Industrie beteiligt - von C/C++ über Matlab bis hin zu PHP und Javascript. Früher war er CTO bei Webiks und Softwarearchitekt bei WalkMe. Derzeit ist er Softwarearchitekt bei Vonage und egghead-Dozent.