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

Como implementar seu próprio servidor de SSR para Web Components

Tempo de leitura: 11 minutos

A renderização do lado do servidor (SSR) é um tema muito em voga atualmente. O que há com os React Server Components trazendo todos esses termos da moda que “eu simplesmente preciso implementar no meu projeto”...Neste artigo, vamos construir nosso próprio servidor para entender a mecânica por trás do paradigma SSR e suas possíveis extensões. Compreender como o SSR funciona vai ajudar você a adaptar as soluções atuais de SSR às suas necessidades. Por exemplo, talvez você precise de componentes web SSR em um Nuxt .

Na Vonage, temos um projeto público chamado Portal do Desenvolvedor. É um site de documentação que não exige login (ou seja, é público) e contém principalmente conteúdo. Também queremos que o conteúdo seja otimizado para mecanismos de busca (SEO), o que o torna um bom candidato para SSR.

O portal do desenvolvedor foi escrito em Vue e é hospedado usando Nuxt. O Nuxt permite a renderização no servidor (SSR) por meio de sua mecanismo de renderização universal . Precisávamos fazer com que o Nuxt também realizasse o SSR dos componentes web do nosso sistema de design. Assim começou nossa jornada de criação de um mecanismo de SSR para Web Components.

O que é SSR?

Em resumo, o SSR é o processo de executar nosso aplicativo em um servidor e retornar HTML simples ao cliente. 

O código Vue é renderizado em nosso portal em um servidor Node.js (Nuxt). O resultado da renderização é HTML (possivelmente com CSS incorporado). Esse HTML (+CSS) é enviado ao navegador e exibido nele — sem nenhum JavaScript. Assim, o usuário consegue visualizar o site muito rapidamente.

Além disso, exibir o layout do site da maneira correta com JavaScript evita grandes mudanças no layout decorrentes de componentes que recebem conteúdo repentinamente e se expandem assim que o JavaScript é executado.

Observe que os bots (como os rastreadores dos mecanismos de busca) geralmente não reconhecem o JavaScript; portanto, disponibilizar imediatamente esse conteúdo rico em HTML pode fazer maravilhas para sua classificação nos mecanismos de busca.

Embora formulários, links, Videos etc. devam funcionar em exemplos simples, não temos interatividade avançada sem o JavaScript. Assim, o usuário consegue visualizar o site, mas não consegue interagir com funcionalidades não nativas. 

A documentação é um exemplo clássico de situação em que o SSR é realmente necessário. Ela exibe principalmente texto e imagens ao usuário, e a interatividade se resume basicamente à rolagem para visualizar mais do texto e das imagens. Essa camada de visualização “simples”, composta por texto e imagens, pode ser denominada versão desidratada da nossa aplicação.

E se precisarmos interagir com a página? Teremos que hidratar nossos componentes. Hidratação é um nome mais atraente para “carregar nosso JavaScript”. Assim que o JavaScript for carregado, teremos nossa funcionalidade.

Em essência, o SSR nos ajuda a carregar nosso conteúdo estático mais rapidamente, para que os usuários possam acessá-lo, mas não interagir com ele. Ele também contribui para nossa classificação em SEO.

Como criar seu próprio servidor SSR?

A primeira coisa que recomendo à maioria das pessoas é: Não monte seu próprio servidor SSR.

Dito isso, neste artigo, vamos construir nosso próprio servidor para aprender os mecanismos por trás do paradigma SSR e suas possíveis extensões. Compreender como o SSR funciona ajudará você a adaptar as soluções atuais de SSR às suas necessidades. Por exemplo, você pode precisar de componentes web com SSR em um Nuxt .

Agora que entendemos a utilidade de criar um servidor SSR (ou a falta dela ;) ), vamos criar um para fins de aprendizado.

Um servidor SSR é, essencialmente, um servidor HTTP que recebe uma solicitação do cliente e, por meio dessa solicitação, analisa um modelo e retorna HTML ao cliente.

Aqui está uma ilustração do processo:

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

A partir disso, podemos definir os componentes básicos do nosso servidor:

  1. Um servidor HTTP que gerencia rotas 

  2. Uma função de renderização

Configurando o servidor HTTP

