Привет, Хабр. Представляю вам свободный перевод статьи Джеймса Кайла «Introducing React Loadable». В ней Джеймс рассказывает, что такое компонент-ориентированный подход к разделению кода и представляет разработанную им библиотеку Loadable — инструмент, позволяющий реализовать этот подход в React.js приложениях.

image
Единый файл сборки и сборка из нескольких файлов

От переводчика: я позволил себе не переводить некоторые глаголы и термины, повсеместно использующиеся в непереведенной транслитерованной форме (как, например, «прелоадер» и «рендеринг»), полагаю, они будут понятны даже пользователям, читающим исключительно материалы на русском языке.

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

Современные инструменты, такие как Browserify и Webpack, неплохо справляются с этой задачей.

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

Роут-ориентированный и компонент-ориентированный подход к разделению кода


Сейчас практически повсеместно используется роут-ориентированный подход. Т.е. фрагменты вашего кода выделяются в отдельные файлы исходя из принципа, что код в каждом файле необходим для работы определенной страницы (роута) вашего приложения. И использование такого подхода довольно логично, ведь клик по ссылке, и, после этого ожидание загрузки страницы — стандартное и привычное действие для пользователя.

Но ведь нет предела совершенству, почему бы не попытаться усовершенствовать и этот подход?

Для приложений на React.js роут — это всего лишь компонент. А это значит, что мы можем попробовать использовать компонент-ориентированный подход к разделению кода.

image
Сборка при роут-ориентированном и компонент-ориентированном подходе

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

Так почему же мы должны грузить код этих компонентов (и, возможно, код сторонних библиотек для работы с этими компонентами) сразу же при заходе на страницу?

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

И конечно же, вам бы хотелось, чтобы для реализации компонент-ориентированного разделения кода не приходилось затрачивать много усилий. Чтобы для выделения компонента в отдельный фрагмент кода достаточно было изменить несколько строк в вашем приложении, а прочая «магия» выполнилась бы «под капотом» без вашего участия.

Введение в React Loadable


Я написал маленькую библиотеку — React Loadable, которая позволяет сделать все именно так, как вы хотите.

Предположим, у нас есть два компонента. Мы импортируем компонент AnotherComponent и используем его в методе render компонента MyComponent.

import AnotherComponent from './another-component';

class MyComponent extends React.Component {
  render() {
    return <AnotherComponent/>;
  }
}

Сейчас импорт происходит синхронно. Нам же нужен способ сделать его асинхронным.

Сделать это можно используя динамический импорт. Изменим код MyComponent таким образом, чтобы AnotherComponent загружался асинхронно.

class MyComponent extends React.Component {
  state = {
    AnotherComponent: null
  };

  componentWillMount() {
    import('./another-component').then(AnotherComponent => {
      this.setState({ AnotherComponent });
    });
  }
  
  render() {
    let {AnotherComponent} = this.state;
    if (!AnotherComponent) {
      return <div>Loading...</div>;
    } else {
      return <AnotherComponent/>;
    };
  }
}

Однако такая реализация асинхронной загрузки требует довольно массивных изменений в коде компонента. А ведь мы еще не описали случай, если динамический импорт завершится неудачей. А что если нам еще и понадобится рендерить приложение на стороне сервера (server-side rendering)?

Используя Loadable — компонент высшего порядка (если вы работаете с react.js, вам наверняка знаком этот термин и принцип использования таких компонентов, если же нет — прочитайте соответствующий раздел документации, либо просто примите к сведению, что это функция, которая принимает react компонент на вход, и на его основе возвращает новый компонент, с расширенным поведением — прим. пер.) из моей библиотеки вы сможете абстрагироваться от всех этих проблем.

Loadable работает предельно просто.

Минимум, что от вас потребуется, это передать в Loadable объект со свойствами loader — функция, которая выполняет динамический импорт компонента и LoadingComponent — компонент, который будет показан в процессе загрузки.

import Loadable from 'react-loadable';

function MyLoadingComponent() {
  return <div>Loading...</div>;
}

const LoadableAnotherComponent = Loadable({
  loader: () => import('./another-component'),
  LoadingComponent: MyLoadingComponent
});

class MyComponent extends React.Component {
  render() {
    return <LoadableAnotherComponent/>;
  }
}

Но что произойдет, если загрузка компонента завершиться неудачей?

Мы должны будем как-то обработать ошибку и известить об этом пользователя.
В случае возникновения ошибки она будет передана в LoadingComponent как свойство компонента (prop).

function MyLoadingComponent({ error }) {
  if (error) {
    return <div>Error!</div>;
  } else {
    return <div>Loading...</div>;
  }
}

Webpack второй версии умеет «из коробки» работать с динамическими импортами — выделять код, подключаемый через динамические импорты в отдельные файлы и загружать их в рантайме. Это означает, что вы с легкостью сможете поэкспериментировать и подобрать оптимальную для производительности вашего приложения схему разделения кода.

Вы можете увидеть рабочий пример, запустив демонстрационный проект из моего репозитория. А если вы хотите детально разобраться в том, как Webpack 2 работает с динамическими импортами, ознакомьтесь со следующими разделами его официальной документации.

