[Из песочницы] Знакомство с React Loadable
Единый файл сборки и сборка из нескольких файлов
От переводчика: я позволил себе не переводить некоторые глаголы и термины, повсеместно использующиеся в непереведенной транслитерованной форме (как, например, «прелоадер» и «рендеринг»), полагаю, они будут понятны даже пользователям, читающим исключительно материалы на русском языке.
С ростом кодовой базы вашего приложения наступает момент, когда единый файл сборки начинает негативно влиять на скорость первоначальной загрузки страницы. Разделение кода на несколько файлов и их динамическая загрузка позволяет решить эту проблему.
Современные инструменты, такие как Browserify и Webpack, неплохо справляются с этой задачей.
При этом, вам предстоит определиться, по какому принципу фрагменты кода будут объединены в один файл и как ваше приложение будет узнавать об успешной загрузке очередного фрагмента и обрабатывать это событие.
Роут-ориентированный и компонент-ориентированный подход к разделению кода
Сейчас практически повсеместно используется роут-ориентированный подход. Т.е. фрагменты вашего кода выделяются в отдельные файлы исходя из принципа, что код в каждом файле необходим для работы определенной страницы (роута) вашего приложения. И использование такого подхода довольно логично, ведь клик по ссылке, и, после этого ожидание загрузки страницы — стандартное и привычное действие для пользователя.
Но ведь нет предела совершенству, почему бы не попытаться усовершенствовать и этот подход?
Для приложений на React.js роут — это всего лишь компонент. А это значит, что мы можем попробовать использовать компонент-ориентированный подход к разделению кода.
Сборка при роут-ориентированном и компонент-ориентированном подходе
Существует масса компонентов, код которых логично было бы выделить в отдельный файл. Модальные окна, неактивные табы и многие другие элементы интерфейса первоначально скрыты и могут и вовсе не появится на странице, если пользователь не предпримет определенных действий.
Так почему же мы должны грузить код этих компонентов (и, возможно, код сторонних библиотек для работы с этими компонентами) сразу же при заходе на страницу?
При этом, с компонент-ориентированным подходом, разделение кода по роутам приложения также прекрасно работает, ведь, как мы упомянули выше, роут — это всего лишь компонент. Так что просто делайте так, как будет лучше для вашего приложения.
И конечно же, вам бы хотелось, чтобы для реализации компонент-ориентированного разделения кода не приходилось затрачивать много усилий. Чтобы для выделения компонента в отдельный фрагмент кода достаточно было изменить несколько строк в вашем приложении, а прочая «магия» выполнилась бы «под капотом» без вашего участия.
Введение в React Loadable
Я написал маленькую библиотеку — React Loadable, которая позволяет сделать все именно так, как вы хотите.
Предположим, у нас есть два компонента. Мы импортируем компонент AnotherComponent
и используем его в методе render
компонента MyComponent
.
import AnotherComponent from './another-component';
class MyComponent extends React.Component {
render() {
return ;
}
}
Сейчас импорт происходит синхронно. Нам же нужен способ сделать его асинхронным.
Сделать это можно используя динамический импорт. Изменим код 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 Loading...;
} else {
return ;
};
}
}
Однако такая реализация асинхронной загрузки требует довольно массивных изменений в коде компонента. А ведь мы еще не описали случай, если динамический импорт завершится неудачей. А что если нам еще и понадобится рендерить приложение на стороне сервера (server-side rendering)?
Используя Loadable
— компонент высшего порядка (если вы работаете с react.js, вам наверняка знаком этот термин и принцип использования таких компонентов, если же нет — прочитайте соответствующий раздел документации, либо просто примите к сведению, что это функция, которая принимает react компонент на вход, и на его основе возвращает новый компонент, с расширенным поведением — прим. пер.) из моей библиотеки вы сможете абстрагироваться от всех этих проблем.
Loadable
работает предельно просто.
Минимум, что от вас потребуется, это передать в Loadable
объект со свойствами loader
— функция, которая выполняет динамический импорт компонента и LoadingComponent
— компонент, который будет показан в процессе загрузки.
import Loadable from 'react-loadable';
function MyLoadingComponent() {
return Loading...;
}
const LoadableAnotherComponent = Loadable({
loader: () => import('./another-component'),
LoadingComponent: MyLoadingComponent
});
class MyComponent extends React.Component {
render() {
return ;
}
}
Но что произойдет, если загрузка компонента завершиться неудачей?
Мы должны будем как-то обработать ошибку и известить об этом пользователя.
В случае возникновения ошибки она будет передана в LoadingComponent как свойство компонента (prop).
function MyLoadingComponent({ error }) {
if (error) {
return Error!;
} else {
return Loading...;
}
}
Webpack второй версии умеет «из коробки» работать с динамическими импортами — выделять код, подключаемый через динамические импорты в отдельные файлы и загружать их в рантайме. Это означает, что вы с легкостью сможете поэкспериментировать и подобрать оптимальную для производительности вашего приложения схему разделения кода.
Вы можете увидеть рабочий пример, запустив демонстрационный проект из моего репозитория. А если вы хотите детально разобраться в том, как Webpack 2 работает с динамическими импортами, ознакомьтесь со следующими разделами его официальной документации.
Предотвращение «предзагрузочного мерцания» компонента
При определенных условиях загрузка компонента может происходить очень быстро (менее чем за 200 миллисекунд); это приведет к кратковременному появлению на экране компонента-прелоадера, который мгновенно заменится на загруженный компонент. Глаз пользователя не успеет увидеть прелоадер, он лишь заметит некое «мерцание» перед загрузкой компонента.
Ряд исследований доказали, что при таком поведении пользователь субъективно воспринимает время загрузки дольше, чем это происходит фактически. Т.е. для этого случая, лучше не показывать прелоадер вовсе, а просто дождаться загрузки компонента.
MyLoadingComponent
также имеет свойство pastDelay
которое установится в true
в момент, когда с начала загрузки компонента пройдет 200 миллисекунд (мне видится, что это решение не в полной мере рабочее — ведь если загрузка компонента будет занимать 400 миллисекунд, то после 200 миллисекундного ожидания, прелоадер покажется на экране на те же 200 миллисекунд — размышления переводчика).
export default function MyLoadingComponent({ error, pastDelay }) {
if (error) {
return Error!;
} else if (pastDelay) {
return Loading...;
} 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 (
{this.state.showComponent && }
)
}
}
Рендеринг на стороне сервера
Моя библиотека также поддерживает и рендеринг на стороне сервера.
Для этого в объекте, передаваемом в качестве аргумента в функцию 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