[译]利用React Router4实现的服务端直出渲染(SSR)

我们已经熟悉React 服务端渲染(SSR)的基本步骤,现在让我们更进一步利用 React RouterV4 实现客户端和服务端的同构。毕竟大多数的应用都需要用到web前端路由器,所以要让SSR能够正常的运行,了解路由器的设置是十分有必要的

基本步骤

路由器配置

前言已经简单的介绍了React SSR,首先我们需要添加ReactRouter4到我们的项目中


$ yarn add react-router-dom

# or, using npm
$ npm install react-router-dom

接着我们会描述一个简单的场景,其中组件是静态的且不需要去获取外部数据。我们会在这个基础之上去了解如何完成取到数据的服务端渲染。

在客户端,我们只需像以前一样将我们的的App组件通过ReactRouter的BrowserRouter来包起来。

src/index.js


import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter } from 'react-router-dom';

import App from './App';

ReactDOM.hydrate(
  <BrowserRouter>
    <App />
  </BrowserRouter>,
  document.getElementById('root')
);

在服务端我们将采取类似的方式,但是改为使用无状态的 StaticRouter

server/index.js


app.get('/*', (req, res) => {
  const context = {};
  const app = ReactDOMServer.renderToString(
    <StaticRouter location={req.url} context={context}>
      <App />
    </StaticRouter>
  );

  const indexFile = path.resolve('./build/index.html');
  fs.readFile(indexFile, 'utf8', (err, data) => {
    if (err) {
      console.error('Something went wrong:', err);
      return res.status(500).send('Oops, better luck next time!');
    }

    return res.send(
      data.replace('<div id="root"></div>', `<div id="root">${app}</div>`)
    );
  });
});

app.listen(PORT, () => {
  console.log(`?? Server is listening on port ${PORT}`);
});

StaticRouter组件需要 locationcontext属性。我们传递当前的url(Express req.url)给location,设置一个空对象给context。context对象用于存储特定的路由信息,这个信息将会以staticContext的形式传递给组件



运行一下程序看看结果是否我们所预期的,我们给App组件添加一些路由信息

src/App.js


import React from 'react';
import { Route, Switch, NavLink } from 'react-router-dom';
import Home from './Home';
import Posts from './Posts';
import Todos from './Todos';
import NotFound from './NotFound';

export default props => {
  return (
    <div>
      <ul>
        <li>
          <NavLink to="/">Home</NavLink>
        </li>
        <li>
          <NavLink to="/todos">Todos</NavLink>
        </li>
        <li>
          <NavLink to="/posts">Posts</NavLink>
        </li>
      </ul>

      <Switch>
        <Route
          exact
          path="/"
          render={props => <Home name="Alligator.io" {...props} />}
        />
        <Route path="/todos" component={Todos} />
        <Route path="/posts" component={Posts} />
        <Route component={NotFound} />
      </Switch>
    </div>
  );
};

现在如果你运行一下程序($ yarn run dev),我们的路由在服务端被渲染,这是我们所预期的。

利用404状态来处理未找到资源的网络请求

我们做一些改进,当渲染NotFound组件时让服务端使用404HTTP状态码来响应。首先我们将一些信息放到NotFound组件的staticContext


import React from 'react';

export default ({ staticContext = {} }) => {
  staticContext.status = 404;
  return <h1>Oops, nothing here!</h1>;
};

然后在服务端,我们可以检查context对象的status属性是否是404,如果是404,则以404状态响应服务端请求。

server/index.js


// ...

app.get('/*', (req, res) => {
  const context = {};
  const app = ReactDOMServer.renderToString(
    <StaticRouter location={req.url} context={context}>
      <App />
    </StaticRouter>
  );

  const indexFile = path.resolve('./build/index.html');
  fs.readFile(indexFile, 'utf8', (err, data) => {
    if (err) {
      console.error('Something went wrong:', err);
      return res.status(500).send('Oops, better luck next time!');
    }

    if (context.status === 404) {
      res.status(404);
    }

    return res.send(
      data.replace('<div id="root"></div>', `<div id="root">${app}</div>`)
    );
  });
});

