利用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 相关文章推荐
永不消失的title提示代码
Feb 15 Javascript
ExtJs使用总结(非常详细)
Mar 22 Javascript
js Array操作的最简短最容易理解方法
Dec 09 Javascript
Jquery基础之事件操作详解
Jun 14 Javascript
JS实现可编辑的后台管理菜单功能【附demo源码下载】
Sep 13 Javascript
javascript实现数据双向绑定的三种方式小结
Mar 09 Javascript
判断jQuery是否加载完成,没完成继续判断的解决方法
Dec 06 jQuery
js判断传入时间和当前时间大小实例(超简单)
Jan 11 Javascript
vue2.0 + ele的循环表单及验证字段方法
Sep 18 Javascript
Vue表情输入组件 微信face表情组件
Feb 11 Javascript
axios+Vue实现上传文件显示进度功能
Apr 14 Javascript
vue 进阶之实现父子组件间的传值
Apr 26 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
smarty模板引擎使用内建函数foreach循环取出所有数组值的方法
2015/01/22 PHP
PHP 5.3和PHP 5.4出现FastCGI Error解决方法
2015/02/12 PHP
如何判断php mysqli扩展类是否开启
2016/12/24 PHP
PHP里面把16进制的图片数据显示在html的img标签上(实现方法)
2017/05/02 PHP
关于javascript document.createDocumentFragment()
2009/04/04 Javascript
跨浏览器开发经验总结(三)   警惕“IE依赖综合症”
2010/05/13 Javascript
AngularJS HTML编译器介绍
2014/12/06 Javascript
AspNet中使用JQuery上传插件Uploadify详解
2015/05/20 Javascript
jQuery可见性过滤器:hidden和:visibility用法实例
2015/06/24 Javascript
整理JavaScript创建对象的八种方法
2015/11/03 Javascript
详解javascript实现瀑布流列式布局
2016/01/29 Javascript
微信小程序中使元素占满整个屏幕高度实现方法
2016/12/14 Javascript
jQuery插件FusionCharts实现的2D柱状图效果示例【附demo源码下载】
2017/03/06 Javascript
利用NPM淘宝的node.js镜像加速nvm
2017/03/27 Javascript
vue页面跳转后返回原页面初始位置方法
2018/02/11 Javascript
layer的prompt弹出框,点击回车,触发确定事件的方法
2019/09/06 Javascript
JavaScript事件循环及宏任务微任务原理解析
2020/09/02 Javascript
[02:49]2018DOTA2亚洲邀请赛主赛事决赛日战况回顾 Mineski鏖战5局夺得辉耀
2018/04/10 DOTA
[38:38]完美世界DOTA2联赛PWL S3 access vs Rebirth 第二场 12.17
2020/12/18 DOTA
Django中url的反向查询的方法
2018/03/14 Python
Python类的继承、多态及获取对象信息操作详解
2019/02/28 Python
在python image 中安装中文字体的实现方法
2019/08/22 Python
Python统计分析模块statistics用法示例
2019/09/06 Python
使用python快速实现不同机器间文件夹共享方式
2019/12/22 Python
利用matplotlib为图片上添加触发事件进行交互
2020/04/23 Python
纯CSS3实现自定义Tooltip边框涂鸦风格的教程
2014/11/05 HTML / CSS
Intimissimi德国网上商店:意大利知名内衣品牌
2018/04/03 全球购物
美国班级戒指、帽子和礼服、毕业产品、年鉴:Balfour
2018/11/01 全球购物
澳大利亚领先的折扣药房:Chemist Direct(有中文站)
2018/11/24 全球购物
澳大利亚著名的纺织品品牌:Canningvale
2020/05/05 全球购物
法学自荐信
2014/06/20 职场文书
公司演讲稿开场白
2014/08/25 职场文书
2014机关党员干部“正风肃纪”思想汇报
2014/09/15 职场文书
婚前协议书范本两则
2014/10/16 职场文书
家电创业计划书
2019/08/05 职场文书
实战 快速定位MySQL的慢SQL
2022/03/22 MySQL