react 服务器端渲染_在React中解密服务器端渲染

react 服务器端渲染

Let’s have a closer look at the feature that allows you to build universal applications with React.

让我们仔细看一下允许您使用React构建通用 应用程序的功能。

Server-Side Rendering — SSR from here on — is the ability of a front-end framework to render markup while running on a back-end system.

服务器端渲染(这里称为SSR)是前端框架后端系统上运行时呈现标记的功能

Applications that have the ability to render both on the server and on the client are called universal apps.

能够在服务器和客户端上进行渲染的应用程序称为通用应用程序

何必呢? (Why bother?)

In order to understand why SSR is needed, we need to understand the evolution of web applications in the past 10 years.

为了理解为什么需要SSR,我们需要了解Web应用程序在过去10年中的发展。

This is tightly coupled with the rise of the Single Page ApplicationSPA from here on. SPAs offer great advantages in speed and UX over traditional server-rendered apps.

这是紧密耦合与崛起单页申请从这里SPA - 与传统的服务器渲染应用程序相比,SPA在速度和用户体验方面具有巨大优势。

But there is a catch. The initial server request is generally returning an empty HTML file with a bunch of CSS and JavaScript (JS) links. Then the external files need to be fetched in order to render relevant markup.

但是有一个问题! 最初的服务器请求通常返回带有一组CSS和JavaScript(JS)链接的 HTML文件。 然后需要提取外部文件以呈现相关标记。

This means that the user will have to wait longer for the initial render. This also means that crawlers may interpret your page as empty.

这意味着用户将不得不等待更长的时间才能进行初始渲染 。 这也意味着搜寻器可能会将您的页面解释为空白。

So the idea is to render your app on the server initially, then to leverage the capabilities of SPAs on the client.

因此,其想法是首先在服务器上呈现您的应用程序,然后在客户端上利用SPA的功能。

SSR + SPA = Universal App*

SSR + SPA =通用应用*

*You will find the term isomorphic app in some articles — it’s the same thing.

*您会在某些文章中找到术语“ 同构应用” ,这是同一回事。

Now the user does not have to wait for your JS to load and gets a fully rendered HTML as soon as the initial request returns a response.

现在,用户不必等待您的JS加载并在初始请求返回响应后立即获得完全 呈现的 HTML

Imagine the huge improvement for users navigating on slow 3G networks. Rather than waiting for over 20s for the website to load, you get content on their screen almost instantly.

想象一下,使用慢速3G网络的用户所获得的巨大进步。 您无需等待20多个秒即可加载网站,而是几乎可以立即在其屏幕上获取内容。

And now, all the requests that are made to your server return fully rendered HTML. Great news for your SEO department!

现在,对服务器的所有请求都将返回完全呈现HTML。 对于您的SEO部门来说是个好消息!

Crawlers will now see your website as any other static site on the web and will index all the content you render on the server.

抓取工具现在会将您的网站视为网络上的任何其他静态网站,并将您在服务器上呈现的所有内容编入索引

So to recap, the two main benefits we get from SSR are:

综上所述,我们从SSR获得的两个主要好处是:

  • Faster times for the initial page render

    初始页面渲染的时间更快
  • Fully indexable HTML pages

    完全可索引HTML页面

了解SSR-一次一步 (Understanding SSR — one step at a time)

Let’s take an iterative approach to build our complete SSR example. We start with React’s API for server rendering and we’ll add something to the mix at each step.

让我们采取一种迭代方法来构建完整的SSR示例。 我们从用于服务器渲染的React API开始,然后在每个步骤中添加一些东西。

You can follow this repository and the tags defined there for each step.

您可以遵循该存储库以及为每个步骤定义的标签。

基本设定 (Basic Setup)

First things first. In order to use SSR, we need a server! We’ll use a simple Express app that will render our React app.

