我們已經熟悉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> ); };
譯者注
原文地址
聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯系,我們將在第一時間刪除處理。TEL:177 7030 7066 E-MAIL:11247931@qq.com