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

 更新时间:2019年01月07日 11:41:58   作者:violinux666  
这篇文章主要介绍了利用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组件需要 location和context属性。我们传递当前的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更便捷的工具matchRoutes和renderRoutes。

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来说很重要,以后找机会写一篇相关的博文

原文地址

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。

相关文章

  • React Native全面屏状态栏和底部导航栏适配教程详细讲解

    React Native全面屏状态栏和底部导航栏适配教程详细讲解

    最近在写 React Native 项目,调试应用时发现顶部状态栏和底部全面屏手势指示条区域不是透明的,看起来很难受。研究了一下这个问题,现在总结一下解决方案,这篇文章主要介绍了React Native全面屏状态栏和底部导航栏适配教程
    2023-01-01
  • 使用Jenkins部署React项目的方法步骤

    使用Jenkins部署React项目的方法步骤

    这篇文章主要介绍了使用Jenkins部署React项目的方法步骤,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2019-03-03
  • React项目搭建与Echarts工具使用详解

    React项目搭建与Echarts工具使用详解

    这篇文章主要介绍了React项目搭建与Echarts工具使用详解,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2023-03-03
  • React实现二级联动的方法

    React实现二级联动的方法

    这篇文章主要为大家详细介绍了React实现二级联动的方法,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-09-09
  • 使用React实现内容滑动组件效果

    使用React实现内容滑动组件效果

    这篇文章主要介绍了使用React实现一个内容滑动组件效果,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2023-05-05
  • React 父子组件通信的实现方法

    React 父子组件通信的实现方法

    这篇文章主要介绍了React 父子组件通信的实现方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-12-12
  • 浅谈React底层实现原理

    浅谈React底层实现原理

    本文主要介绍了浅谈React底层实现原理,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-07-07
  • 使用useEffect模拟组件生命周期

    使用useEffect模拟组件生命周期

    这篇文章主要介绍了使用useEffect模拟组件生命周期,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-09-09
  • React Native项目中使用Lottie动画的方法

    React Native项目中使用Lottie动画的方法

    这篇文章主要介绍了React Native 实现Lottie动画的相关知识,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2021-10-10
  • React安装node-sass失败解决方案分享

    React安装node-sass失败解决方案分享

    Node-sass是一个库,它将Node.js绑定到LibSass(流行样式表预处理器Sass的C版本),它允许用户以令人难以置信的速度将.scss文件本地编译为css,并通过连接中间件自动编译,下面这篇文章主要给大家介绍了关于React安装node-sass失败解决的相关资料,需要的朋友可以参考下
    2022-11-11

最新评论