// ...

重定向

补充一下,我们可以做一些类似重定向的工作。如果我们有使用Redirect组件,ReactRouter会自动添加重定向的url到context对象的属性上。

server/index.js (部分)


if (context.url) {
  return res.redirect(301, context.url);
}

读取数据

有时候我们的服务端渲染应用需要数据呈现,我们需要用一种静态的方式来定义我们的路由而不是只涉及到客户端的动态的方式。失去定义动态路由的定义是服务端渲染最适合所需要的应用的原因(译者注:这句话的意思应该是SSR不允许路由是动态定义的)。



我们将使用fetch在客户端和服务端,我们增加isomorphic-fetch到我们的项目。同时我们也增加serialize-javascript这个包,它可以方便的序列化服务器上获取到的数据。


$ yarn add isomorphic-fetch serialize-javascript

# or, using npm:
$ npm install isomorphic-fetch serialize-javascript


我们定义我们的路由信息为一个静态数组在routes.js文件里

src/routes.js


import App from './App';
import Home from './Home';
import Posts from './Posts';
import Todos from './Todos';
import NotFound from './NotFound';

import loadData from './helpers/loadData';

const Routes = [
  {
    path: '/',
    exact: true,
    component: Home
  },
  {
    path: '/posts',
    component: Posts,
    loadData: () => loadData('posts')
  },
  {
    path: '/todos',
    component: Todos,
    loadData: () => loadData('todos')
  },
  {
    component: NotFound
  }
];

export default Routes;

有一些路由配置现在有一个叫loadData的键,它是一个调用loadData函数的函数。这个是我们的loadData函数的实现

helpers/loadData.js


import 'isomorphic-fetch';

export default resourceType => {
  return fetch(`https://jsonplaceholder.typicode.com/${resourceType}`)
    .then(res => {
      return res.json();
    })
    .then(data => {
      // only keep 10 first results
      return data.filter((_, idx) => idx < 10);
    });
};

我们简单的使用fetch来从REST API 获取数据

在服务端我们将使用ReactRouter的matchPath去寻找当前url所匹配的路由配置并判断它有没有loadData属性。如果是这样,我们调用loadData去获取数据并把数据放到全局window对象中在服务器的响应中

server/index.js


import React from 'react';
import express from 'express';
import ReactDOMServer from 'react-dom/server';
import path from 'path';
import fs from 'fs';
import serialize from 'serialize-javascript';
import { StaticRouter, matchPath } from 'react-router-dom';
import Routes from '../src/routes';

import App from '../src/App';

const PORT = process.env.PORT || 3006;
const app = express();

app.use(express.static('./build'));

app.get('/*', (req, res) => {
  const currentRoute =
    Routes.find(route => matchPath(req.url, route)) || {};
  let promise;

  if (currentRoute.loadData) {
    promise = currentRoute.loadData();
  } else {
    promise = Promise.resolve(null);
  }

  promise.then(data => {
    // Lets add the data to the context
    const context = { data };

    const app = ReactDOMServer.renderToString(
      <StaticRouter location={req.url} context={context}>
        <App />
      </StaticRouter>
    );

    const indexFile = path.resolve('./build/index.html');
    fs.readFile(indexFile, 'utf8', (err, indexData) => {
      if (err) {
        console.error('Something went wrong:', err);
        return res.status(500).send('Oops, better luck next time!');
      }

      if (context.status === 404) {
        res.status(404);
      }
      if (context.url) {
        return res.redirect(301, context.url);
      }

      return res.send(
        indexData
          .replace('<div id="root"></div>', `<div id="root">${app}</div>`)
          .replace(
            '</body>',
            `<script>window.__ROUTE_DATA__ = ${serialize(data)}</script></body>`
          )
      );
    });
  });
});

app.listen(PORT, () => {
  console.log(`?? Server is listening on port ${PORT}`);
});

请注意,我们添加组件的数据到context对象。在服务端渲染中我们将通过staticContext来访问它。

