Thứ hai, 04/06/2018 | 00:00 GMT+7

Sử dụng Bộ định tuyến React 4 với Kết xuất phía server


Bây giờ ta đã xem xét cài đặt cơ bản cho kết xuất phía server React (SSR), hãy nâng cao mọi thứ và xem cách sử dụng React Router v4 trên cả client và server . Rốt cuộc, hầu hết các ứng dụng thực tế đều cần định tuyến, vì vậy chỉ có ý nghĩa khi tìm hiểu về cách cài đặt định tuyến để nó hoạt động với kết xuất phía server .

Cài đặt cơ bản

Ta sẽ bắt đầu những thứ mà ta đã để mọi thứ trong phần giới thiệu của ta về React SSR, nhưng trên hết cài đặt đó, ta cũng cần thêm React Router 4 vào dự án của bạn :

$ yarn add react-router-dom

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

Và tiếp theo, ta sẽ cài đặt một kịch bản định tuyến đơn giản trong đó các thành phần của ta là tĩnh và không cần phải tìm nạp dữ liệu bên ngoài. Sau đó, ta sẽ xây dựng dựa trên đó để xem cách ta sẽ cài đặt mọi thứ cho các tuyến thực hiện một số tìm nạp dữ liệu khi kết xuất.

Ở phía client , ta chỉ cần bọc thành phần Ứng dụng của ta bằng thành phần BrowserRouter của React Router, như thường lệ:

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')
);

Và sau đó trên server , ta sẽ sử dụng thành phần StaticRouter tương tự, nhưng không trạng thái:

server / index.js
import React from 'react';
import express from 'express';
import ReactDOMServer from 'react-dom/server';
import { StaticRouter } from 'react-router-dom';

// ...other imports and Express config

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

Thành phần StaticRouter mong đợi một location và một chỗ dựa context . Ta chuyển url hiện tại (Express ' req.url ) đến chỗ dựa location và một đối tượng trống cho chỗ dựa context . Đối tượng context rất hữu ích để lưu trữ thông tin về một lộ trình cụ thể hiển thị và thông tin đó sau đó sẽ được cung cấp cho thành phần dưới dạng một staticContext prop.


Để kiểm tra xem mọi thứ có hoạt động như ta mong đợi hay không, hãy thêm một số tuyến vào thành phần Ứng dụng của ta :

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

Ta đang sử dụng thành phần Switch để chỉ hiển thị một tuyến đường phù hợp.

Bây giờ nếu bạn kiểm tra cài đặt này ( $ yarn run dev ), bạn sẽ thấy rằng mọi thứ đang hoạt động như mong đợi và các tuyến của ta đang được hiển thị phía server .

Cung cấp NotFound bằng trạng thái 404

Ta có thể cải thiện một chút về mọi thứ và phân phát nội dung với mã trạng thái HTTP là 404 khi hiển thị thành phần NotFound . Đầu tiên, đây là cách ta có thể đính kèm một số dữ liệu vào staticContext trong thành phần NotFound :

src / NotFound.js
import React from 'react';

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

Sau đó, trên server , ta có thể kiểm tra trạng thái 404 trên đối tượng context và cung cấp file có trạng thái 404 nếu kiểm tra của ta đánh giá là true:

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

// ...

Chuyển hướng

Một lưu ý nhỏ là bạn có thể làm điều gì đó tương tự để đối phó với chuyển hướng. React Router tự động thêm thuộc tính url với url được chuyển hướng vào đối tượng ngữ cảnh khi thành phần Chuyển hướng được sử dụng:

server / index.js (một phần)
if (context.url) {
  return res.redirect(301, context.url);
}

Đang tải dữ liệu

Trong trường hợp một số tuyến của ứng dụng của ta cần tải dữ liệu khi hiển thị, ta cần một cách tĩnh để xác định các tuyến của ta thay vì cách làm việc đó khi chỉ có khách hàng tham gia. Việc mất khả năng xác định các tuyến động là một lý do tại sao kết xuất phía server được lưu giữ tốt nhất cho các ứng dụng thực sự cần nó.


Vì ta sẽ sử dụng tìm nạp trên cả client và server , hãy thêm isomorphic-fetch vào dự án. Ta cũng sẽ thêm gói serialize-javascript , gói này sẽ rất hữu ích để tuần tự hóa dữ liệu đã tìm nạp của ta trên server :

$ yarn add isomorphic-fetch serialize-javascript

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

Hãy xác định các tuyến đường của ta dưới dạng một mảng tĩnh trong file routes.js :

src / route.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;

Một số tuyến của ta hiện có khóa loadData trỏ đến một hàm gọi hàm loadData . Đây là cách triển khai của ta cho loadData :

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

Ta chỉ sử dụng API tìm nạp để lấy một số dữ liệu từ API REST.

Trên server , ta sẽ sử dụng matchPath của React Router để tìm đường hiện tại và xem nó có thuộc tính loadData . Nếu đúng như vậy, ta gọi loadData để lấy dữ liệu và thêm nó vào phản hồi của server bằng cách sử dụng một biến được đính kèm với đối tượng window :

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 => {
    // Let's 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}`);
});

