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

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
-
上一篇
写给社区的回顾和展望:TiDB 2019, Level Up !
作者:黄东旭 2018 年对于 TiDB 和 PingCAP 来说是一个由少年向成年的转换的一年,如果用一个关键字来概括就是「蜕变」。在这一年很欣喜的看到 TiDB 和 TiKV 在越来越多的用户使用在了越来越广泛的场景中,作为一个刚刚 3 岁多的开源项目,没有背后强大的社区的话,是没有办法取得这样的进展的。 同时在技术上,2018 年我觉得也交出了一份令人满意的答卷,TiDB 的几个主要项目今年一共合并了 4380 个提交,这几天在整理 2018 年的 Change Log 时候,对比了一下年初的版本,这 4380 个 Commits 背后代表了什么,这里简单写一个文章总结一下。 回想起来,TiDB 是最早定位为 HTAP 的通用分布式数据库之一,如果熟悉我们的老朋友一定知道,我们最早时候一直都是定位 NewSQL,当然现在也是。但是 NewSQL 这个词有个问题,到底 New 在哪,解决了哪些问题,很难一目了然,其实一开始我们就想解决一个 MySQL 分库分表的问题,但是后来慢慢随着我们的用户越来越多,使用的场景也越来越清晰,很多用户的场景已经开始超出了一个「更大的 MySQL 」...
-
下一篇
Hadoop文件系统元数据管理机制
edits log 默认是 64MB,当写满的时候或者到一定周期的时候,Namanode就会进行 CheckPoint。Checkpoint是一个内部事件,这个事件激活以后会触发数据库写进程(DBWR)将数据缓冲(DATABUFFER CACHE)中的脏数据块写出到数据文件中。 这里仅仅是以一份副本来描述,实际上默认是切片后每一个切片的数据块都有三份副本,保存在不同的Datanode中,假设有多个不同的机架,每个机架有多台主机,意味着有多个机架上面的Datanode,每一台主机作为一个Datanode,数据块的副本保存顺序是: 1、先在本机架上面找寻最近的一台主机保存第一份副本; 2、然后到其他机架上面随机选择一台主机保存第二份副本; 3、最后再在本机架上面除第一份副本的主机外随机选择另一台主机保存第三份副本。 若是Namenode宕机了,还能否恢复数据?重启集群之后还能提供服务么? 可以恢复数据,通过FSimage恢复数据;Namenode宕机了提供不了查询数据和保存数据的功能,因此不能提供服务。 可以使用 多个Namenode副本,副本namenode与namenode的数据保持一...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- MySQL数据库在高并发下的优化方案
- CentOS关闭SELinux安全模块
- CentOS8编译安装MySQL8.0.19
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- SpringBoot2初体验,简单认识spring boot2并且搭建基础工程
- Linux系统CentOS6、CentOS7手动修改IP地址
- Docker容器配置,解决镜像无法拉取问题
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7
- CentOS7编译安装Cmake3.16.3,解决mysql等软件编译问题
- CentOS7,CentOS8安装Elasticsearch6.8.6