利用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 相关文章推荐
一个tab标签切换效果代码
Mar 27 Javascript
js使用post 方式打开新窗口
Feb 26 Javascript
jQuery晃动层特效实现方法
Mar 09 Javascript
JavaScript实现强制重定向至HTTPS页面
Jun 10 Javascript
javascript框架设计之种子模块
Jun 23 Javascript
jquery实现点击弹出可放大居中及关闭的对话框(附demo源码下载)
May 10 Javascript
VueJs 搭建Axios接口请求工具
Nov 20 Javascript
使用jquery DataTable和ajax向页面显示数据列表的方法
Aug 09 jQuery
JS中getElementsByClassName与classList兼容性问题解决方案分析
Aug 07 Javascript
JS如何把字符串转换成json
Feb 21 Javascript
JS出现404错误原理及解决方案
Jul 01 Javascript
基于javascript实现移动端轮播图效果
Dec 21 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
php根据日期判断星座的函数分享
2014/02/13 PHP
PHP动态生成javascript文件的2个例子
2014/04/11 PHP
php 删除cookie方法详解
2014/12/01 PHP
浅谈php7的重大新特性
2015/10/23 PHP
php实现XML和数组的相互转化功能示例
2017/02/08 PHP
PHP filesize函数用法浅析
2019/02/15 PHP
读jQuery之二(两种扩展)
2011/06/11 Javascript
SyntaxHighlighter语法高亮插件使用说明
2011/08/14 Javascript
JQuery radio(单选按钮)操作方法汇总
2015/04/15 Javascript
javascript 使用for循环时该注意的问题-附问题总结
2015/08/19 Javascript
JavaScript+html5 canvas制作的百花齐放效果完整实例
2016/01/26 Javascript
node.js从数据库获取数据
2016/05/08 Javascript
JavaScript实现自定义媒体播放器方法介绍
2017/01/03 Javascript
让网站自动生成章节目录索引的多个js代码
2018/01/07 Javascript
使用 vue-i18n 切换中英文效果
2018/05/23 Javascript
Vue项目数据动态过滤实践及实现思路
2018/09/11 Javascript
js重写alert事件(避免alert弹框标题出现网址)
2020/12/04 Javascript
[08:29]DOTA2每周TOP10 精彩击杀集锦vol.7
2014/06/25 DOTA
Python使用稀疏矩阵节省内存实例
2014/06/27 Python
Python实现去除代码前行号的方法
2015/03/10 Python
python通过smpt发送邮件的方法
2015/04/30 Python
Python 多线程抓取图片效率对比
2016/02/27 Python
基于Python实现一个简单的银行转账操作
2016/03/06 Python
Python中列表、字典、元组数据结构的简单学习笔记
2016/03/20 Python
利用Python暴力破解zip文件口令的方法详解
2017/12/21 Python
用tensorflow实现弹性网络回归算法
2018/01/09 Python
Python爬取商家联系电话以及各种数据的方法
2018/11/10 Python
Django模型中字段属性choice使用说明
2020/03/30 Python
浅谈Html5页面打开app的一些思考
2020/03/30 HTML / CSS
New Balance法国官方网站:购买鞋子和服装
2019/09/01 全球购物
大学总结自我鉴定
2014/01/18 职场文书
化学教学随笔感言
2014/02/19 职场文书
学校安全责任书
2014/04/14 职场文书
银行求职信怎么写
2014/05/26 职场文书
商场圣诞节活动总结
2015/05/06 职场文书
原生JS封装vue Tab切换效果
2021/04/28 Vue.js