Предотвращение «предзагрузочного мерцания» компонента


При определенных условиях загрузка компонента может происходить очень быстро (менее чем за 200 миллисекунд); это приведет к кратковременному появлению на экране компонента-прелоадера, который мгновенно заменится на загруженный компонент. Глаз пользователя не успеет увидеть прелоадер, он лишь заметит некое «мерцание» перед загрузкой компонента.

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

MyLoadingComponent также имеет свойство pastDelay которое установится в true в момент, когда с начала загрузки компонента пройдет 200 миллисекунд (мне видится, что это решение не в полной мере рабочее — ведь если загрузка компонента будет занимать 400 миллисекунд, то после 200 миллисекундного ожидания, прелоадер покажется на экране на те же 200 миллисекунд — размышления переводчика).

export default function MyLoadingComponent({ error, pastDelay }) {
  if (error) {
    return <div>Error!</div>;
  } else if (pastDelay) {
    return <div>Loading...</div>;
  } else {
    return null;
  }
}

Значение 200 миллисекунд используется по умолчанию, но вы можете изменить его, указав соответствующее свойство:

Loadable({
  loader: () => import('./another-component'),
  LoadingComponent: MyLoadingComponent,
  delay: 300
});

Предварительная загрузка


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

Компонент, созданный вызовом функции Loadable предоставляет статический метод preload для этих целей.

let LoadableMyComponent = Loadable({
  loader: () => import('./another-component'),
  LoadingComponent: MyLoadingComponent,
});

class MyComponent extends React.Component {
  state = { showComponent: false };

  onClick = () => {
    this.setState({ showComponent: true });
  };

  onMouseOver = () => {
    LoadableMyComponent.preload();
  };

  render() {
    return (
      <div>
        <button onClick={this.onClick} onMouseOver={this.onMouseOver}>
          Show loadable component
        </button>
        {this.state.showComponent && <LoadableMyComponent/>}
      </div>
    )
  }
}

Рендеринг на стороне сервера


Моя библиотека также поддерживает и рендеринг на стороне сервера.

Для этого в объекте, передаваемом в качестве аргумента в функцию Loadable в свойстве serverSideRequirePath необходимо указать полный путь к компоненту.

import path from 'path';

const LoadableAnotherComponent = Loadable({
  loader: () => import('./another-component'),
  LoadingComponent: MyLoadingComponent,
  delay: 200,
  serverSideRequirePath: path.join(__dirname, './another-component')
});

В этом случае, если страница, на которой присутствует компонент, будет рендериться на стороне сервера, то загрузка компонента произойдет синхронно, а если же на стороне клиента — асинхронно.

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

Как уже отмечалось выше, если мы хотим, чтобы компонент был отрендерен на стороне сервера, то в свойстве serverSideRequirePath объекта-конфига указывается полный путь к модулю определяющему этот компонент. В библиотеке Loadable доступна функция flushServerSideRequires, вызов которой вернет массив из путей ко всем модулям Loadable-компонентов текущей страницы. При запуске Webpack c флагом --json по завершению сборки будет создан файл output-webpack-stats.json хранящий подробную информацию о сборке. Используя эти данные мы сможем вычислить, какие кусочки сборки необходимы для текущей страницы и подключить их через теги script в html файле сгенерированной страницы (см. пример кода).

Осталась последняя, пока не решенная задача — настроить Webpack, чтобы он мог разруливать все это на клиенте. Шон, я буду ждать твоего сообщения, после публикации этой статьи (здесь автор обращается к Sean Larkin — создателю и основному меинтейнеру проекта Webpack. Сегодня, уже заканчивая перевод, я натолкнулся на вот этот твит и, как я понимаю, это проблема была решена, а реализация рендеринга на стороне сервера еще более упростилась — прим. пер.).

Потенциально, я могу представить массу инструментов и технологий, использование которых в связке с моей библиотекой может сделать ваши приложения еще более крутыми. К примеру, c React Fiber мы сможем не только динамически загружать код компонента, но и реализовать «умный» рендеринг — т.е. определить приоритет, какие части загруженного компонента должны быть отрендерены в первую очередь, и какие отложено, т.е. лишь после того, как будет завершен рендеринг более приоритетных элементов.

В завершение я призываю всех установить и попробовать мою библиотеку, а также поставить «Star» её репозиторию на github.

yarn add react-loadable
# or
npm install --save react-loadable
Поделиться с друзьями
-->

Комментарии (3)


  1. justboris
    05.04.2017 00:42

    Полезная штука!


    Только очень напоминает опцию bundle из Require.js, существующую уже лет 5 как.


    И это всегда печалит в постах формата "Introducing new X". В тексте преподносится как что-то абсолютно новое, как будто раньше эту проблему вообще никак не решали.


    1. marklangovoi
      05.04.2017 23:47

      Думаю не стоит путать описание на этапе конфигурации сборщика и import, который разделит все сам.


    1. PFight77
      09.04.2017 08:45

      Лет 5 назад еще реакта не было: ) Про фичу вебпака разделять на бандлы через динамический импорт здесь только упоминается, статья в целом про обертку для реактовских компонент.