我们已经熟悉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(`"htmlcode">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(`"htmlcode">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来说很重要,以后找机会写一篇相关的博文
原文地址
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。
《魔兽世界》大逃杀!60人新游玩模式《强袭风暴》3月21日上线
暴雪近日发布了《魔兽世界》10.2.6 更新内容,新游玩模式《强袭风暴》即将于3月21 日在亚服上线,届时玩家将前往阿拉希高地展开一场 60 人大逃杀对战。
艾泽拉斯的冒险者已经征服了艾泽拉斯的大地及遥远的彼岸。他们在对抗世界上最致命的敌人时展现出过人的手腕,并且成功阻止终结宇宙等级的威胁。当他们在为即将于《魔兽世界》资料片《地心之战》中来袭的萨拉塔斯势力做战斗准备时,他们还需要在熟悉的阿拉希高地面对一个全新的敌人──那就是彼此。在《巨龙崛起》10.2.6 更新的《强袭风暴》中,玩家将会进入一个全新的海盗主题大逃杀式限时活动,其中包含极高的风险和史诗级的奖励。
《强袭风暴》不是普通的战场,作为一个独立于主游戏之外的活动,玩家可以用大逃杀的风格来体验《魔兽世界》,不分职业、不分装备(除了你在赛局中捡到的),光是技巧和战略的强弱之分就能决定出谁才是能坚持到最后的赢家。本次活动将会开放单人和双人模式,玩家在加入海盗主题的预赛大厅区域前,可以从强袭风暴角色画面新增好友。游玩游戏将可以累计名望轨迹,《巨龙崛起》和《魔兽世界:巫妖王之怒 经典版》的玩家都可以获得奖励。