Правильная обработка ошибок в динамических страницах NextJS | BRDN

Правильная обработка ошибок в динамических страницах NextJS

Веб-приложения дали разработчикам сильные инструменты и позволили делать то, что раньше было невозможно: виртуальный DOM, управление состоянием компонента, смена страниц без перезагрузки, вычисления на стороне клиента, offline-режим, улучшенная политика кеширования через Service Workers, псевдо-установка приложений - без этого мы уже не можем представить себе современное приложение.

За всем этим фасадом скрывается множество проблемных моментов и сложностей. В частности, одностраничные приложения до сих пор плохо индексируются поисковыми системами, потому что выполнение JS-кода на них крайне ограничено (а где-то и не выполняется). Для решения этой проблемы есть серверная отрисовка.

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

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

Динамический шаблон страницы (или просто динамическая страница) - это общий компонент, который выдаётся по запросу к определённому адрессу, содержимое которого выстраивается на основе приходящих данных. Например, страница товара.

В случаях, когда мы просто пользуемся шаблоном, NextJS не может знать какие страницы есть, а каких нет - поэтому его стандартная логика не работает, страницы с ошибкой (404, 500 и пр.) не выводятся. Совершается бесконечная загрузка.

Например, создадим файл pages/[id]/index.js. Квадратные скобки в названии папки указывают на то, что страница - шаблон. Содержимое скобок (id) - это параметр контекста, который записывается исходя из запроса. Например, по запросу somesite.com/example таким параметром будет строка example (в контексте страницы будет хранится значение id: "example").

Содержимое возьмём такое:

import ChildComponent from '/components/ChildComponent';

const DynamicPage = () => (
    <div className='container'>
        <ChildComponent />
    </div>
);

export default DynamicPage;

Допустим, мы делаем какую-то обработку внутри ChildComponent. Вызываем данные и пр. Но что, если таких данных нет? Вызовет ли NextJS 404 ошибку? Нет, не вызовет, потому что сам из себя он не знает о том, какие страницы подходят в шаблон, а при каких нужно вызывать ошибку. Например, при вызове страницы somesite.com/wrong-address, на сервере, к которому мы обращаемся за данными, нет записи с ключом wrong-address. В этом случае, в ответ на запрос придёт 404 ошибка, но NextJS об этом не узнает и будет показывать пустой компонент. Можно это исправить.

import React from 'react';

import ChildComponent from '/components/ChildComponent';
import Error from '/pages/404';

const DynamicPage = () => {
    // флаг ошибки
    const [is_error, toggle_error] = React.useState(false);
    
    // вызываем данные при монтировании компонента
    React.useEffect(() => {
        fetch_data();
    }, []);

    // функция вызова данных
    const fetch_data = () => {
        // ...
    }

    return(
        <div className='container'>
            {
                is_error ?
                <Error /> : // если флаг включён, то показываем компонент ошибки
                <ChildComponent /> // если ошибки не было, то стандартный компонент
            }
        </div>
    );
}

export default DynamicPage;

Мы перенесли вызов данных в родителя, где настроили логику с флагом ошибки is_error. По комментариям виден механизм работы: если при вызове происходит ошибка, то показываем компонент Error, который берётся из нашей страницы /pages/404.js; если ошибки не было, то компонент ChildComponent.

Вроде бы, проблема решена. Мы заходим на somesite.com/wrong-address - показывается ошибка. Однако, тут есть некоторые проблемы. Посмотрите внимательно на код выше. Вам не кажется, что работа настроена однобоко? Не смущает ничего? Правильно: не обрабатываются другие ошибки. Если с сервером будут проблемы и он в ответе передаст, например, 500 ошибку, будет выдан компонент 404 ошибки.

К нашему удобству, NextJS позволяет использовать компонент стандартной ошибки. Давайте добавим необходимую обработку:

import React from 'react';
import NextError from 'next/error';

import ChildComponent from '/components/ChildComponent';
import Error from '/pages/404';

const DynamicPage = () => {
    const [error_code, change_error_code] = React.useState(null);
    
    React.useEffect(() => {
        fetch_data();
    }, []);

    const fetch_data = () => {
        // ...
    }

    // проверяем на состояние ошибки
    if (error_code) {
        // если ошибка 404, то выводим свой компонент 404 ошибки
        if (error_code === 404) {
            return(
                <div className='container'>
                    <Error />
                </div>
            );
        // если другая ошибка, выводим компонент `NextError` с обязательной передачей кода ошибки
        } else {
            return(
                <div className='container'>
                    <NextError statusCode={error_code} />
                </div>
            );
        }
    // если ошибки нет, выводим рабочий компонент
    } else {
        return(
            <div className='container'>
                <ChildComponent />
            </div>
        );
    }
}

export default DynamicPage;

Мы убрали флаг is_error в состоянии компонента, так как его функции может выполнять флаг error_code (если он в положении null, ошибки нет).

Кажется, что теперь точно всё: все ошибки обрабатываются. Но если мы попробуем запустить индексацию неверного адресса приложения в веб-аналитике поисковых систем (Яндекс.Вебмастер или Google Search Console), то увидим, что 404 ошибку они не выдадут. Почему? Дело в том, что поисковые роботы индексируют страницы не по содержимому, а по коду, который передаёт сервер в ответ на запрос страницы. Роботу одинаково, выдаётся ли компонент ChildComponent, NextError или Error - код страницы приходит 200, значит страница есть.

Вот несколько примеров, когда эта тонкость работы имеет огромное значение:

  1. Изменилась работа сервера, из-за чего поменялись идентификаторы сущностей (id/slug);
  2. Каталог товаров активно обновляется (удаляются/добавляются новые товары - с удалением как раз и будут проблемы, поиск будет выдавать старые данные);
  3. Поменялся принцип построения url-строки в приложении (например, вместо id стали использоваться slug).

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

Как это исправить? Кажется, просто: возьми, да добавь в код метод, который бы передавал ошибку. Но дело в том, что такого метода просто нет. JS, который исполняется у пользователя в браузере, не может изнутри страницы передать код статуса страницы. Этим занимается сервер. В случаях статических страниц NextJS знает, какие страницы есть, а каких нет - поэтому выдаёт ошибку сам. В случае же динамических страниц, как и писалось выше, он знать этого не может. А передавать состояние страницы из самого компонента нельзя.

И что? Больше нет вариантов? Есть. Опять же, к нашему удобству, NextJS позволяет пользоваться своей серверной обёрткой из JS при построении страницы. Метод, который позволяет это сделать, называется getServerSideProps. Этот метод также даёт доступ к контексту, обёртке высшего уровня всего приложения. В этой обёртке и хранится код ошибки, который выдаёт серверная обёртка при запросе за страницей. А по особенностям работы самого JS мы знаем: имея ссылку на сущность, мы можем её изменять.

Остаётся дело за малым: добавить изменение кода ответа серверной обёртки. Для этого мы добавим сам метод getServerSideProps в компонент страницы, а также перенесём вызов данных туда (также немного оптимизируем структуру):

import NextError from 'next/error';

import Error from '/pages/404';
import ChildComponent from '/components/ChildComponent';

const DynamicPage = ({
    statusCode = 200,
    data: null,
}) => {
    // если коды позволяют, выводим обычный компонент
    if (fetch_res.status >= 200 && fetch_res.status < 300) {
        return (
            <ChildComponent data={data} />  // теперь в нашей странице нужные данные есть ещё до её открытия
        );
    // если ошибка 404, то выводим свой компонент ошибки
    } else if (statusCode === 404) return <Error />;
    // если ошибка другая, выводим компонент `next/error`
    else return <NextError statusCode={statusCode} />;
};

// добавили метод серверной обёртки `getServerSideProps`, который исполняется до построения страницы
export async function getServerSideProps(context) {
    // получаем параметр `id` сущности из контекста, чтобы потом обратиться к серверу за данными ("example"/"wrong-address" из нашего примера)
    const { id } = context.query;

    // делаем запрос
    const fetch_res = await fetch(/* ...url */);
    // немного оптимизируем работу и достанем из запроса данные
    const fetch_json = await fetch_res.json();

    let statusCode = 200;

    // проверяем код ответа у запроса
    if (fetch_res.status < 200 || fetch_res.status >= 300) {
        // передаём код ошибки в обёртку (для ответа сервера)
        context.res.statusCode = fetch_res.status;
        // передаём код ошибки в компонент (для вывода компонента)
        statusCode = fetch_res.status;
    }

    return {
        props: {
            statusCode,
            data: fetch_json,   // передаём полученные данные в компонент страницы
        },
    };
}

export default DynamicPage;

Всё. Теперь у нас и для пользователя все ошибки обрабатываются, и поисковые боты получают ошибку при запросе несуществующей страницы. Обработка ошибок настроена верно.

Надеемся, материал вам был полезен и вы узнали кое-что новое.