本文旨在幫助讀者在react router v4下搭建本身的知足seo需求的server side render app。react
reactgit
很少說了,近年來發展迅猛,被各大主流應用普遍使用;github
react-router-v4web
react路由作了一次較大的改動,若是你的app是16年或者更早搭建的話,相信路由仍是v3版本,不過沒有關係,你能夠參照本文嘗試如今就升級你的router,v4會給你帶來更多的靈活性。express
與v3相比,v4主要有如下更改:npm
匹配規則
本文假設你已經熟悉react,redux和express,而且構建過本身的app;redux
1. 安裝React Router v4;promise
npm i --save react-router-dom react-router-config
複製代碼
或用yarn
,react-router-config
是web的router,它進一步封裝了react-router
, (native app的安裝包是react-router-native
), 而且保留了一些react-router
的接口,因此若是你對系統已經裝了react-router
,建議刪除,直接用react-router-config
就好。bash
2. 配置路由;react-router
若是你在用v3,那麼你可能有這樣一個集中式的路由文件:
import React from 'react';
import { Route } from 'react-router';
const Routes = () => (
<Route path="/" onEnter={() => {}} onChange={() => {}}>
<Route path=":channel/abc" component={MyContainer1} />
<Route path=":channel/def" component={MyContainer2} />
<Route path=":channel/*" component={NotFoundContainer} status={404} />
</Route>
);
export default Routes;
複製代碼
v4是大相徑庭的作法,首先,定義一個你的路由配置:
# routes.js
import RootApp from './RootApp';
import Home from './Home';
import List from './List';
const routes = [
{ component: RootApp,
routes: [ # 多級嵌套
{ path: '/',
exact: true,
component: Home
},
{ path: '/home',
component: Home
},
{ path: '/list',
component: List
}
]
}
];
export default routes;
複製代碼
在你的client端,使用<BrowserRouter>
render你的routes, 你的client.js
長成這個樣子:
# client.js
import React from 'react';
import {render} from 'react-dom';
import BrowserRouter from 'react-router-dom/BrowserRouter';
import { renderRoutes } from 'react-router-config';
import { createStore, applyMiddleware } from 'redux';
import { Provider } from 'react-redux';
import thunk from 'redux-thunk';
import routes from './routes'; # 上文定義的routes配置;
import reducers from './modules';
const store = createStore(
reducers, window.__INITIAL_STATE__, applyMiddleware(thunk)
);
const AppRouter = () => {
return (
<Provider store={store}>
<BrowserRouter> # 使用v4的BrowserRouter;
{renderRoutes(routes)} # 使用v4的renderRoutes;
</BrowserRouter>
</Provider>
)
}
render(<AppRouter />, document.querySelector('#app'));
複製代碼
在你的server端,使用<StaticRouter>
,eg. server.js
:
# server.js
import express from 'express';
import request from 'request';
import React from 'react';
import { renderToString } from 'react-dom/server';
import StaticRouter from 'react-router-dom/StaticRouter';
import { renderRoutes } from 'react-router-config';
import routes from './routes'; # 上文定義的routes配置;
const router = express.Router();
router.get('*', (req, res) => {
let context = {};
const content = renderToString(
<StaticRouter location={req.url} context={context}>
{renderRoutes(routes)}
</StaticRouter>
);
res.render('index', {title: 'Express', data: false, content });
});
module.exports = router;
複製代碼
實現你的root component:
# RootApp.js
import React from "react";
import { renderRoutes } from "react-router-config";
const RootApp = (props) => {
return (
<div>
{renderRoutes(props.route.routes)} # 分佈式,若是你使用嵌套路由,在你的每一個father component上作相似的render;
</div>
);
};
export default RootApp;
複製代碼
renderRoutes
會幫你作相似這樣的事情:
render() {
return(
<Switch>
<Route exact path="/" component={Home}/>
<Route path="/home" component={Home}/>
<Route path="/list" component={List}/>
</Switch>
)
}
複製代碼
3. fetch 數據
若是你的app須要fetch數據,v4下可使用如下幾種方式:
集中式:
在你的路由配置中把fetch動做配置好,而後server.js內統一處理:
# routes.js
import RootApp from './RootApp';
import Home from './Home';
import List from './List';
import {fetchRootData,fetchHomeData, fetchListData} from './fetchData';
const routes = [
{ component: RootApp,
fetchData: () => {fetchRootData}
routes: [
{ path: '/',
exact: true,
component: Home,
fetchData: () => {fetchHomeData}
},
{ path: '/home',
component: Home,
fetchData: () => {fetchHomeData}
},
{ path: '/list',
component: List,
fetchData: () => {fetchListData}
}
]
}
];
export default routes;
複製代碼
# server.js
import express from 'express';
import request from 'request';
import React from 'react';
import { renderToString } from 'react-dom/server';
import StaticRouter from 'react-router-dom/StaticRouter';
import { renderRoutes } from 'react-router-config';
import routes from './routes'; # 上文定義的routes配置;
const router = express.Router();
router.get('*', (req, res) => {
const {path, query} = req;
const matchedRoutes = matchRoutes(routes, path); # 注意,這裏使用path作match而不是req.url, 由於req.url內含query,v4 router在作正則match的時候並不會過濾query;
const store = configureStore();
const dispatch = store.dispatch;
const promises = matchedRoutes.map(({route}) => {
let fetchData = route.fetchData;
return fetchData instanceof Function ? fetchData(store) : Promise.resolve(null)
});
return Promise.all(promises)
.then(throwErrorIfApiResponseFailed(store)) # implement yourself
.then(handleSuccessPage(store, req, res)) # server side <StaticRouter> render, implement yourself
.catch(handleError(res, query)); # error handler, return error page and error code, implement yourself
});
module.exports = router;
複製代碼
這種方式集中配置,集中處理,可是有一個問題須要注意的是client side如何fetchData,v4已不直接支持history.listen(client url變化時能夠攔截並追加一系列操做), 因此須要尋找一種方式讓client side也能拿到數據。
分佈式:
使用react生命週期函數,在合適的時機觸發Action請求數據,將fetch數據動做散落在每一個component級;
#
componentDidMount() {
fetchData();
}
複製代碼
你可能會有疑問,這隻能給client用啊,server side怎麼辦?你能夠把fetchData Action這樣綁到component上,而後server side也能夠統一處理:
# RootApp
class List extends Component {
static fetchData(store) {
return store.dispatch(fetchUsers());
}
componentDidMount() {
this.props.fetchUsers();
}
render() {
return <div></div>
}
}
複製代碼
# server.js
import express from 'express';
import request from 'request';
import React from 'react';
import { renderToString } from 'react-dom/server';
import StaticRouter from 'react-router-dom/StaticRouter';
import { renderRoutes } from 'react-router-config';
import routes from './routes'; # 上文定義的routes配置;
const router = express.Router();
router.get('*', (req, res) => {
const {path, query} = req;
const matchedRoutes = matchRoutes(routes, path);
const store = configureStore();
const dispatch = store.dispatch;
const promises = matchedRoutes.map(({route}) => {
let fetchData = route.component.fetchData;
return fetchData instanceof Function ? fetchData(store) : Promise.resolve(null)
});
return Promise.all(promises) # server side 集中請求數據;
.then(throwErrorIfApiResponseFailed(store))
.then(handleSuccessPage(store, req, res)) # server side <StaticRouter> render, implement yourself
.catch(handleError(res, query)); # error handler, return error page and error code, implement yourself
});
module.exports = router;
複製代碼
5. Handle 404 錯誤頁面
# routes.js
import RootApp from './RootApp';
import Home from './Home';
import List from './List';
const routes = [
{ component: RootApp,
routes: [
{ path: '/',
exact: true,
component: Home
},
{ path: '/home',
component: Home
},
{ path: '/list',
component: List
},
{
+ path: '*',
+ component: NotFound
}
]
}
];
export default routes;
複製代碼
你的NotFound component本身維護錯誤碼;
# Notfound.js
import React from 'react';
import { Route } from 'react-router-dom';
const NotFound = () => {
return (
<Route render={({ staticContext }) => {
if (staticContext) {
staticContext.status = 404;
}
return (
<div>
<h1>404 : Not Found</h1>
</div>
)
}}/>
);
};
export default NotFound;
複製代碼
server 端:
# server.js
import express from 'express';
import request from 'request';
import React from 'react';
import { renderToString } from 'react-dom/server';
import StaticRouter from 'react-router-dom/StaticRouter';
import { renderRoutes } from 'react-router-config';
import routes from './routes'; # 上文定義的routes配置;
const router = express.Router();
router.get('*', (req, res) => {
let context = {};
const content = renderToString(
<StaticRouter location={req.url} context={context}>
{renderRoutes(routes)}
</StaticRouter>
);
+ if(context.status === 404) { # 獲取狀態碼並響應;
+ res.status(404);
+ }
res.render('index', {title: 'Express', data: false, content });
});
module.exports = router;
複製代碼
5. Handle redirects重定向
# routes.js
import AppRoot from './AppRoot';
import Home from './Home';
import List from './List';
import NotFound from './Notfound';
+import ListToUsers from './ListToUsers';
const routes = [
{ component: AppRoot,
routes: [
{ path: '/',
exact: true,
component: Home
},
{ path: '/home',
component: Home
},
+ { path: '/list',
+ component: ListToUsers
+ }
+ { path: '/users',
+ component: List
+ }
{
path: '*',
component: NotFound
}
]
}
];
export default routes;
複製代碼
與404 的處理相似,component本身維護狀態碼:
# ListToUsers.jsx
import React from 'react';
import { Route, Redirect } from 'react-router-dom';
const ListToUsers = () => {
return (
<Route render={({ staticContext }) => {
if (staticContext) {
staticContext.status = 302;
}
return <Redirect from="/list" to="/users" /> # react redirect
}}/>
);
};
export default ListToUsers;
複製代碼
server 端:
# server.js
import express from 'express';
import request from 'request';
import React from 'react';
import { renderToString } from 'react-dom/server';
import StaticRouter from 'react-router-dom/StaticRouter';
import { renderRoutes } from 'react-router-config';
import routes from './routes'; # 上文定義的routes配置;
const router = express.Router();
router.get('*', (req, res) => {
let context = {};
const content = renderToString(
<StaticRouter location={req.url} context={context}>
{renderRoutes(routes)}
</StaticRouter>
);
if(context.status === 404) {
res.status(404);
}
+ if (context.status === 302) { # 獲取狀態碼並響應;
+ return res.redirect(302, context.url);
+ }
res.render('index', {title: 'Express', data: false, content });
});
module.exports = router;
複製代碼
關於react-router-v4 如何作 server side render就介紹到這裏啦,示例中有部分爲僞代碼。v4 讓你更加靈活的處理多頁面。