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

IOException發表於2018-02-25

前言

本文旨在幫助讀者在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 讓你更加靈活的處理多頁面。

相關文章