Afmystificering af rendering på serversiden i React

Lad os se nærmere på den funktion, der giver dig mulighed for at oprette universelle applikationer med React .

Server-Side Rendering - SSR herfra - er en front-end-rammes evne til at gengive markering, mens du kører på et back-end-system .

Applikationer, der har mulighed for at gengive både på serveren og på klienten kaldes universelle apps .

Hvorfor bekymre sig?

For at forstå hvorfor SSR er nødvendig, er vi nødt til at forstå udviklingen af ​​webapplikationer i de sidste 10 år.

Dette er tæt koblet med stigningen af applikationen Single Page - SPA herfra . SPA'er tilbyder store fordele i hastighed og UX i forhold til traditionelle servergengivne apps.

Men der er en fangst. Den oprindelige serveranmodning returnerer normalt en tom HTML- fil med en masse CSS- og JavaScript-links (JS). Derefter skal de eksterne filer hentes for at gøre relevant markup.

Dette betyder, at brugeren bliver nødt til at vente længere på den første gengivelse . Dette betyder også, at webcrawlere kan fortolke din side som tom.

Så ideen er først at gengive din app på serveren og derefter udnytte funktionerne i SPA'er på klienten.

SSR + SPA = Universal App *

* Du finder udtrykket isomorf app i nogle artikler - det er det samme.

Nu behøver brugeren ikke vente på, at din JS indlæses og får en fuldt gengivet HTML, så snart den oprindelige anmodning returnerer et svar.

Forestil dig den enorme forbedring for brugere, der navigerer på langsomme 3G-netværk. I stedet for at vente på over 20'erne for at webstedet skal indlæses, får du indhold på deres skærm næsten øjeblikkeligt.

Og nu returnerer alle anmodninger til din server fuldt gengivet HTML. Gode ​​nyheder til din SEO-afdeling!

Crawlere vil nu se dit websted som ethvert andet statisk sted på internettet og vil indeksere alt det indhold, du gengiver på serveren.

Så for at opsummere er de to vigtigste fordele, vi får fra SSR:

  • Hurtigere tidspunkter for den første side gengives
  • Fuldt indekserbare HTML-sider

Forståelse af SSR - et trin ad gangen

Lad os tage en iterativ tilgang til at opbygge vores komplette SSR-eksempel. Vi starter med React's API til servergengivelse, og vi tilføjer noget til blandingen ved hvert trin.

Du kan følge dette lager og de tags, der er defineret der for hvert trin.

Grundlæggende opsætning

Første ting først. For at kunne bruge SSR har vi brug for en server! Vi bruger en simpel Express- app, der gengiver vores React-app.

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 = (  ); const reactDom = renderToString( jsx ); res.writeHead( 200, { "Content-Type": "text/html" } ); res.end( htmlTemplate( reactDom ) ); } ); app.listen( 2048 ); function htmlTemplate( reactDom ) { return `     React SSR ${ reactDom } `; }

Vi er nødt til at fortælle Express at betjene vores statiske filer fra vores outputmappe - linje 10.

Vi opretter en rute, der håndterer alle ikke-statiske indgående anmodninger. Denne rute svarer med den gengivne HTML.

Vi bruger renderToString- linje 13–14 - til at konvertere vores start-JSX til en, stringsom vi indsætter i HTML-skabelonen.

Som en note bruger vi de samme Babel-plugins til klientkoden og til serverkoden. Så JSX- og ES-moduler fungerer indeni server.js.

Den tilsvarende metode på klienten er nu ReactDOM.hydrate. Denne funktion bruger den servergengivne React-app og vedhæfter begivenhedshåndterere.

import ReactDOM from "react-dom"; import Layout from "./components/Layout"; const app = document.getElementById( "app" ); ReactDOM.hydrate( , app );

For at se det fulde eksempel skal du tjekke basictagget i arkivet.

Det er det! Du oprettede lige din første servergengivne React-app!

Reager router

Vi skal være ærlige her, appen gør ikke meget. Så lad os tilføje et par ruter og se, hvordan vi håndterer serverdelen.

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 ( 

{ this.state.title }

Home About Contact ); } }

Den Layoutkomponent nu gør flere ruter på klienten.

Vi skal efterligne routeropsætningen på serveren. Nedenfor kan du se de vigtigste ændringer, der skal udføres.

/* ... */ import { StaticRouter } from "react-router-dom"; /* ... */ app.get( "/*", ( req, res ) => { const context = { }; const jsx = (    ); const reactDom = renderToString( jsx ); res.writeHead( 200, { "Content-Type": "text/html" } ); res.end( htmlTemplate( reactDom ) ); } ); /* ... */

På serveren skal vi indpakke vores React-applikation i StaticRouterkomponenten og levere location.

Som en sidebemærkning contextbruges den til at spore potentielle omdirigeringer, mens du gengiver React DOM. Dette skal håndteres med et 3XX-svar fra serveren.

