前言
本文旨在幫助讀者在react router v4下搭建自己的滿足seo需求的server side render app。
react
不多說了,近年來發展迅猛,被各大主流應用廣泛使用;
react-router-v4
react路由做了一次較大的改動,如果你的app是16年或者更早搭建的話,相信路由還是v3版本,不過沒有關係,你可以參照本文嘗試現在就升級你的router,v4會給你帶來更多的靈活性。
與v3相比,v4主要有以下更改:
- 宣告式路由;
- 無需集中配置,分散式路由;
- 路由支援正則匹配;
匹配規則
- 對於web app和react native app來說,是獨立的包;
- 取消了原本的onEnter 和 onChange回撥函式,你需要使用component的生命週期函式來實現同樣的功能;
本文假設你已經熟悉react,redux和express,並且構建過自己的app;
搭建
1. 安裝React Router v4;
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
就好。
2. 配置路由;
如果你在用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 讓你更加靈活的處理多頁面。