现在我们可以在需要加载时获取数据的组件的构造函数和componentDidMount方法里添加一些判断

src/Todos.js


import React from 'react';
import loadData from './helpers/loadData';

class Todos extends React.Component {
  constructor(props) {
    super(props);

    if (props.staticContext && props.staticContext.data) {
      this.state = {
        data: props.staticContext.data
      };
    } else {
      this.state = {
        data: []
      };
    }
  }

  componentDidMount() {
    setTimeout(() => {
      if (window.__ROUTE_DATA__) {
        this.setState({
          data: window.__ROUTE_DATA__
        });
        delete window.__ROUTE_DATA__;
      } else {
        loadData('todos').then(data => {
          this.setState({
            data
          });
        });
      }
    }, 0);
  }

  render() {
    const { data } = this.state;
    return <ul>{data.map(todo => <li key={todo.id}>{todo.title}</li>)}</ul>;
  }
}

export default Todos;

工具类

ReactRouterConfig是由ReactRouter团队提供和维护的包。它提供了两个处理ReactRouter和SSR更便捷的工具matchRoutesrenderRoutes

matchRoutes

前面的例子都非常简单都,都没有嵌套路由。有时在多路由的情况下,使用matchPath是行不通的,因为它只能匹配一条路由。matchRoutes是一个能帮助我们匹配多路由的工具。

这意味着在匹配路由的过程中我们可以往一个数组里存放promise,然后调用promise.all去解决所有匹配到的路由的取数逻辑。


import { matchRoutes } from 'react-router-config';

// ...

const matchingRoutes = matchRoutes(Routes, req.url);

let promises = [];

matchingRoutes.forEach(route => {
  if (route.loadData) {
    promises.push(route.loadData());
  }
});

Promise.all(promises).then(dataArr => {
  // render our app, do something with dataArr, send response
});

// ...

renderRoutes

renderRoutes接收我们的静态路由配置对象并返回所需的Route组件。为了matchRoutes能适当的工作renderRoutes应该被使用。

通过使用renderRoutes,我们的程序改成了一个更简洁的形式。

src/App.js


import React from 'react';
import { renderRoutes } from 'react-router-config';
import { Switch, NavLink } from 'react-router-dom';

import Routes from './routes';

import Home from './Home';
import Posts from './Posts';
import Todos from './Todos';
import NotFound from './NotFound';

export default props => {
  return (
    <div>
      {/* ... */}

      <Switch>
        {renderRoutes(Routes)}
      </Switch>
    </div>
  );
};

译者注

  • SSR服务端React组件的生命周期不会运行到componentDidMount,componentDidMount只有在客户端才会运行。
  • React16不再推荐使用componentWillMount方法,应使用constructor来代替。
  • staticContext的实现应该跟redux的高阶组件connect类似,也是通过包装一层react控件来实现子组件的属性传递。
  • 文章只是对SSR做了一个入门的介绍,如Loadable和样式的处理在文章中没有介绍,但这两点对于SSR来说很重要,以后找机会写一篇相关的博文

原文

原文地址:https://www.cnblogs.com/datiangou/p/10229354.html

时间: 2024-11-10 11:24:12

[译]利用React Router4实现的服务端直出渲染(SSR)的相关文章

winform客户端利用webClient实现与Web服务端的数据传输

由于项目需要,最近研究了下WebClient的数据传输.关于WebClient介绍网上有很多详细介绍,大概就是利用WebClient可以实现对Internet资源的访问.无外乎客户端发送请求,服务端处理请求.回应请求.所以,我下面就简单描述下学习过程中遇到的一些问题: 1.关于Winform客户端请求 WebClient wc = new WebClient();//初始化webclient string path = "http://192.168.1.115:8089/Handler1.as

服务端和客户端渲染

服务端渲染:DOM树在服务端生成,然后返回给前端. 客户端渲染(SSR):前端去后端取数据生成DOM树. 服务端渲染的优点: 1.尽量不占用前端的资源,前端这块耗时少,速度快. 2.有利于SEO优化,因为在后端有完整的html页面,所以爬虫更容易爬取信息. 服务端渲染的缺点: 1.不利于前后端分离,开发的效率降低了. 2.对html的解析,对前端来说加快了速度,但是加大了服务器的压力. 客户端渲染的优点: 1.前后端分离,开发效率高. 2.用户体验更好,我们将网站做成SPA(单页面应用)或者部分