O servidor HTTP é bastante comum. Ele serve arquivos estáticos e analisa as rotas que lhe são enviadas (como página inicial):

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}/`);
});

O Sistema de Roteamento

Nosso servidor importa uma objeto de rotas . Esse objeto `routes` armazena uma tabela hash para as rotas. Cada rota possui um método que retorna HTML válido. Ele é usado assim:

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

No nosso projeto, teremos uma pasta “routes” que conterá um arquivo arquivo index.mjs .

Além do arquivo de índice, vamos criar uma página inicial pasta onde ficará a rota da página inicial. Ficará assim:

The routes folder consists of an index barrel file and a home-page folder.Routes folder structurepágina inicial vai se destacar index.mjs :

The template index file exposes a `template` method that is used as the rendering function in the server.Homepage template index fileEsse objeto HomePage também será exportado a partir do arquivo arquivo routes/index.mjs :

export * de ‘./home-page/index.mjs’

Agora só precisamos implementar getHomePageTemplate em home-page.template.mjs:

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

Por fim, precisamos usar a rota em nosso servidor; portanto, vamos alterar o arquivo principal index.mjs:

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}/`);
});

Aqui, importamos as rotas (linha 5) e as utilizamos ao retornarmos text/html (linha 41).

Os resultados são impressionantes!

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

Vamos adicionar um modelo melhor

Esse modelo é bem sem graça… vamos dar um toque mais ousado. Para isso, vou usar o sistema de design . Os são componentes web puros. Vamos usá-los para dar um toque especial ao nosso modelo e renderizá-los no lado do servidor. 

Na página do componente de botão página do componente de botões, podemos citar o exemplo de aparência, que apresenta quatro botões diferentes:

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

Podemos substituir nosso modelo em home-page.template.mjspelo código de exemplo:

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>
    `;
}

E o resultado aqui é:

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

Uma página em branco ao lado de um corpo não tão vazio assim. Onde estão os componentes do exemplo de código?

Eles não carregam porque exigem que a gente carregue os arquivos JS e CSS.

Como carregar CSS e JavaScript?

Essa é geralmente uma pergunta trivial, mas como isso é feito em um servidor SSR?

Vamos optar pela maneira mais simples de fazer isso, que é usar uma CDN. Você pode importar componentes do Vivid usando esta convenção:

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

Com isso, podemos importar nosso código para o modelo:

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>
    `;
}

Se testarmos nosso cliente, veremos nossos componentes. Bem… mais ou menos:

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

Uma coisa que precisamos fazer para tornar os componentes do Vivid funcionem é adicionar o classe vvd-root ao elemento que os envolve (geralmente o body…).