首先是第一件事。 为了使用SSR,我们需要一台服务器! 我们将使用一个简单的Express应用程序来渲染我们的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="./app.bundle.js"></script>
        </body>
        </html>
    `;
}

We need to tell Express to serve our static files from our output folder — line 10.

我们需要告诉Express从输出文件夹(第10行)提供静态文件。

We create a route that handles all non-static incoming requests. This route will respond with the rendered HTML.

我们创建一个处理所有非静态传入请求的路由。 该路由将使用渲染HTML进行响应。

We use renderToString — lines 13–14 — to convert our starting JSX into a string that we insert in the HTML template.

我们使用renderToString (第13-14行)将起始JSX转换为要插入HTML模板的string

As a note, we’re using the same Babel plugins for the client code and for the server code. So JSX and ES Modules work inside server.js.

注意,我们在客户端代码和服务器代码中使用了相同的Babel插件。 因此, JSXES模块server.js工作。

The corresponding method on the client is now ReactDOM.hydrate . This function will use the server-rendered React app and will attach event handlers.

客户端上的相应方法现在是ReactDOM.hydrate 。 该函数将使用服务器渲染的React应用程序,并将附加事件处理程序。

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

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

To see the full example, check out the basic tag in the repository.

要查看完整的示例,请在存储库中检出basic标签。

That’s it! You just created your first server-rendered React app!

而已! 您刚刚创建了第一个服务器渲染的 React应用!

React路由器 (React Router)

We have to be honest here, the app doesn’t do much. So let’s add a few routes and see how we handle the server part.

我们必须在这里说实话,该应用程序没有做太多事情。 因此,让我们添加一些路由,看看我们如何处理服务器部分。

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

The Layout component now renders multiple routes on the client.

Layout组件现在在客户端上呈现多个路由。

We need to mimic the Router setup on the server. Below you can see the main changes that should be done.

我们需要模拟服务器上的路由器设置。 您可以在下面看到应完成的主要更改。

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

/* ... */

On the server, we need to wrap our React application in the StaticRouter component and provide the location.

在服务器上,我们需要将React应用程序包装在StaticRouter组件中并提供location

As a side note, the context is used for tracking potential redirects while rendering the React DOM. This needs to be handled with a 3XX response from the server.

附带说明一下,该context用于在呈现React DOM时跟踪潜在的重定向。 这需要通过服务器的3XX响应来处理。

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

完整的示例可以在同一存储库router标记中看到。

Redux (Redux)

Now that we have routing capabilities, let’s integrate Redux.

现在我们有了路由功能,让我们集成Redux

In the simple scenario, we need Redux to handle state management on the client. But what if we need to render parts of the DOM based on that state? It makes sense to initialize Redux on the server.

在简单的场景中,我们需要Redux来处理客户端上的状态管理。 但是,如果我们需要根据该状态呈现DOM的一部分,该怎么办? 在服务器上初始化Redux是有意义的。

If your app is dispatching actions 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.

如果您的应用程序正在服务器调度 操作 ,则它需要捕获状态并将其与HTML一起通过网络发送。 在客户端上,我们将该初始状态提供给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 = (
        <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="./app.bundle.js"></script>
        
        /* ... */
    `;
}

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

看起来很难看,但是我们需要将完整的JSON状态与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 = (
    <ReduxProvider store={ store }>
        <Router>
            <Layout />
        </Router>
    </ReduxProvider>
);

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.

请注意,我们两次调用createStore ,首先在服务器上,然后在客户端上。 但是,在客户端上,我们使用保存在服务器上的任何状态来初始化状态。 此过程类似于DOM水合。

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

完整的示例可以在同一存储库redux标签上看到。

取得资料 (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.

难题的最后一部分是加载数据。 这是有点棘手的地方。 假设我们有一个提供JSON数据的API。

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.

在我们的代码库中,我从公共API获取2018年Formula 1赛季的所有事件。 假设我们要在主页上显示所有事件。

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.

在安装React应用程序并渲染所有内容之后,我们只能从客户端调用API。 但这将对UX造成严重影响,可能在用户看到相关内容之前显示微调器或装载器。

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

我们已经有了Redux,作为将数据存储在服务器上并将其发送到客户端的一种方式。

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?

如果我们在服务器上进行API调用,将结果存储在Redux中,然后呈现完整HTML以及客户端的相关数据,该怎么办?

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.

请记住, serverFetch已组成,您可以使用听起来更合适的东西。

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

这里要注意的是, fetchDataRedux的thunk动作 ,在分派时返回Promise。

On the server, we can use a special function from react-router, called matchRoute.

在服务器上,我们可以使用react-router一个特殊功能,称为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 ) );
    } );
} );

/* ... */

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

这样,我们获得了当React在当前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.

我们收集数据需求,然后等待所有API调用返回。 最后,我们恢复服务器渲染,但是Redux中已有数据。

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

完整的示例可以在同一存储库中fetch-data标签上看到。

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 <head> tag. For example, you may want to set the title, meta tags, keywords, and so on.

作为奖励,让我们看一下SEO。 使用React时,您可能需要在<he ad>标签中设置不同的值。 例如,您可能希望SE t为 书名,遇到了一个标签,关键词 ,等等。

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

请记住, <he ad>标签通常不属于您的React应用!

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

在这种情况下,您可以覆盖react-helmet 。 并且它对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;

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.

您只需将head数据添加到组件树中的任何位置。 这为您提供了在客户端上已安装的React应用程序外部更改值的支持。

And now we add the support for SSR:

现在,我们添加了对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>
        
        /* ... */
    `;
}

And now we have a fully functional React SSR example!

现在我们有了一个功能齐全的React SSR示例!

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.

我们从Express应用程序上下文中的简单HTML渲染开始。 我们逐渐添加了路由,状态管理和数据获取。 最后,我们处理了React应用程序范围之外的更改。

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

最终的代码库位于前面提到的同一存储库的 master上。

结论 (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.

如您所见,SSR没什么大不了的,但是它可能变得复杂。 如果您逐步建立自己的需求,则更容易掌握。

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.

是否值得在您的应用程序中添加SSR? 与往常一样,这取决于。 如果您的网站是公开的并且可以供成千上万的用户访问,则这是必须的。 但是,如果您要构建类似工具/仪表板的应用程序,那么可能就不值得付出努力。

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.

您是否对SSR使用类似的方法? 还是您认为我错过了什么? 在下方或Twitter上给我留言。

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

如果您发现本文有用,请帮助我与社区分享!

翻译自: https://www.freecodecamp.org/news/demystifying-reacts-server-side-render-de335d408fe4/

react 服务器端渲染

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值