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


Posted in Javascript onJanuary 07, 2019

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

原文地址

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

Javascript 相关文章推荐
javascript实现的网页局布刷新效果
Dec 01 Javascript
js 实现无缝滚动 兼容IE和FF
Jul 15 Javascript
jQuery点击弹出下拉菜单的小例子
Aug 01 Javascript
js中符号转意问题示例探讨
Aug 19 Javascript
JS去掉第一个字符和最后一个字符的实现代码
Feb 20 Javascript
用C/C++来实现 Node.js 的模块(二)
Sep 24 Javascript
如何使用HTML5地理位置定位功能
Apr 27 Javascript
jQuery 1.9.1源码分析系列(十五)之动画处理
Dec 03 Javascript
jQuery表格插件datatables用法汇总
Mar 29 Javascript
H5基于iScroll实现下拉刷新和上拉加载更多
Jul 18 Javascript
jQuery 防止相同的事件快速重复触发方法
Feb 08 jQuery
详解ES6通过WeakMap解决内存泄漏问题
Mar 09 Javascript
Node.js EventEmmitter事件监听器用法实例分析
Jan 07 #Javascript
小程序二次贝塞尔曲线实现购物车商品曲线飞入效果
Jan 07 #Javascript
jQuery实现的别踩白块小游戏完整示例
Jan 07 #jQuery
jQuery判断自定义属性data-val用法示例
Jan 07 #jQuery
jQuery实现的简单歌词滚动功能示例
Jan 07 #jQuery
微信小程序发送短信验证码完整实例
Jan 07 #Javascript
JS数组求和的常用方法实例小结
Jan 07 #Javascript
You might like
基于Zend的Captcha机制的应用
2013/05/02 PHP
Laravel 5框架学习之用户认证
2015/04/09 PHP
PHP实现无限级分类(不使用递归)
2015/10/22 PHP
PHP get_html_translation_table()函数用法讲解
2019/02/16 PHP
基于jquery的无缝循环新闻列表插件
2011/03/07 Javascript
关于JAVASCRIPT urldecode URL解码的问题
2012/01/08 Javascript
JavaScript获取元素尺寸和大小操作总结
2015/02/27 Javascript
javascript实现的字符串与十六进制表示字符串相互转换方法
2015/07/17 Javascript
jQuery实现多级下拉菜单jDropMenu的方法
2015/08/28 Javascript
AngularJS基础 ng-show 指令简单示例
2016/08/03 Javascript
AngularJS 文件上传控件 ng-file-upload详解
2017/01/13 Javascript
vue父组件向子组件动态传值的两种方法
2017/11/11 Javascript
JavaScript实现为事件句柄绑定监听函数的方法分析
2017/11/14 Javascript
旺旺在线客服代码 旺旺客服代码生成器
2018/01/09 Javascript
jQuery实现获取动态添加的标签对象示例
2018/06/28 jQuery
JS封装的模仿qq右下角消息弹窗功能示例
2018/08/22 Javascript
详解TypeScript+Vue 插件 vue-class-component的使用总结
2019/02/18 Javascript
关于layui flow loading占位图的实现方法
2019/09/21 Javascript
[01:17]炒鸡美酒第四天TA暴走
2018/06/05 DOTA
Python实现list反转实例汇总
2014/11/11 Python
python非递归全排列实现方法
2017/04/10 Python
基于Django模板中的数字自增(详解)
2017/09/05 Python
pandas 把数据写入txt文件每行固定写入一定数量的值方法
2018/12/28 Python
python应用文件读取与登录注册功能
2019/09/23 Python
Python读取表格类型文件代码实例
2020/02/17 Python
PyCharm 2020 激活到 2100 年的教程
2020/03/25 Python
Pytorch如何切换 cpu和gpu的使用详解
2021/03/01 Python
Bally巴利中国官网:经典瑞士鞋履、手袋及配饰奢侈品牌
2018/10/09 全球购物
意大利香水和化妆品购物网站:Parfimo.it
2019/10/06 全球购物
英国豪华家具和家居用品购物网站:Teddy Beau
2020/10/12 全球购物
办加油卡单位介绍信
2014/01/09 职场文书
元旦晚会邀请函
2014/02/01 职场文书
2014年惩防体系建设工作总结
2014/12/01 职场文书
考博导师推荐信范文
2015/03/27 职场文书
2015初一年级组工作总结
2015/07/24 职场文书
详解Python中下划线的5种含义
2021/07/15 Python