如何使用 react-router-v4 做 server side render

前言

本文旨在帮助读者在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
复制代码

或用yarnreact-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 让你更加灵活的处理多页面。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值