溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

如何使用React Router4實現(xiàn)服務(wù)端渲染ssr

發(fā)布時間:2021-07-01 11:24:00 來源:億速云 閱讀:193 作者:小新 欄目:web開發(fā)

這篇文章將為大家詳細(xì)講解有關(guān)如何使用React Router4實現(xiàn)服務(wù)端渲染ssr,小編覺得挺實用的,因此分享給大家做個參考,希望大家閱讀完這篇文章后可以有所收獲。

我們已經(jīng)熟悉React 服務(wù)端渲染(SSR)的基本步驟,現(xiàn)在讓我們更進(jìn)一步利用 React RouterV4 實現(xiàn)客戶端和服務(wù)端的同構(gòu)。畢竟大多數(shù)的應(yīng)用都需要用到web前端路由器,所以要讓SSR能夠正常的運行,了解路由器的設(shè)置是十分有必要的

基本步驟

路由器配置

前言已經(jīng)簡單的介紹了React SSR,首先我們需要添加ReactRouter4到我們的項目中

$ yarn add react-router-dom

# or, using npm
$ npm install react-router-dom

接著我們會描述一個簡單的場景,其中組件是靜態(tài)的且不需要去獲取外部數(shù)據(jù)。我們會在這個基礎(chǔ)之上去了解如何完成取到數(shù)據(jù)的服務(wù)端渲染。

在客戶端,我們只需像以前一樣將我們的的App組件通過ReactRouter的BrowserRouter來包起來。

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ù)端我們將采取類似的方式,但是改為使用無狀態(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è)置一個空對象給context。context對象用于存儲特定的路由信息,這個信息將會以staticContext的形式傳遞給組件

運行一下程序看看結(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)在如果你運行一下程序($ yarn run dev),我們的路由在服務(wù)端被渲染,這是我們所預(yù)期的。

利用404狀態(tài)來處理未找到資源的網(wǎng)絡(luò)請求

我們做一些改進(jìn),當(dāng)渲染NotFound組件時讓服務(wù)端使用404HTTP狀態(tài)碼來響應(yīng)。首先我們將一些信息放到NotFound組件的staticContext

import React from 'react';

export default ({ staticContext = {} }) => {
 staticContext.status = 404;
 return <h2>Oops, nothing here!</h2>;
};

然后在服務(wù)端,我們可以檢查context對象的status屬性是否是404,如果是404,則以404狀態(tài)響應(yīng)服務(wù)端請求。

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>`)
  );
 });
});

// ...

重定向

補充一下,我們可以做一些類似重定向的工作。如果我們有使用Redirect組件,ReactRouter會自動添加重定向的url到context對象的屬性上。

server/index.js (部分)

if (context.url) {
 return res.redirect(301, context.url);
}

讀取數(shù)據(jù)

有時候我們的服務(wù)端渲染應(yīng)用需要數(shù)據(jù)呈現(xiàn),我們需要用一種靜態(tài)的方式來定義我們的路由而不是只涉及到客戶端的動態(tài)的方式。失去定義動態(tài)路由的定義是服務(wù)端渲染最適合所需要的應(yīng)用的原因(譯者注:這句話的意思應(yīng)該是SSR不允許路由是動態(tài)定義的)。

我們將使用fetch在客戶端和服務(wù)端,我們增加isomorphic-fetch到我們的項目。同時我們也增加serialize-javascript這個包,它可以方便的序列化服務(wù)器上獲取到的數(shù)據(jù)。

$ yarn add isomorphic-fetch serialize-javascript

# or, using npm:
$ npm install isomorphic-fetch serialize-javascript

我們定義我們的路由信息為一個靜態(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)在有一個叫l(wèi)oadData的鍵,它是一個調(diào)用loadData函數(shù)的函數(shù)。這個是我們的loadData函數(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);
  });
};

我們簡單的使用fetch來從REST API 獲取數(shù)據(jù)

在服務(wù)端我們將使用ReactRouter的matchPath去尋找當(dāng)前url所匹配的路由配置并判斷它有沒有l(wèi)oadData屬性。如果是這樣,我們調(diào)用loadData去獲取數(shù)據(jù)并把數(shù)據(jù)放到全局window對象中在服務(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}`);
});

請注意,我們添加組件的數(shù)據(jù)到context對象。在服務(wù)端渲染中我們將通過staticContext來訪問它。

現(xiàn)在我們可以在需要加載時獲取數(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;

工具類

ReactRouterConfig是由ReactRouter團(tuán)隊提供和維護(hù)的包。它提供了兩個處理ReactRouter和SSR更便捷的工具matchRoutes和renderRoutes。

matchRoutes

前面的例子都非常簡單都,都沒有嵌套路由。有時在多路由的情況下,使用matchPath是行不通的,因為它只能匹配一條路由。matchRoutes是一個能幫助我們匹配多路由的工具。

這意味著在匹配路由的過程中我們可以往一個數(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)路由配置對象并返回所需的Route組件。為了matchRoutes能適當(dāng)?shù)墓ぷ鱮enderRoutes應(yīng)該被使用。

通過使用renderRoutes,我們的程序改成了一個更簡潔的形式。

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組件的生命周期不會運行到componentDidMount,componentDidMount只有在客戶端才會運行。

  • React16不再推薦使用componentWillMount方法,應(yīng)使用constructor來代替。

  • staticContext的實現(xiàn)應(yīng)該跟redux的高階組件connect類似,也是通過包裝一層react控件來實現(xiàn)子組件的屬性傳遞。

  • 文章只是對SSR做了一個入門的介紹,如Loadable和樣式的處理在文章中沒有介紹,但這兩點對于SSR來說很重要

關(guān)于“如何使用React Router4實現(xiàn)服務(wù)端渲染ssr”這篇文章就分享到這里了,希望以上內(nèi)容可以對大家有一定的幫助,使各位可以學(xué)到更多知識,如果覺得文章不錯,請把它分享出去讓更多的人看到。

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI