Как я создал свое первое приложение React: изоморфизм

Часть 4: Изоморфизм

Это четвертая и последняя статья из серии Как я создал свое первое приложение React, в которой показаны шаги, которые я предпринял, пытаясь создать изоморфное приложение для голосования с помощью React. Весь код доступен в моем репозитории GitHub: question-it.

В этой статье я расскажу об изоморфизме: что такое изоморфное приложение и как его можно создать с помощью React, GraphQL и Relay.

вступление

Изоморфное приложение — это приложение, которое может работать как на клиенте, так и на сервере. С появлением и ростом популярности Node.js JavaScript стал надежным серверным языком. Это, наряду с доминированием над клиентскими веб-приложениями, сделало разработку изоморфных приложений JavaScript выполнимой.

Почему?

Традиционно веб-приложения становились доступными благодаря почти чистому рендерингу на стороне сервера. Клиент запросит страницу, и сервер будет использовать данные, которые у него есть, для отображения html, который будет отправлен клиенту. Это можно было бы сделать с помощью ASP.NET, Java JSP или любой другой серверной технологии. Недостатком этого является то, что при большом количестве пользователей приложению потребуется очень мощная инфраструктура для обработки нагрузки, даже если само приложение не будет сложным.

Позже началась эра рендеринга на стороне клиента. С интерфейсными фреймворками, такими как AngularJS и Ember, создание SPA (одностраничных приложений) стало проще, чем когда-либо. Клиент запрашивает ресурс, а сервер просто отправляет базовый html-скелет с кучей файлов JavaScript, благодаря которым все работает, а в случае SPA даже обрабатывает маршрутизацию внутри приложения.

На сервере было гораздо меньше работы, и разработчик мог сэкономить немного ресурсов и денег. Но клиентские приложения тоже не были идеальными. Поскольку от клиента требовалось загрузить почти весь код приложения, первоначальный рендеринг приложения отнимал много времени, а пользовательский опыт ухудшился. Это было более заметно, так как приложение стало больше.

А потом появились изоморфные приложения. С приложениями, которые можно запускать как на сервере, так и на клиенте, приложения JavaScript могут использовать преимущества обоих подходов и лишь некоторые недостатки. Когда клиент запрашивает ресурс, наша библиотека рендеринга запускается на сервере и генерирует html для отправки клиенту вместе со всем кодом приложения. Клиенту не нужно было бы ждать, как он делал бы с чистым приложением на стороне клиента. А что с сервером? Что ж, после того, как сервер выполнит первоначальный рендеринг, большая часть остальной работы будет выполняться на клиенте, так же, как это делается с приложениями Angular.

Изоморфные реагирующие приложения или изоморфные приложения в целом можно организовать с помощью трех папок:

  • client: содержит весь код, специфичный для клиентской стороны. Установка корневого компонента React на какой-либо элемент DOM.
  • сервер: содержит весь специфичный для сервера код. Обработка запросов и обслуживание html.
  • общий: содержит код, который будет использоваться как на клиенте, так и на сервере. Компоненты React и другой код приложения можно найти здесь.

Итак, как делать изоморфные приложения с помощью React?

Изоморфное здание

Наличие кода, который работает как на клиенте, так и на сервере, означает, что мы должны создавать код для обеих сред: браузера и Node.js. Это связано с тем, что веб-приложения, созданные с помощью webpack, не будут работать на Node.js.

К счастью, webpack можно настроить для сборки кода для Node.js, используя параметр конфигурации:

target: ‘node’, // defaults to ‘web’

Это означает, что вам придется создать другую конфигурацию веб-пакета для сервера. Это может быть несколько утомительно, так как повторяющийся код приводит к необходимости внесения изменений в оба файла. Кроме того, это может быть довольно сложно для начинающих.

К счастью, благодаря пользователю GitHub halt-hammerzeit, создание изоморфных приложений снова стало простым. Он создал инструмент под названием universal-webpack, который преобразует конфигурацию веб-пакета клиента в конфигурацию веб-пакета сервера.



Всю необходимую документацию можно найти в репозитории.

Изоморфная реакция

Чтобы заставить изоморфизм работать с React, обычно нужно возиться только с кодом на стороне сервера. В клиентском коде код должен быть почти таким же:

import ReactDOM from 'react-dom’;
import App from '../shared/components/root’;
ReactDOM.render(<App />, document.getElementByID('react-root'));

На сервере мы не можем монтировать компоненты, вместо этого мы визуализируем наш компонент в строку, которая затем будет отправлена ​​клиенту в виде HTML. В библиотеке react-dom для этого есть специальная функция: renderToString.

Соответствующий код будет выглядеть следующим образом (обратите внимание, что этот код не использует библиотеку маршрутизации, которая сопоставляет компонент с URL-адресом):