Lưu ý cách bây giờ ta cũng thêm dữ liệu đã tải của thành phần vào đối tượng ngữ cảnh. Ta sẽ truy cập điều này từ staticContext khi hiển thị trên server .

Bây giờ, trong các thành phần của ta cần tìm nạp dữ liệu khi tải, ta có thể thêm một số logic đơn giản hàm tạo của chúng và phương thức vòng đời componentDidMount của chúng:

Dưới đây là một ví dụ với thành phần Todos của ta :

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;

Khi kết xuất trên server , ta có thể truy cập dữ liệu từ props.staticContext.data vì ta đã đưa vào đối tượng ngữ cảnh của StaticBrowser.

Có một chút logic hơn đang diễn ra với phương thức componentDidMount . Lưu ý phương thức này chỉ được gọi trên client . Nếu __ROUTE_DATA__ được đặt trên đối tượng cửa sổ chung, điều đó nghĩa là ta đang bù nước sau khi server kết xuất và ta có thể lấy dữ liệu trực tiếp từ __ROUTE_DATA__ và sau đó xóa nó. Nếu __ROUTE_DATA__ không được đặt, thì ta đã đến trên tuyến đường đó bằng cách sử dụng định tuyến phía client , server hoàn toàn không liên quan và ta cần tiếp tục và tìm nạp dữ liệu.

Một điều thú vị khác ở đây là việc sử dụng setTimeout với giá trị trễ là 0ms. Đây chỉ là để ta có thể đánh dấu JavaScript tiếp theo đảm bảo rằng __ROUTE_DATA__ khả dụng.

Cấu hình bộ định tuyến phản ứng

Có một gói có sẵn và được duy trì bởi group React Router, React Router Config , cung cấp hai tiện ích giúp xử lý React Router và SSR dễ dàng hơn nhiều: matchRoutesrenderRoutes .

matchRoutes

Các tuyến đường trong ví dụ trước của ta khá đơn giản và không có các tuyến đường lồng nhau. Trong trường hợp nhiều tuyến đường có thể được hiển thị cùng một lúc, việc sử dụng matchPath sẽ không hoạt động vì nó sẽ chỉ trùng với một tuyến đường. matchRoutes là một tiện ích giúp kết hợp nhiều tuyến đường có thể.

Điều đó nghĩa là thay vào đó ta có thể điền vào một mảng với các hứa hẹn cho các tuyến phù hợp và sau đó gọi Promise.all trên tất cả các tuyến phù hợp để giải quyết lời hứa loadData của mỗi tuyến phù hợp.

Một chút gì đó như thế này:

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

Tiện ích renderRoutes nhận đối tượng cấu hình tuyến tĩnh của ta và trả về các thành phần tuyến cần thiết. renderRoutes nên được sử dụng để matchRoutes hoạt động bình thường.

Vì vậy, với renderRoutes thành phần Ứng dụng của ta sẽ thay đổi thành version đơn giản hơn này:

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

Nếu bạn cần một tài liệu tham khảo tốt về những gì ta đã làm ở đây, hãy xem phần Kết xuất Server của tài liệu Bộ định tuyến React.


Tags:

Các tin liên quan

Cách cài đặt Linux, Nginx, MySQL, PHP ( LEMP) trên Ubuntu 18.04
2018-05-23
server Express cơ bản trong Node.js
2018-05-04
Thiết lập server ban đầu với Ubuntu 18.04
2018-04-27
Tự động thiết lập server ban đầu với Ubuntu 18.04
2018-04-27
Bắt đầu với kết xuất phía server bằng Nuxt.js
2018-04-16
Cách bảo vệ server của bạn trước lỗ hổng Meltdown và Spectre
2018-01-10
Sơ lược về lịch sử Linux
2017-10-27
Cách thiết lập Shiny Server trên Ubuntu 16.04
2017-10-25
Cách thiết lập server lưu trữ đối tượng bằng Minio trên Ubuntu 16.04
2017-08-30
Cách chạy server MongoDB an toàn với OpenVPN và Docker trên Ubuntu 16.04
2017-03-24