Vamos definir um wrapper para o nosso modelo:

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>
    `;
}

Eis o resultado:

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

Então, os botões funcionam, mas… Você consegue identificar o problema?

O HTML é carregado — como podemos ver pela div que o envolve — e, em seguida, os botões são renderizados assim que o JS entra em ação, causando uma grande mudança no layout. Imagine isso acontecendo em um aplicativo maior, com muito mais componentes. 

Isso não é bom…

Como podemos evitar esse efeito de flash? Vamos renderizar os componentes no servidor!

Criação da função de renderização

Em vez de carregar o JS no lado do cliente, podemos renderizar os componentes no servidor e enviar um HTML completo. Portanto, precisamos encontrar uma maneira de renderizar nossos componentes no servidor como se estivessem em um navegador.

Cada framework possui um método de renderização diferente. 

Os componentes da Web são renderizados nativamente pelo navegador. Os componentes da Web também introduzem o conceito de shadow DOM. Em essência, o shadow DOM é um fragmento de documento no qual é possível adicionar HTML e CSS. Para isso, o navegador cria uma shadow root dentro do nosso componente:

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

Tudo o que está fora da raiz de sombra fica “na luz”, enquanto o restante fica na sombra. A vantagem de um shadowDOM é que ele encapsula os estilos. Os estilos dentro dele não afetam nada do lado de fora e (quase completamente) o contrário também se aplica.

Isso significa que, se pegarmos nosso modelo e definirmos como o `innerHTML` de uma div, devemos obter os componentes renderizados. Vamos testar isso no navegador:

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);

Se você colar esse código no seu navegador, deverá ver a div vermelha sem o botão, pois o JS não seria importado.

No entanto, se você tivesse importado o JS antes, teria funcionado:

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);

Conforme testado no Google.com:

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

A questão é que os elementos `document`, `body` e `HTML` não existem nativamente no lado do servidor. Então…

Como é possível renderizar HTML em um servidor?

Ótima pergunta! Que bom que você perguntou.

Existem várias maneiras de renderizar HTML no lado do servidor. 

Como o Vivid testa seus componentes usando o jsdom, sabemos que ele é capaz de renderizar nossos componentes sem um navegador.

Portanto, se criarmos um ambiente JSDOM em nosso servidor, poderemos usar nosso código para renderizar nossos componentes.

Isso é bem fácil, graças ao todo-poderoso NPM!

npm i global-jsdom/register jsdom vai adicionar jsdom — uma biblioteca que simula a API DOM do navegador no ambiente de execução do servidor, permitindo a criação de código de marcação como se estivesse no navegador. global-jsdom/register expõe a API do navegador globalmente para que possamos usá-la em nosso código. Assim, podemos renderizar nossos componentes no lado do servidor.

Vamos alterar um pouco o código do nosso modelo para usar isso:

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;
}

Importamos global-jsdom/register. Observe que importamos o @vonage/vivid/button no lado do servidor, para que o componente web seja renderizado como tal.

Definimos o jsdom renderizar nosso modelo simplesmente adicionando-o ao DOM e retornando seu innerHTML. Fica assim:

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, NÃO! Não há botões na visualização! Eles estão, de fato, no DOM. Também podemos ver o campo de entrada no DOM simplificado dentro de cada botão (ele está lá para resolver a associação com o formulário). 

A razão pela qual não vemos nada é que innerHTML não nos fornece o conteúdo do shadowDOM.

Então, o que poderíamos tentar fazer é obter o shadowDOM de cada componente assim:

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)));

O que nos dá esta interface de usuário:

The four buttons show, but they are deformed.The four buttons deformedUhu! Dá pra ver alguma coisa, mas… Não é exatamente a mesma coisa, né?

Ao analisar o HTML, podemos ver que o shadowroot está faltando neste link do gist.

Isso certamente pode afetar o estilo do componente, já que estamos perdendo o encapsulamento.

Como renderizar explicitamente o Shadow DOM sem JavaScript

Para esse fim, a especificação HTML agora define um atributo `shadowrootmode` para a tag template. Quando o navegador encontra <template shadowrootmode=”open”>, ele sabe que deve pegar tudo o que estiver dentro desse modelo e renderizá-lo dentro de um shadow DOM.

Com base nesse conhecimento, podemos alterar nosso código da seguinte forma:

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)));

Agora a exibição fica assim:

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

Foi exatamente assim que esperávamos que ficasse! Viva!

Se você der uma olhada no DOM agora, ele fica assim:

The four buttons in the DOM, including the shadow-rootThe buttons' HTML snippet from the Elements PanelNão é demais? Renderizamos nossos componentes web no lado do servidor e evitamos a mudança de layout no nosso aplicativo!

Vamos tentar dar um toque especial ao nosso aplicativo.

Manuseio de componentes complexos

O botão que usamos era bem simples. Vamos tentar usar um botão com um ícone dentro:

<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>

E fica assim no navegador:

The buttons appear, but internal icon elements are not renderedThe buttons appear, but internal icon elements are not renderedAlgo mudou, mas não conseguimos ver nenhum ícone…

O código HTML dentro do botão fica assim:

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 innerHTMLPodemos ver o ícone vwc bem ali no meio. Podemos identificar dois problemas aqui:

  1. O ícone não possui atributos — portanto, ele não sabe exatamente como se exibir. 

  2. O ícone não tem conteúdo — principalmente, não possui shadowroot.

Solução para o problema do ícone não receber atributos

Vamos resolver a questão mais simples. O ícone recebe seus atributos do componente botão. O modelo é renderizado de forma assíncrona. Isso significa que, depois que adicionarmos o div ao DOM, a atualização propriamente dita ocorre após mais uma iteração do ciclo de eventos. Portanto, precisamos aguardar a conclusão do processo de renderização.

Para isso, podemos definir a função modelo como assíncrona e aguardar um ciclo do loop de eventos:

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;
}

Observe que adicionamos a “mágica” await new Promise(res => setTimeout(res)); na linha 28. 

Agora, quando damos uma olhada no nosso HTML, vemos que o ícone recebe os seguintes atributos:

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

Carregando componentes internos

O segundo problema — que faz com que não vejamos os ícones — decorre do fato de não recebermos o código HTML dos componentes internos do shadowroot.

Uma maneira de resolver isso seria localizar todos os componentes da web de forma recursiva e exibi-los.

Para localizar os componentes, podemos percorrer a árvore DOM desta forma:

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));
}

Essa função recebe um elemento (supostamente nossa div envolvente) e localiza todos os componentes da web com shadowDOM.

Agora, tudo o que resta fazer é analisar cada um deles em nosso arquivo de modelo:

Vamos lá:

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;
}

Observe a alteração na linha 54 — estamos percorrendo todos os elementos com o shadow DOM na ordem inversa e acrescentando um shadowroot com seu innerHTML.

O resultado é impressionante:

The four buttons with their icons rendered correctlyThe four buttons with their icons rendered correctlySe você acompanhou tudo até aqui, parabéns! Você já entendeu o básico sobre SSR.

Será que podemos ajudar mais?

Nosso servidor SSR simples pode ser ainda mais otimizado. Por exemplo, alguns elementos, como o CSS e os arquivos SVG dos ícones, ainda dependem de servidores distantes. Podemos adicionar mais lógica ao nosso servidor SSR para buscá-los e incorporá-los diretamente no HTML retornado.

É possível obter mais ideias a partir de outros sistemas SSR. Por exemplo, os React Server Components possuem uma API dedicada para buscar e enviar solicitações ao servidor, que, por sua vez, solicita os dados e renderiza a visualização necessária.

O Qwik configura service workers para buscar o JS em segundo plano.

Todas as estruturas SSR já vêm com várias otimizações prontas, mas nem sempre atendem às suas necessidades; portanto, entender como elas funcionam é um bom ponto de partida para ampliá-las.

Resumo

Foi uma aventura e tanto, não foi?

Criar um mecanismo de SSR é, em essência, bastante simples, mas sempre pode ser aprimorado, ajustado e otimizado. É possível que você acabe tendo que manter uma base de código extensa apenas para lidar com o SSR.

Você pode optar por usar o Next.js (React), o Nuxt.js (Vue) ou alguma outra biblioteca de SSR. Se estiver usando componentes web, bibliotecas de SSR como o litssr ou o fastssr podem facilitar bastante o seu trabalho. 

Uma grande ressalva em relação a essas estruturas ou bibliotecas SSR é que elas funcionam apenas com a estrutura ou biblioteca para a qual foram projetadas. 

Nosso caso de uso consistia em criar um mecanismo de SSR para funcionar em conjunto com o Nuxt. Portanto, você pode considerar meu código um plugin de SSR. Espero que este artigo tenha dado uma ideia de como começar a criar um plugin como esse, caso surja a necessidade. 

O que todos os SSRs têm em comum é a existência de alguma função de renderização. Essa função é aplicada ao seu modelo e retorna uma string HTML que é enviada ao cliente (bem, exceto os React Server Components, que, na verdade, enviam um JSON — mas isso está fora do escopo deste artigo). 

Parte do código HTML é carregada posteriormente, depois que o JavaScript é carregado de forma assíncrona, sem bloquear a página. Neste artigo, aprendemos como fazer isso usando componentes da web e o Shadow DOM.

Não bloqueamos a página durante o carregamento do JavaScript, o que nos ajuda a exibir o conteúdo mais rapidamente, evitar grandes mudanças no layout e, possivelmente, melhorar nossa classificação em SEO.

Junte-se a nós na nossa Slack da Comunidade Vonage ou envie-nos uma mensagem no X, antes conhecido como Twitter, e diga-nos como podemos ajudar!

Compartilhar:

https://a.storyblok.com/f/270183/400x400/7bf76cb05c/yonatankra.png
Yonatan KraArquiteto de Software da Vonage

Yonatan já participou de alguns projetos incríveis tanto no meio acadêmico quanto no setor privado — desde C/C++, passando por Matlab, até PHP e JavaScript. Ex-CTO da Webiks e arquiteto de software na WalkMe. Atualmente, ele é arquiteto de software na Vonage e instrutor na Egghead.