原文
我们已经熟悉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(
,
document.getElementById('root')
);
复制代码
在服务端我们将采取类似的方式,但是改为使用无状态的 StaticRouter
server/index.js
app.get('/*', (req, res) => {
const context = {};
const app = ReactDOMServer.renderToString(
);
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('', `"root">${app}`)
);
});
});
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 (
-
"/">Home
-
"/todos">Todos
-
"/posts">Posts
"/"
render={props => "Alligator.io" {...props} />}
/>
"/todos" component={Todos} />
"/posts" component={Posts} />
);
};
复制代码
现在如果你运行一下程序($ yarn run dev),我们的路由在服务端被渲染,这是我们所预期的。
利用404状态来处理未找到资源的网络请求
我们做一些改进,当渲染NotFound组件时让服务端使用404HTTP状态码来响应。首先我们将一些信息放到NotFound组件的staticContext
import React from 'react';
export default ({ staticContext = {} }) => {
staticContext.status = 404;
return Oops, nothing here!
;
};
复制代码
然后在服务端,我们可以检查context对象的status属性是否是404,如果是404,则以404状态响应服务端请求。
server/index.js
// ...
app.get('/*', (req, res) => {
const context = {};
const app = ReactDOMServer.renderToString(
);
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('', `"root">${app}`)
);
});
});
// ...
复制代码
重定向
补充一下,我们可以做一些类似重定向的工作。如果我们有使用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(
);
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('', `"root">${app}`)
.replace(
'