Să aruncăm o privire mai atentă la caracteristica care vă permite să construiți universal aplicații cu Reacţiona.

Redarea pe partea de server – SSR de aici înainte – este capacitatea unui cadru front-end pentru a reda marcajul în timpul rulării pe un sistem back-end.

Se apelează aplicații care au capacitatea de a reda atât pe server cât și pe client aplicații universale.

De ce sa te deranjezi?

Pentru a înțelege de ce este nevoie de SSR, trebuie să înțelegem evoluția aplicațiilor web în ultimii 10 ani.

Acest lucru este strâns asociat cu creșterea Aplicație pentru o singură paginăSPA de aici înainte. SPA-urile oferă avantaje mari în ceea ce privește viteza și UX față de aplicațiile tradiționale redate de server.

Dar există o captură. Cererea inițială de server returnează în general un gol HTML fișier cu o grămadă de link-uri CSS și JavaScript (JS). Apoi, fișierele externe trebuie preluate pentru a reda marcajul relevant.

ad-banner

Aceasta înseamnă că utilizatorul va trebui să aștepte mai mult pentru redarea inițială. Aceasta înseamnă, de asemenea, că crawlerele pot interpreta pagina dvs. ca fiind goală.

Deci, ideea este să redați aplicația dvs. pe server inițial, apoi să valorificați capacitățile SPA-urilor pe client.

SSR + SPA = aplicație universală *

* Veți găsi termenul aplicație izomorfă în unele articole – este același lucru.

Acum utilizatorul nu trebuie să aștepte încărcarea JS și primește un in intregime redat HTML de îndată ce solicitarea inițială returnează un răspuns.

Imaginați-vă îmbunătățirea uriașă pentru utilizatorii care navighează pe rețele 3G lente. În loc să așteptați peste 20 de ani pentru încărcarea site-ului, veți primi conținut pe ecranul lor aproape instantaneu.

Demistificarea randarii de pe server in React

Și acum, toate cererile adresate serverului dvs. returnează HTML complet redat. Vești minunate pentru departamentul dvs. SEO!

Crawlerele acum va vedea site-ul dvs. ca orice alt site static de pe web și va index tot conținutul pe care îl redați pe server.

Deci, pentru a recapitula, cele două avantaje principale pe care le obținem din SSR sunt:

  • Timpuri mai rapide pentru randarea inițială a paginii
  • Pagini HTML complet indexabile

Înțelegerea SSR – un pas la rând

Să adoptăm o abordare iterativă pentru a ne construi exemplul SSR complet. Începem cu API-ul React pentru redarea serverului și vom adăuga ceva la mix la fiecare pas.

Puteți urmări acest depozit și etichetele definite acolo pentru fiecare pas.

Configurare de bază

Să începem cu începutul. Pentru a utiliza SSR, avem nevoie de un server! Vom folosi un simplu Expres aplicație care va reda aplicația noastră React.

import express from "express";
import path from "path";

import React from "react";
import { renderToString } from "react-dom/server";
import Layout from "./components/Layout";

const app = express();

app.use( express.static( path.resolve( __dirname, "../dist" ) ) );

app.get( "/*", ( req, res ) => {
    const jsx = ( <Layout /> );
    const reactDom = renderToString( jsx );

    res.writeHead( 200, { "Content-Type": "text/html" } );
    res.end( htmlTemplate( reactDom ) );
} );

app.listen( 2048 );

function htmlTemplate( reactDom ) {
    return `
        <!DOCTYPE html>
        <html>
        <head>
            <meta charset="utf-8">
            <title>React SSR</title>
        </head>
        
        <body>
            <div id="app">${ reactDom }</div>
            <script src="https://www.freecodecamp.org/news/demystifying-reacts-server-side-render-de335d408fe4/./app.bundle.js"></script>
        </body>
        </html>
    `;
}

Trebuie să spunem Express să ne servească fișierele statice din folderul nostru de ieșire – linia 10.

Creăm un traseu care gestionează toate cererile primite nestatice. Această rută va răspunde cu HTML redat.

Folosim renderToString – liniile 13-14 – pentru a converti JSX-ul nostru de pornire într-un string pe care îl inserăm în șablonul HTML.

Ca o notă, folosim aceleași pluginuri Babel pentru codul clientului și pentru codul serverului. Asa de JSX și Module ES lucrează în interior server.js.

Metoda corespunzătoare pentru client este acum ReactDOM.hydrate . Această funcție va utiliza aplicația React redată de server și va atașa gestionare de evenimente.

import ReactDOM from "react-dom";
import Layout from "./components/Layout";

const app = document.getElementById( "app" );
ReactDOM.hydrate( <Layout />, app );

Pentru a vedea exemplul complet, consultați basic eticheta în repertoriu.

Asta este! Tocmai ți-ai creat primul redat de server Aplicația React!

React Router

Trebuie să fim sinceri aici, aplicația nu face mare lucru. Deci, să adăugăm câteva rute și să vedem cum gestionăm partea serverului.

import { Link, Switch, Route } from "react-router-dom";
import Home from "./Home";
import About from "./About";
import Contact from "./Contact";

export default class Layout extends React.Component {
    /* ... */

    render() {
        return (
            <div>
                <h1>{ this.state.title }</h1>
                <div>
                    <Link to="/">Home</Link>
                    <Link to="/about">About</Link>
                    <Link to="/contact">Contact</Link>
                </div>
                <Switch>
                    <Route path="/" exact component={ Home } />
                    <Route path="/about" exact component={ About } />
                    <Route path="/contact" exact component={ Contact } />
                </Switch>
            </div>
        );
    }
}

Layout componentă redă acum mai multe rute pe client.

Trebuie să imităm configurarea routerului pe server. Mai jos puteți vedea principalele modificări care ar trebui făcute.

/* ... */
import { StaticRouter } from "react-router-dom";
/* ... */

app.get( "/*", ( req, res ) => {
    const context = { };
    const jsx = (
        <StaticRouter context={ context } location={ req.url }>
            <Layout />
        </StaticRouter>
    );
    const reactDom = renderToString( jsx );

    res.writeHead( 200, { "Content-Type": "text/html" } );
    res.end( htmlTemplate( reactDom ) );
} );

/* ... */

Pe server, trebuie să înfășurăm aplicația React în StaticRouter componentă și să furnizeze location.

Ca o notă laterală, context este folosit pentru urmărirea redirecționărilor potențiale în timpul redării React DOM. Acest lucru trebuie tratat cu un Răspuns 3XX de pe server.

Exemplul complet poate fi văzut pe router eticheta în același depozit.

Redux

Acum că avem capacități de rutare, să ne integrăm Redux.

În scenariul simplu, avem nevoie de Redux pentru a gestiona gestionarea stării pe client. Dar ce se întâmplă dacă trebuie să redăm părți din DOM bazate pe acea stare? Este logic să inițializăm Redux pe server.

Dacă aplicația dvs. este expediere acțiuni pe Server, trebuie captură statului și trimiteți-l prin fir împreună cu codul HTML. Pe client, alimentăm starea inițială în Redux.

Să vedem mai întâi serverul:

/* ... */
import { Provider as ReduxProvider } from "react-redux";
/* ... */

app.get( "/*", ( req, res ) => {
    const context = { };
    const store = createStore( );

    store.dispatch( initializeSession( ) );

    const jsx = (
        <ReduxProvider store={ store }>
            <StaticRouter context={ context } location={ req.url }>
                <Layout />
            </StaticRouter>
        </ReduxProvider>
    );
    const reactDom = renderToString( jsx );

    const reduxState = store.getState( );

    res.writeHead( 200, { "Content-Type": "text/html" } );
    res.end( htmlTemplate( reactDom, reduxState ) );
} );

app.listen( 2048 );

function htmlTemplate( reactDom, reduxState ) {
    return `
        /* ... */
        
        <div id="app">${ reactDom }</div>
        <script>
            window.REDUX_DATA = ${ JSON.stringify( reduxState ) }
        </script>
        <script src="https://www.freecodecamp.org/news/demystifying-reacts-server-side-render-de335d408fe4/./app.bundle.js"></script>
        
        /* ... */
    `;
}