import { renderToString } from 'react-dom/server’;
import ComponentToRender from '../shared/components/componentToRender’;
/*
 * INSERT MORE CODE HERE
 */
app.get('some url', (req, res, next) => {
  // this url matches the component that should be renderd
  const componentHTML = renderToString(InitialComponent);
  const HTML = `
      <!DOCTYPE html>
      <html>
        <head>
          /* insert meta, link, style and script tags here */
          <script type="application/javascript" src="bundle.js"/>
        </head>
        <body>
          <div id="react-root">${componentHTML}</div> 
          /* This div will later be used to mount the entire application using the client code */          
          /* insert script tags here */
        </body>
      </html>
    `;
  res.end(HTML);
});

Изоморфная маршрутизация

В настоящее время многие приложения являются одностраничными приложениями: приложения, которые по существу представляют собой одну страницу, отправляемую с сервера клиенту. В этих приложениях почти вся обработка маршрутов выполняется на клиенте с использованием некоторой библиотеки маршрутизации. В нашем случае самой популярной библиотекой маршрутизации для React является React Router.



Я не собираюсь учить вас, как использовать React Router здесь, только как его можно использовать для изоморфных приложений. Если что-то непонятно, вам, вероятно, следует перейти к репозиторию React Router на GitHub, где вы можете найти очень подробную документацию.

Обычно клиентский код остается нетронутым, но в случае использования асинхронных маршрутов перед рендерингом необходимо убедиться, что асинхронное поведение разрешено. Это можно сделать с помощью функции match:

import ReactDOM from 'react-dom';
import { Router, browserHistory, match } from 'react-router';
import routes from '../shared/routes';
match({ history, routes }, (error, redirectLocation, renderProps) => {
  render(<Router {...renderProps} />, document.getElementById('react-root'))
});

Код сервера выглядит одинаково независимо от того, используете ли вы асинхронные маршруты или нет. На сервере мы отправляем URL-адрес запроса в match, который вызывает обратный вызов со всей информацией, необходимой для первоначального рендеринга:

import { renderToString } from 'react-dom/server’;
import routes from '../shared/routes';
/*
 * INSERT MORE CODE HERE
 */
// react-rounder will handle the routing
app.get('*', (req, res, next) => {
match({ routes, location: req.url }, (error, redirectLocation, renderProps) => {
    // react-router encountered an error
    if (error) {
      res.status(500).send(error.message);
    }
    // redirect from Redirect or IndexRedirect
    else if (redirectLocation) {
      res.redirect(302, redirectLocation.pathname + redirectLocation.search);
    }
    // found a route, RouterContext will render the needed components according to renderProps
    else if (renderProps) {
     const componentHTML = renderToString(<RouterContext {...renderProps} />);
      const HTML = `
      <!DOCTYPE html>
      <html>
        <head>
          /* insert meta, link, style and script tags here */
          <script type="application/javascript" src="bundle.js"/>
        </head>
        <body>
          <div id="react-root">${componentHTML}</div> 
          /* This div will later be used to mount the entire application using the client code */          
          /* insert script tags here */
        </body>
      </html>
    `;
     res.status(200).send(HTML);
    }
    // didn't match any route
    else {
      res.status(404).send('Not found');
    }
  });
});

И теперь у вас есть изоморфная маршрутизация!

Изоморфная эстафета

Если вы используете Relay (как и я) для выборки данных в вашем приложении React. Возможно, вы задавались вопросом, можно ли сделать выборку данных изоморфной. Это означает, что когда сервер будет отображать компонент React, он также разрешит и передаст ему начальные данные, которые требуются компоненту. Таким образом, клиенту не нужно будет отправлять еще один запрос данных после первоначального рендеринга.

На момент написания Relay не поддерживал рендеринг на стороне сервера. К счастью, как и во многих других проектах с открытым исходным кодом, вы можете рассчитывать на то, что кто-то позаботится об этом:



Именно это и делает пакет isomorphic-relay от denvned. Isomorphic Relay выполняет это, используя другой сетевой уровень ретрансляции на сервере, который сохраняет разрешенные запросы данных и отправляет эти данные клиенту вместе с визуализированным компонентом. Затем клиент вводит эти данные в свое хранилище Relay, и, поскольку Relay умен, он больше не будет запрашивать эти данные.

Для пользователей React Router, которые интегрируют React Router с Relay с помощью react-router-relay, также есть isomorphic-relay-router, также созданный denvned. Isomorphic Relay Router добавляет поддержку рендеринга на стороне сервера в react-router-relay с помощью isomorphic-relay. Подобно обычному пакету Isomorphic Relay, Isomorphic Relay Router требует очень простой настройки:

app.get('*', (req, res, next) => {
match({ routes, location: req.url }, (error, redirectLocation, renderProps) => {
    if (error) {
      res.status(500).send(error.message);
    }
    else if (redirectLocation) {
      res.redirect(302, redirectLocation.pathname + redirectLocation.search);
    }
    else if (renderProps) {
       IsomorphicRouter.prepareData(renderProps, networkLayer).then(render).catch(next);
    }
    else {
      res.status(404).send('Not found');
    }
    
    function render({ data, props }) {
      const componentHTML = renderToString(IsomorphicRouter.render(props));
      const HTML = `
      <!DOCTYPE html>
      <html>
        <head>
          /* insert meta, link, style and script tags here */
          <script type="application/javascript" src="bundle.js"/>
        </head>
        <body>
          <div id="react-root">${componentHTML}</div> 
          // passing the data to the client
          <script id="preloadedData">
            ${JSON.stringify(data)}
          </script>     
          /* insert script tags here */
        </body>
      </html>
      `;
      res.status(200).send(HTML);
    }
  });
});

И как только у клиента есть данные, ему просто нужно ввести их в хранилище ретрансляции и отобразить:

import IsomorphicRouter from 'isomorphic-relay-router';

const environment = new Relay.Environment();

environment.injectNetworkLayer(new Relay.DefaultNetworkLayer('/graphql'));

const data = JSON.parse(document.getElementById('preloadedData').textContent);

IsomorphicRelay.injectPreparedData(environment, data);

const rootElement = document.getElementById('react-root');

match({routes, history: browserHistory}, (error, redirectLocation, renderProps) => {
  IsomorphicRouter.prepareInitialRender(environment, renderProps).then(props => {
    ReactDOM.render(<Router {...props} />, rootElement);
  });
});

Изоморфный стиль

Изоморфный стиль? Что это? Как сервер может отображать css, и даже если может, что бы это вообще значило?



Загрузка изоморфного стиля — это в значительной степени выдуманный термин, который относится к рендерингу критического пути CSS во время рендеринга на стороне сервера. CSS критического пути — это часть файлов CSS приложения, которая имеет решающее значение для того, что отображается для пользователя. Используя обычный загрузчик стилей webpack, все импортированные файлы css внедряются в HTML как теги стиля. С другой стороны, использование isomorphic-style-loader от kriasoft будет внедрять только критический CSS, поэтому CSS несмонтированных компонентов не будет внедряться. Это оптимизирует рендеринг критического пути и обеспечивает лучшее взаимодействие с пользователем и меньшее время загрузки.

Чтобы изоморфный-стиль-загрузчик заработал, на сервере нужно передать компонентам функцию insertCss в качестве контекста React:

app.get('*', (req, res, next) => {
match({ routes, location: req.url }, (error, redirectLocation, renderProps) => {
    if (error) {
      res.status(500).send(error.message);
    }
    else if (redirectLocation) {
      res.redirect(302, redirectLocation.pathname + redirectLocation.search);
    }
    else if (renderProps) {
       IsomorphicRouter.prepareData(renderProps, networkLayer).then(render).catch(next);
    }
    else {
      res.status(404).send('Not found');
    }
    
    function render({ data, props }) {
      const css = [];
      const InitialComponent = (
        <Root onInsertCss={(styles) => css.push(styles._getCss())}>
          {IsomorphicRouter.render(props)}
        </Root>
      );
      const componentHTML = renderToString(InitialComponent);
      const HTML = `
      <!DOCTYPE html>
      <html>
        <head>
          /* insert meta, link, style and script tags here */
          <script type="application/javascript" src="bundle.js"/>
          <style type="text/css">${css.join('')}</style>
        </head>
        <body>
          <div id="react-root">${componentHTML}</div> 
          // passing the data to the client
          <script id="preloadedData">
            ${JSON.stringify(data)}
          </script>     
          /* insert script tags here */
        </body>
      </html>
      `;
      res.status(200).send(HTML);
    }
  });
});

Здесь функция insertCss передается как реквизит корневому компоненту, который просто передаст этот реквизит в контекст.

Затем осталось экспортировать контейнер для каждого компонента, который будет использовать insertCss:

import React from 'react';
import withStyles from 'isomorphic-style-loader/lib/withStyles';
import s from './MyComponent.css';

class MyComponent extends React.Component {
  ...
}
export default withStyles(s)(MyComponent);

withStyles просто создает контейнер для компонента и вставляет заданный css при монтировании компонента.

Это в основном все, что нужно знать о создании изоморфных приложений с помощью React, но если я что-то пропустил, буду рад услышать!

И это конец моей серии статей Как я создал свое первое приложение React, обязательно ознакомьтесь с ней: