Как загрузить правильные данные на стороне сервера с помощью React и Redux
В тот момент, когда вы окунетесь в мир серверного рендеринга, все может быстро усложниться. Особенно в больших приложениях, которые содержат много вложенных компонентов и вызовов API, и каждый из них вызывается и отображается в браузере только тогда, когда это необходимо. Мы обязательно хотим предварительно загрузить данные, необходимые для отображения заголовка на этом веб-сайте. Но всегда ли мне нужно предварительно загружать данные с нашей домашней страницы? Вы могли найти это сообщение в блоге на Google.com и, возможно, никогда не посетить нашу домашнюю страницу или все другие наши сообщения в блогах сегодня. А как насчет вложенного компонента в этой статье, при каких условиях мы предварительно загружаем его данные? Давайте ответим на эти вопросы.
Первоначальная настройка проекта
Решая эту проблему, мы собираемся использовать экспресс-пакет в качестве нашего веб-сервера и использовать метод React renderToString () для визуализации всех компонентов на стороне сервера.
server.js: получить данные и отобразить веб-страницу.
await store.dispatch(fetchGeneral());
const initialRender = renderToString(
<RenderServerside store={store} location={url} />
);
const initialState = store.getState();
Мы передаем все состояние клиентской стороне через тег скрипта в html-документе, используя функцию serialize () в пакете serialize-javascript. (Этот парсер javascript позволяет нам анализировать больше типов объектов, таких как Date, поэтому я бы рекомендовал использовать этот пакет вместо функции JSON.stringify ().) На стороне клиента мы можем читать объект window.initialState и анализировать его в магазин Redux.
<body>
<script>window.initialState = ${serialize(initialState)};</script>
<div id="root">${initialRender || ''}</div>
<script src="${paths.script}" defer async></script>
</body>
С помощью этих шагов мы можем предварительно загрузить и проанализировать состояние для клиента. Но что нам нужно для предварительной загрузки этой страницы?
Разберем сложность на несколько задач
- В настоящее время мы выполняем только одну выборку перед тем, как начать рендеринг страницы на стороне сервера, но у нас также есть несколько вложенных компонентов на нашем веб-сайте. Это расширяет код в этом файле несколькими операторами if, чтобы решить, какие данные нам нужно получить. Это сделает код неподдерживаемым, поэтому нам будет лучше, если мы позволим компонентам решать это самим.
- Без рендеринга на стороне сервера вы получаете данные на стороне клиента в методе componentDidMount (). При рендеринге на стороне сервера вы используете renderToString () для рендеринга компонентов. Но метод renderToString () не присоединяет визуализированные компоненты к DOM, поэтому метод componentDidMount () никогда не вызывается на стороне сервера. Нам нужен другой способ сделать код в методе componentDidMount () доступным на стороне сервера.
- У вас может быть вложенный компонент, который зависит от данных родительского компонента. Как мы ждем ответов в нашем родительском компоненте и анализируем данные для наших дочерних компонентов?
Разбиение сложности на компоненты
Идеальное место, чтобы решить, какие данные нам нужны, и получить данные на стороне клиента - это метод componentDidMount (). Таким образом, мы можем начать выборку сразу после того, как компонент был смонтирован, или пропустить выборку, если данные уже доступны в магазине.
class App extends Component {
componentDidMount() {
const { name } = this.props;
if (name) return;
this.props.fetchGeneral();
}
Когда мы копируем эту логику на сервер, мы дублируем логику в двух отдельных частях приложения. Компонент и функция рендеринга на стороне сервера. Еще более проблематично то, что мы объединяем логику всех компонентов в одну функцию и делаем файл излишне сложным. У каждого компонента есть свой собственный набор правил, отображать ли дочерний компонент, поэтому эта функция будет значительно расширяться в будущем. Разработчику практически невозможно определить в этой единственной функции, какие данные требуются во всех наших вложенных компонентах, и поддерживать их в будущем. И когда к команде присоединяется новый разработчик, есть большая вероятность, что он или она, вероятно, отредактируют компонент, но также забудут обновить наше дерево решений на стороне сервера. Мы не хотим, чтобы это произошло. Итак, давайте займемся проблемой номер 1 и перенесем эту сложность подальше от сервера.
Есть всего две проблемы:
- Метод didComponentMount () никогда не вызывается, когда мы используем функцию renderToString () React. Поэтому нам нужно вызвать метод didComponentMount () со стороны сервера самостоятельно.
- Нам нужно вызвать этот метод перед выполнением renderToString (), потому что функции renderToString () требуется хранилище с предварительно выбранными данными. Поскольку на этом этапе у нас нет сконструированных компонентов React, нам нужно сделать метод в наших компонентах React статическим.
Итак, давайте займемся проблемой номер 2 и сделаем этот метод доступным на стороне сервера. Мы делаем это, перемещая код в новый статический метод preInitStore (). Таким образом, мы можем выполнить его с помощью кода App.preInitStore () со стороны сервера.
class App extends Component {
static preInitStore() {
this.props.fetchGeneral();
}
Устранение ограничений статического метода
Теперь мы можем вызвать метод App.preInitStore () перед выполнением renderToString (). Но поскольку метод preInitStore() является статическим, у нас также нет ссылки на компонент приложения в этом свойстве и, следовательно, мы не можем вызвать метод this.props.fetchGeneral(). К счастью, есть способ отправить действие из объекта store с помощью метода store.dispatch (). Поэтому нам нужно проанализировать хранилище со стороны сервера в методе preInitStore () в качестве параметра:
await App.preInitStore(store);
return {
renderedString: renderToString(...
… Теперь мы можем выполнить это в нашем методе preInitStore ():
class App extends Component {
static preInitStore(store) {
store.dispatch(fetchGeneral());
Теперь у нас есть метод, который мы можем вызывать со стороны сервера, в то время как вся логика находится в самом компоненте.
(Примечание: поскольку теперь у нас есть статический метод в нашем компоненте, мы также можем совместно использовать другие статические методы между серверным и клиентским кодом внутри компонента.)
Подождем ответа
Важная часть нашего решения все еще отсутствует. Поскольку вызовы выборки в наших действиях являются обещаниями, браузеру необходимо дождаться разрешения этих обещаний, прежде чем мы сможем выполнить метод renderToString(). Мы можем облегчить это, ожидая этих обещаний в нашем методе preInitStore (), а также в создателях действий.
export const fetchGeneral = () => async dispatch => {
const response = await fetch('http://localhost:3000/assets/api/general.json');
const payload = await response.json();
dispatch(success(payload));
class App extends Component {
static async preInitStore(store) {
await store.dispatch(fetchGeneral());
С этой модификацией вызывающий метод App.preInitStore () может ждать, пока данные не будут получены из API и сохранены в хранилище.
Подробнее об async, await и promises в Javascript читайте в документации Mozilla.
Решаем все наши задачи!
А теперь пришло время объединить кусочки головоломки, чтобы мы могли решить задачу номер 3! Когда мы ожидаем всех методов dispatch () в дочерних компонентах, компонент App теперь может ожидать метод preInitStore () в дочерних компонентах.
class App extends Component {
static async preInitStore(store) {
await store.dispatch(fetchGeneral());
await Routing.preInitStore(store);
}
И поскольку мы ожидаем действия fetchGeneral () в компоненте App перед тем, как выполнить метод дочерних компонентов preInitStore (), мы также решили проблему номер 3! Поскольку дочерние компоненты могут получить эти данные с помощью метода store.getState ().
export class Routing extends Component {
static async preInitStore(store) {
const state = store.getState();
await store.dispatch(fetchRoutingData(state.route));
(Совет: метод приложения preInitStore () теперь отвечает за вызов методов дочерних компонентов preInitStore (). Таким образом, в случае react маршрутизатора это было бы идеальным местом, чтобы решить, какой компонент инициализировать, проверив URL-адрес с экспресс-веб-сервера. См. Полный пример проекта GitHub.)
Впереди еще одна оптимизация!
Теперь мы переместили серверный код в компоненты. Но метод preInitStore () никогда не используется на стороне клиента. Мы можем оптимизировать это, чтобы сэкономить немного байтов для наших посетителей, используя плагин webpack-strip-block для веб-пакетов. Давайте настроим этот плагин таким образом, чтобы он удалял любой код, помеченный как SERVERSIDE-ONLY, чтобы он был удален из нашего последнего клиентского пакета.
npm install --save-dev webpack-strip-block
module.exports = {
...
module: {
rules: [{
test: /.js?$/,
use: [{
loader: 'webpack-strip-block',
options: {
start: 'SERVERSIDE-ONLY:START',
end: 'SERVERSIDE-ONLY:END'
}
}]
}]
}
...
}
Теперь мы можем исключить наши методы preInitStore () из клиентского пакета, добавив 2 комментария:
class App extends Component {
/* SERVERSIDE-ONLY:START */
static async preInitStore(store) {
...
}
/* SERVERSIDE-ONLY:END */
Заключение
Нам удалось снизить сложность нашей функции рендеринга на стороне сервера и сделать наш код удобным для сопровождения:
- Путем разделения логики выборки состояния на стороне сервера на компоненты.
- Добавив статический метод async preInitStore () и сделав его доступным с сервера.
- И используя async / await в методе и действиях preInitStore (). Чтобы мы могли дождаться ответов API и использовать данные, полученные родительским компонентом, в дочерних компонентах.
Надеюсь, мне удалось сделать ваши серверные веб-сайты более удобными в обслуживании. Если у вас есть вопросы или вы хотите попробовать это самостоятельно, вы можете ознакомиться с полным решением на GitHub по ссылке ниже. В нем также есть пример react-router.