Pare urât, dar trebuie să trimitem starea JSON completă împreună cu HTML-ul nostru.

Apoi ne uităm la client:

import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter as Router } from "react-router-dom";
import { Provider as ReduxProvider } from "react-redux";

import Layout from "./components/Layout";
import createStore from "./store";

const store = createStore( window.REDUX_DATA );

const jsx = (
    <ReduxProvider store={ store }>
        <Router>
            <Layout />
        </Router>
    </ReduxProvider>
);

const app = document.getElementById( "app" );
ReactDOM.hydrate( jsx, app );

Observați că sunăm createStore de două ori, mai întâi pe server, apoi pe client. Cu toate acestea, pe client, inițializăm starea cu orice stare a fost salvată pe server. Acest proces este similar cu hidratarea DOM.

Exemplul complet poate fi văzut pe redux eticheta în același depozit.

Obțineți date

Piesa finală a puzzle-ului este încărcarea datelor. Aici devine un pic mai complicat. Să presupunem că avem un API care servește date JSON.

În baza noastră de cod, aduc toate evenimentele din sezonul de Formula 1 2018 dintr-un API public. Să presupunem că vrem să afișăm toate evenimentele pe Acasă pagină.

Putem apela API-ul nostru numai de la client după ce aplicația React este montată și totul este redat. Dar acest lucru va avea un impact negativ asupra UX, arătând potențial un spinner sau un încărcător înainte ca utilizatorul să vadă conținut relevant.

Avem deja Redux, ca o modalitate de a stoca date pe server și de a le trimite clientului.

Ce se întâmplă dacă facem apeluri API pe server, stocăm rezultatele în Redux și apoi redăm codul HTML complet cu datele relevante pentru client?

Dar cum putem ști ce apeluri trebuie efectuate?

În primul rând, avem nevoie de un mod diferit de declarare a rutelor. Deci trecem la așa-numitul fișier de configurare a rutelor.

export default [
    {
        path: "/",
        component: Home,
        exact: true,
    },
    {
        path: "/about",
        component: About,
        exact: true,
    },
    {
        path: "/contact",
        component: Contact,
        exact: true,
    },
    {
        path: "/secret",
        component: Secret,
        exact: true,
    },
];

Și declarăm în mod static cerințele de date pentru fiecare componentă.

/* ... */
import { fetchData } from "../store";

class Home extends React.Component {
    /* ... */

    render( ) {
        const { circuits } = this.props;

        return (
            /* ... */
        );
    }
}
Home.serverFetch = fetchData; // static declaration of data requirements

/* ... */

Ține minte că serverFetch este alcătuit, puteți folosi orice sună mai bine pentru dvs.

Ca o notă aici, fetchData este o Acțiune redux, returnând o promisiune când este expediată.

Pe server, putem folosi o funcție specială de la react-router, numit matchRoute.

/* ... */
import { StaticRouter, matchPath } from "react-router-dom";
import routes from "./routes";

/* ... */

app.get( "/*", ( req, res ) => {
    /* ... */

    const dataRequirements =
        routes
            .filter( route => matchPath( req.url, route ) ) // filter matching paths
            .map( route => route.component ) // map to components
            .filter( comp => comp.serverFetch ) // check if components have data requirement
            .map( comp => store.dispatch( comp.serverFetch( ) ) ); // dispatch data requirement

    Promise.all( dataRequirements ).then( ( ) => {
        const jsx = (
            <ReduxProvider store={ store }>
                <StaticRouter context={ context } location={ req.url }>
                    <Layout />
                </StaticRouter>
            </ReduxProvider>
        );
        const reactDom = renderToString( jsx );

        const reduxState = store.getState( );

        res.writeHead( 200, { "Content-Type": "text/html" } );
        res.end( htmlTemplate( reactDom, reduxState ) );
    } );
} );

