利用React Router4實(shí)現(xiàn)的服務(wù)端直出渲染(SSR)
我們已經(jīng)熟悉React 服務(wù)端渲染(SSR)的基本步驟,現(xiàn)在讓我們更進(jìn)一步利用 React RouterV4 實(shí)現(xiàn)客戶(hù)端和服務(wù)端的同構(gòu)。畢竟大多數(shù)的應(yīng)用都需要用到web前端路由器,所以要讓SSR能夠正常的運(yùn)行,了解路由器的設(shè)置是十分有必要的
基本步驟
路由器配置
前言已經(jīng)簡(jiǎn)單的介紹了React SSR,首先我們需要添加ReactRouter4到我們的項(xiàng)目中
$ yarn add react-router-dom # or, using npm $ npm install react-router-dom
接著我們會(huì)描述一個(gè)簡(jiǎn)單的場(chǎng)景,其中組件是靜態(tài)的且不需要去獲取外部數(shù)據(jù)。我們會(huì)在這個(gè)基礎(chǔ)之上去了解如何完成取到數(shù)據(jù)的服務(wù)端渲染。
在客戶(hù)端,我們只需像以前一樣將我們的的App組件通過(guò)ReactRouter的BrowserRouter來(lái)包起來(lái)。
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')
);
在服務(wù)端我們將采取類(lèi)似的方式,但是改為使用無(wú)狀態(tài)的 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屬性。我們傳遞當(dāng)前的url(Express req.url)給location,設(shè)置一個(gè)空對(duì)象給context。context對(duì)象用于存儲(chǔ)特定的路由信息,這個(gè)信息將會(huì)以staticContext的形式傳遞給組件
運(yùn)行一下程序看看結(jié)果是否我們所預(yù)期的,我們給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>
);
};
現(xiàn)在如果你運(yùn)行一下程序($ yarn run dev),我們的路由在服務(wù)端被渲染,這是我們所預(yù)期的。
利用404狀態(tài)來(lái)處理未找到資源的網(wǎng)絡(luò)請(qǐng)求
我們做一些改進(jìn),當(dāng)渲染NotFound組件時(shí)讓服務(wù)端使用404HTTP狀態(tài)碼來(lái)響應(yīng)。首先我們將一些信息放到NotFound組件的staticContext
import React from 'react';
export default ({ staticContext = {} }) => {
staticContext.status = 404;
return <h1>Oops, nothing here!</h1>;
};
然后在服務(wù)端,我們可以檢查context對(duì)象的status屬性是否是404,如果是404,則以404狀態(tài)響應(yīng)服務(wù)端請(qǐng)求。
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>`)
);
});
});
// ...
重定向
補(bǔ)充一下,我們可以做一些類(lèi)似重定向的工作。如果我們有使用Redirect組件,ReactRouter會(huì)自動(dòng)添加重定向的url到context對(duì)象的屬性上。
server/index.js (部分)
if (context.url) {
return res.redirect(301, context.url);
}
讀取數(shù)據(jù)
有時(shí)候我們的服務(wù)端渲染應(yīng)用需要數(shù)據(jù)呈現(xiàn),我們需要用一種靜態(tài)的方式來(lái)定義我們的路由而不是只涉及到客戶(hù)端的動(dòng)態(tài)的方式。失去定義動(dòng)態(tài)路由的定義是服務(wù)端渲染最適合所需要的應(yīng)用的原因(譯者注:這句話(huà)的意思應(yīng)該是SSR不允許路由是動(dòng)態(tài)定義的)。
我們將使用fetch在客戶(hù)端和服務(wù)端,我們?cè)黾觟somorphic-fetch到我們的項(xiàng)目。同時(shí)我們也增加serialize-javascript這個(gè)包,它可以方便的序列化服務(wù)器上獲取到的數(shù)據(jù)。
$ yarn add isomorphic-fetch serialize-javascript # or, using npm: $ npm install isomorphic-fetch serialize-javascript
我們定義我們的路由信息為一個(gè)靜態(tài)數(shù)組在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;
有一些路由配置現(xiàn)在有一個(gè)叫l(wèi)oadData的鍵,它是一個(gè)調(diào)用loadData函數(shù)的函數(shù)。這個(gè)是我們的loadData函數(shù)的實(shí)現(xiàn)
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);
});
};
我們簡(jiǎn)單的使用fetch來(lái)從REST API 獲取數(shù)據(jù)
在服務(wù)端我們將使用ReactRouter的matchPath去尋找當(dāng)前url所匹配的路由配置并判斷它有沒(méi)有l(wèi)oadData屬性。如果是這樣,我們調(diào)用loadData去獲取數(shù)據(jù)并把數(shù)據(jù)放到全局window對(duì)象中在服務(wù)器的響應(yīng)中
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}`);
});
請(qǐng)注意,我們添加組件的數(shù)據(jù)到context對(duì)象。在服務(wù)端渲染中我們將通過(guò)staticContext來(lái)訪(fǎng)問(wèn)它。
現(xiàn)在我們可以在需要加載時(shí)獲取數(shù)據(jù)的組件的構(gòu)造函數(shù)和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;
工具類(lèi)
ReactRouterConfig是由ReactRouter團(tuán)隊(duì)提供和維護(hù)的包。它提供了兩個(gè)處理ReactRouter和SSR更便捷的工具matchRoutes和renderRoutes。
matchRoutes
前面的例子都非常簡(jiǎn)單都,都沒(méi)有嵌套路由。有時(shí)在多路由的情況下,使用matchPath是行不通的,因?yàn)樗荒芷ヅ湟粭l路由。matchRoutes是一個(gè)能幫助我們匹配多路由的工具。
這意味著在匹配路由的過(guò)程中我們可以往一個(gè)數(shù)組里存放promise,然后調(diào)用promise.all去解決所有匹配到的路由的取數(shù)邏輯。
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接收我們的靜態(tài)路由配置對(duì)象并返回所需的Route組件。為了matchRoutes能適當(dāng)?shù)墓ぷ鱮enderRoutes應(yīng)該被使用。
通過(guò)使用renderRoutes,我們的程序改成了一個(gè)更簡(jiǎn)潔的形式。
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服務(wù)端React組件的生命周期不會(huì)運(yùn)行到componentDidMount,componentDidMount只有在客戶(hù)端才會(huì)運(yùn)行。
- React16不再推薦使用componentWillMount方法,應(yīng)使用constructor來(lái)代替。
- staticContext的實(shí)現(xiàn)應(yīng)該跟redux的高階組件connect類(lèi)似,也是通過(guò)包裝一層react控件來(lái)實(shí)現(xiàn)子組件的屬性傳遞。
- 文章只是對(duì)SSR做了一個(gè)入門(mén)的介紹,如Loadable和樣式的處理在文章中沒(méi)有介紹,但這兩點(diǎn)對(duì)于SSR來(lái)說(shuō)很重要,以后找機(jī)會(huì)寫(xiě)一篇相關(guān)的博文
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
關(guān)于react useState更新異步問(wèn)題
這篇文章主要介紹了關(guān)于react useState更新異步問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。2022-08-08
React-router中結(jié)合webpack實(shí)現(xiàn)按需加載實(shí)例
本篇文章主要介紹了React-router中結(jié)合webpack實(shí)現(xiàn)按需加載實(shí)例,非常具有實(shí)用價(jià)值,需要的朋友可以參考下2017-05-05
在React項(xiàng)目中實(shí)現(xiàn)一個(gè)簡(jiǎn)單的錨點(diǎn)目錄定位
錨點(diǎn)目錄定位功能在長(zhǎng)頁(yè)面和文檔類(lèi)網(wǎng)站中非常常見(jiàn),它可以讓用戶(hù)快速定位到頁(yè)面中的某個(gè)章節(jié),本文講給大家介紹一下React項(xiàng)目中如何實(shí)現(xiàn)一個(gè)簡(jiǎn)單的錨點(diǎn)目錄定位,文中有詳細(xì)的實(shí)現(xiàn)代碼,需要的朋友可以參考下2023-09-09
ReactNative踩坑之配置調(diào)試端口的解決方法
本篇文章主要介紹了ReactNative踩坑之配置調(diào)試端口的解決方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-07-07
詳解React?如何防止?XSS?攻擊論$$typeof?的作用
這篇文章主要介紹了詳解React?如何防止?XSS?攻擊論$$typeof?的作用,文章圍繞主題展開(kāi)詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,需要的小伙伴可以參考一下2022-07-07