如何利用cURL和python对服务端和web端进行接口测试

工具描述 cURL是利用URL语法在命令行方式下工作的文件传输工具,是开源爱好者编写维护的免费工具,支持包括Windows.Linux.Mac等数十个操作系统,最新版本为7.27.0,但是我推荐大家使用7.26.0,从这里可以下载7.26.0版本. 以下是官方介绍的翻译: cURL是一个使用URL语法来传输数据的命令行工具,支持DICT,FILE,FTP,FTPS,GOPHER,HTTP,HTTPS,IMAP,IMAPS,LDAP,LDAPS,POP3,POP3S,RTMP,RTSP,SCP,S

利用socketserver实现异步多线程服务端简单聊天功能

废话不多说,直接上代码 程序组成 数据访问层:model-------chat_log.py(用于保存聊天记录及展示) ---user_info.py(用户登录信息表,验证通过才能聊天) 公共功能层:utility-------sqlhelper.py(用于处理查询及插入数据的操作) ---conf.py(用于保存数据库连接字符串) 主程序:server.py--------Socket服务端 client.py----------Socket客户端 server.py 1 #!/usr/bin

利用IDEA创建Web Service服务端和客户端的详细过程

创建服务端 一.file–>new–>project 二.点击next后输入服务端名,点击finish,生成目录如下 三.在 HelloWorld.Java 文件中右击,选 WebServices ,再选 Generate Wsdl From Java Code ,确定 点击ok会自动给我们生成HelloWorld.wsdl,画红线处注意 四.配置tomcat,此处不多说,主要是配置好tomcat后还需要加入 Axis 的库 点击右下角fix即可,重新启动 tomcat ,在浏览器中输入:ht

利用控制台承载SignalR作为服务端、及第三方推送信息

一 首先建立一个控制台需要引用一些组件 特别要注意引用Microsoft.Owin.Host.HttpListener别忘了这个组件,不引用他可能程序正常运行不会报错,但服务器一直开启失败(我之前就是掉过这个坑了) 二.建立一个控制台程序且建立集线器类MsgHub继承Microsoft.AspNet.SignalR.Hub 三.初始化服务端 四.调用InitSinalR方法即可启用成功服务端 到此完成服务端程序,客户端只需要请求上面地址即可运行 五.写到这只是完成了客户和服务消息传递,怎么将第三

React(0.13) 服务端渲染的两个函数

1.React.renderToString 函数,  参数是组件,返回一个字符串 <!DOCTYPE html> <html> <head> <title>React JS</title> <script src="../build_0.13/react.js"></script> <script src="../build_0.13/JSXTransformer.js"&g

服务端预渲染之Nuxt(介绍篇)

现在前端开发一般都是前后端分离,mvvm和mvc的开发框架,如Angular.React和Vue等,虽然写框架能够使我们快速的完成开发,但是由于前后台分离,给项目SEO带来很大的不便,搜索引擎在检索的时候是在网页中爬取数据,由于单页面应用读取到的页面是几乎空白的,无法爬取到任何数据信息. <!DOCTYPE html> <html> <head> <meta charset=utf-8> <meta name=viewport content=&quo

如何使用Vue2做服务端渲染

花费了一个月时间,终于在新养车之家项目中成功部署了vue2服务端渲染(SSR),并且使用上了Vuex 负责状态管理,首屏加载时间从之前4G网络下的1000ms,提升到了现在500-700ms之间,SSR的优势有很多,现在让我来跟你细细道来. 技术栈 服务端:Nodejs(v6.3) 前端框架 Vue2.1.10 前端构建工具:webpack2.2 && gulp 代码检查:eslint 源码:es6 前端路由:vue-router2.1.0 状态管理:vuex2.1.0 服务端通信:axi