/* ... */

Cu aceasta, obținem o listă de componente care vor fi montate atunci când React este redat în șir pe adresa URL curentă.

Adunăm cerințele de date și așteptăm să revină toate apelurile API. În cele din urmă, reluăm redarea serverului, dar cu datele deja disponibile în Redux.

Exemplul complet poate fi văzut pe fetch-data eticheta în același depozit.

Probabil că observați că acest lucru vine cu o penalizare de performanță, deoarece amânăm randarea până când datele sunt preluate.

Aici începeți compararea valorilor și faceți tot posibilul pentru a înțelege care sunt apelurile esențiale și care nu. De exemplu, preluarea produselor pentru o aplicație de comerț electronic ar putea fi crucială, dar prețurile și filtrele din bara laterală pot fi încărcate leneș.

Cască

Ca bonus, să ne uităm la SEO. În timp ce lucrați cu React, vă recomandăm să setați valori diferite în <heanunț> etichetă. De exemplu, poate doriți să vedețiT-ul title, metA etichete, cheiecuvinte și așa mai departe.

Rețineți că <heanunțul> eticheta nu face parte, în mod normal, din aplicația dvs. React!

cască de reacție te-ai ocupat de acest scenariu. Și are un mare sprijin pentru SSR.

import React from "react";
import Helmet from "react-helmet";

const Contact = () => (
    <div>
        <h2>This is the contact page</h2>
        <Helmet>
            <title>Contact Page</title>
            <meta name="description" content="This is a proof of concept for React SSR" />
        </Helmet>
    </div>
);

export default Contact;

Tu doar adaugi head date oriunde în arborele componentelor. Acest lucru vă oferă suport pentru schimbarea valorilor în afara aplicației React montate pe client.

Și acum adăugăm suportul pentru SSR:

/* ... */
import Helmet from "react-helmet";
/* ... */

app.get( "/*", ( req, res ) => {
    /* ... */
        const jsx = (
            <ReduxProvider store={ store }>
                <StaticRouter context={ context } location={ req.url }>
                    <Layout />
                </StaticRouter>
            </ReduxProvider>
        );
        const reactDom = renderToString( jsx );
        const reduxState = store.getState( );
        const helmetData = Helmet.renderStatic( );

        res.writeHead( 200, { "Content-Type": "text/html" } );
        res.end( htmlTemplate( reactDom, reduxState, helmetData ) );
    } );
} );

app.listen( 2048 );

function htmlTemplate( reactDom, reduxState, helmetData ) {
    return `
        <!DOCTYPE html>
        <html>
        <head>
            <meta charset="utf-8">
            ${ helmetData.title.toString( ) }
            ${ helmetData.meta.toString( ) }
            <title>React SSR</title>
        </head>
        
        /* ... */
    `;
}

Și acum avem un exemplu complet funcțional React SSR!

Am pornit de la o redare simplă a HTML în contextul unui Expres aplicație. Am adăugat treptat rutare, gestionarea stării și preluarea datelor. În cele din urmă, am gestionat modificările în afara domeniului de aplicare al aplicației React.

Baza de cod finală este activată master pe același depozit asta a fost menționat anterior.

Concluzie

După cum ați văzut, SSR nu este o problemă mare, dar poate deveni complexă. Și este mult mai ușor de înțeles dacă îți construiești nevoile pas cu pas.

Merită să adăugați SSR la aplicația dvs.? Ca întotdeauna, depinde. Este obligatoriu dacă site-ul dvs. este public și accesibil pentru sute de mii de utilizatori. Dar dacă construiți o aplicație de tip instrument / tablou de bord, s-ar putea să nu merite efortul.

Cu toate acestea, valorificarea puterii aplicațiilor universale este un pas înainte pentru comunitatea front-end.

Folosiți o abordare similară pentru SSR? Sau crezi că mi-a fost dor de ceva? Lăsați-mi un mesaj mai jos sau mai jos Stare de nervozitate.

Dacă vi s-a părut util acest articol, ajutați-mă să îl împărtășesc comunității!