Det fulde eksempel kan ses på routertagget i det samme arkiv.

Redux

Nu hvor vi har rutefunktioner, lad os integrere Redux.

I det enkle scenario har vi brug for Redux til at håndtere statsadministration på klienten. Men hvad hvis vi har brug for at gengive dele af DOM baseret på denne tilstand? Det giver mening at initialisere Redux på serveren.

If your app is dispatchingactions on the server, it needs to capture the state and send it over the wire together with the HTML. On the client, we feed that initial state into Redux.

Let’s have a look at the server first:

/* ... */ import { Provider as ReduxProvider } from "react-redux"; /* ... */ app.get( "/*", ( req, res ) => { const context = { }; const store = createStore( ); store.dispatch( initializeSession( ) ); const jsx = (      ); 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 ` /* ... */ ${ reactDom } window.REDUX_DATA = ${ JSON.stringify( reduxState ) }   /* ... */ `; }

It looks ugly, but we need to send the full JSON state together with our HTML.

Then we look at the 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 = (      ); const app = document.getElementById( "app" ); ReactDOM.hydrate( jsx, app );

Notice that we call createStore twice, first on the server, then on the client. However, on the client, we initialize the state with whatever state was saved on the server. This process is similar to the DOM hydration.

The full example can be seen on the redux tag in the same repository.

Fetch Data

The final piece of the puzzle is loading data. This is where it gets a bit trickier. Let’s say we have an API serving JSON data.

In our codebase, I fetch all the events from the 2018 Formula 1 season from a public API. Let’s say we want to display all the events on the Home page.

We can call our API only from the client after the React app is mounted and everything is rendered. But this will have a bad impact on UX, potentially showing a spinner or a loader before the user sees relevant content.

We already have Redux, as a way of storing data on the server and sending it over to the client.

What if we make our API calls on the server, store the results in Redux, and then render the full HTML with the relevant data for the client?

But how can we know which calls need to be made?

First, we need a different way of declaring routes. So we switch to the so-called routes config file.

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

And we statically declare the data requirements on each component.

/* ... */ import { fetchData } from "../store"; class Home extends React.Component { /* ... */ render( ) { const { circuits } = this.props; return ( /* ... */ ); } } Home.serverFetch = fetchData; // static declaration of data requirements /* ... */

Keep in mind that serverFetch is made up, you can use whatever sounds better for you.

As a note here, fetchData is a Redux thunk action, returning a Promise when dispatched.

On the server, we can use a special function from react-router, called 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 = (      ); const reactDom = renderToString( jsx ); const reduxState = store.getState( ); res.writeHead( 200, { "Content-Type": "text/html" } ); res.end( htmlTemplate( reactDom, reduxState ) ); } ); } ); /* ... */

With this, we get a list of components that will be mounted when React is rendered to string on the current URL.

We gather the data requirements and we wait for all the API calls to return. Finally, we resume the server render, but with data already available in Redux.

The full example can be seen on the fetch-data tag in the same repository.

You probably notice that this comes with a performance penalty, because we’re delaying the render until the data is fetched.

This is where you start comparing metrics and do your best to understand which calls are essential and which aren’t. For example, fetching products for an e-commerce app might be crucial, but prices and sidebar filters can be lazy loaded.

Helmet

As a bonus, let’s look at SEO. While working with React, you may want to set different values in your ad> tag. For example, you may want to se t the t itle, meta tags, keywords, and so on.

Keep in mind that the ad> tag is normally not part of your React app!

react-helmet has you covered in this scenario. And it has great support for SSR.

import React from "react"; import Helmet from "react-helmet"; const Contact = () => ( 

This is the contact page

Contact Page ); export default Contact;

You just add your head data anywhere in your component tree. This gives you support for changing values outside the mounted React app on the client.

And now we add the support for SSR:

/* ... */ import Helmet from "react-helmet"; /* ... */ app.get( "/*", ( req, res ) => { /* ... */ const jsx = ( ); 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 ` ${ helmetData.title.toString( ) } ${ helmetData.meta.toString( ) } React SSR /* ... */ `; }

And now we have a fully functional React SSR example!

We started from a simple render of HTML in the context of an Express app. We gradually added routing, state management, and data fetching. Finally, we handled changes outside the scope of the React application.

The final codebase is on master on the same repository that was mentioned before.

Conclusion

As you’ve seen, SSR is not a big deal, but it can get complex. And it’s much easier to grasp if you build your needs step by step.

Is it worth adding SSR to your application? As always, it depends. It’s a must if your website is public and accessible to hundreds of thousands of users. But if you’re building a tool/dashboard-like application it might not be worth the effort.

However, leveraging the power of universal apps is a step forward for the front-end community.

Do you use a similar approach for SSR? Or you think I missed something? Drop me a message below or on Twitter.

If you found this article useful, help me share it with the community!