DevGang
Авторизоваться

Создание динамических хлебных крошек в NextJS 

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

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

Давайте создадим интеллектуальный компонент React NextBreadcrumbs, который проанализирует текущий маршрут и создаст динамический дисплей breadcrumbs, который может эффективно обрабатывать как статические, так и динамические маршруты.

Мои проекты обычно вращаются вокруг Nextjs и MUI (ранее Material-UI), поэтому я собираюсь подойти к этой проблеме с этой точки зрения, хотя решение должно работать для любого приложения, связанного с Nextjs.

Хлебные крошки статического маршрута

Для начала наш компонент NextBreadcrumbs будет обрабатывать только статические маршруты, а это означает, что наш проект имеет только статические страницы, определенные в каталоге pages.

Ниже приведены примеры статических маршрутов, поскольку они не содержат ['s and] в именах маршрутов, что означает, что структура каталогов точно соответствует ожидаемым URL-адресам, которые они обслуживают.

  1. pages/index.js --> /
  2. pages/about.js --> /about
  3. pages/my/super/nested/route.js --> /my/super/nested/route

Позднее решение будет расширено для обработки динамических маршрутов.

Определение базового компонента

Мы можем начать с фундаментального компонента, который использует компонент MUI Breadcrumbs в качестве основы.

import Breadcrumbs from '@mui/material/Breadcrumbs';
import * as React from 'react';

export default function NextBreadcrumbs() {
  return (
    <Breadcrumbs aria-label="breadcrumb" />
  );
}

Вышеупомянутое создает базовую структуру компонента React NextBreadcrumbs, импортирует правильные зависимости и отображает пустой компонент MUI Breadcrumbs.

Затем мы можем добавить хуки next/router, которые позволят нам построить хлебные крошки из текущего маршрута.

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

В такой ситуации /settings/notifications будет выглядеть следующим образом:

Home (/ link) > Settings (/settings link) > Notifications (no link)

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

import Breadcrumbs from '@mui/material/Breadcrumbs';
import Link from '@mui/material/Link';
import Typography from '@mui/material/Typography';
import { useRouter } from 'next/router';
import React from 'react';


export default function NextBreadcrumbs() {
  // Gives us ability to load the current route details
  const router = useRouter();

  return (
    <Breadcrumbs aria-label="breadcrumb" />
  );
}


// Each individual "crumb" in the breadcrumbs list
function Crumb({ text, href, last=false }) {
  // The last crumb is rendered as normal text since we are already on the page
  if (last) {
    return <Typography color="text.primary">{text}</Typography>
  }

  // All other crumbs will be rendered as links that can be visited 
  return (
    <Link underline="hover" color="inherit" href={href}>
      {text}
    </Link>
  );
}

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

Мы создадим список объектов хлебных крошек, содержащих информацию, которая будет отображаться каждым элементом Crumb. Каждая цепочка будет создана путем анализа свойства маршрутизатора Nextjs asPath, которое представляет собой строку, содержащую маршрут, как показано в строке URL-адреса браузера.

Мы удалим все параметры запроса, такие как ?query=value, из URL-адреса, чтобы упростить процесс создания навигационной цепочки.

export default function NextBreadcrumbs() {
  // Gives us ability to load the current route details
  const router = useRouter();

  function generateBreadcrumbs() {
    // Remove any query parameters, as those aren't included in breadcrumbs
    const asPathWithoutQuery = router.asPath.split("?")[0];

    // Break down the path between "/"s, removing empty entities
    // Ex:"/my/nested/path" --> ["my", "nested", "path"]
    const asPathNestedRoutes = asPathWithoutQuery.split("/")
                                                 .filter(v => v.length > 0);

    // Iterate over the list of nested route parts and build
    // a "crumb" object for each one.
    const crumblist = asPathNestedRoutes.map((subpath, idx) => {
      // We can get the partial nested route for the crumb
      // by joining together the path parts up to this point.
      const href = "/" + asPathNestedRoutes.slice(0, idx + 1).join("/");
      // The title will just be the route string for now
      const title = subpath;
      return { href, text }; 
    })

    // Add in a default "Home" crumb for the top-level
    return [{ href: "/", text: "Home" }, ...crumblist];
  }

  // Call the function to generate the breadcrumbs list
  const breadcrumbs = generateBreadcrumbs();

  return (
    <Breadcrumbs aria-label="breadcrumb" />
  );
}

С помощью этого списка хлебных крошек, теперь мы можем сделать их с помощью компонентов Breadcrumbs и Crumb. Как упоминалось ранее, для краткости показана только часть return нашего компонента..

// ...rest of NextBreadcrumbs component above...
  return (
    {/* The old breadcrumb ending with '/>' was converted into this */}
    <Breadcrumbs aria-label="breadcrumb">
      {/*
        Iterate through the crumbs, and render each individually.
        We "mark" the last crumb to not have a link.
      */}
      {breadcrumbs.map((crumb, idx) => (
        <Crumb {...crumb} key={idx} last={idx === breadcrumbs.length - 1} />
      ))}
    </Breadcrumbs>
  );

Это должно начать генерировать некоторые очень простые, но работающие хлебные крошки на нашем сайте после рендеринга; /user/settings/notifications будет отображаться как

Home > user > settings > notifications

Запоминание сгенерированных хлебных крошек

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

const router = useRouter();

  // this is the same "generateBreadcrumbs" function, but placed
  // inside a "useMemo" call that is dependent on "router.asPath"
  const breadcrumbs = React.useMemo(function generateBreadcrumbs() {
    const asPathWithoutQuery = router.asPath.split("?")[0];
    const asPathNestedRoutes = asPathWithoutQuery.split("/")
                                                 .filter(v => v.length > 0);

    const crumblist = asPathNestedRoutes.map((subpath, idx) => {
      const href = "/" + asPathNestedRoutes.slice(0, idx + 1).join("/");
      return { href, text: subpath }; 
    })

    return [{ href: "/", text: "Home" }, ...crumblist];
  }, [router.asPath]);

  return // ...rest below...

Улучшение отображения текста в навигационной цепочке

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

Прямо сейчас, если у нас есть путь вроде /user/settings/notifications, он покажет:

Home > user > settings > notifications

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

const _defaultGetDefaultTextGenerator= path => path

export default function NextBreadcrumbs({ getDefaultTextGenerator=_defaultGetDefaultTextGenerator }) {
  const router = useRouter();

  // Two things of importance:
  // 1. The addition of getDefaultTextGenerator in the useMemo dependency list
  // 2. getDefaultTextGenerator is now being used for building the text property
  const breadcrumbs = React.useMemo(function generateBreadcrumbs() {
    const asPathWithoutQuery = router.asPath.split("?")[0];
    const asPathNestedRoutes = asPathWithoutQuery.split("/")
                                                 .filter(v => v.length > 0);

    const crumblist = asPathNestedRoutes.map((subpath, idx) => {
      const href = "/" + asPathNestedRoutes.slice(0, idx + 1).join("/");
      return { href, text: getDefaultTextGenerator(subpath, href) }; 
    })

    return [{ href: "/", text: "Home" }, ...crumblist];
  }, [router.asPath, getDefaultTextGenerator]);

  return ( // ...rest below

Тогда наш родительский компонент может иметь что-то вроде следующего: присвоить подпутям названия или, возможно, даже заменить их новой строкой.

{/* Assume that `titleize` is written and works appropriately */}
<NextBreadcrumbs getDefaultTextGenerator={path => titleize(path)} /

Эта реализация затем приведет к следующим хлебным крошкам. Полный пример кода внизу содержит больше подобных примеров.

Home > User > Settings > Notifications

Динамические маршруты Nextjs

Маршрутизатор Nextjs позволяет включать динамические маршруты, использующие сопоставление с образцом, чтобы URL-адреса могли иметь слаги, UUID и другие динамические значения, которые затем будут переданы в ваши представления.

Например, если в вашем приложении Nextjs есть компонент страницы по адресу pages/post/[post_id].js, то маршруты /post/1 и /post/abc будут соответствовать ему.

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

Прямо сейчас, если вы посетите /post/abc, вы увидите хлебные крошки, похожие на

post > abc

но если сообщение с UUID имеет заголовок My First Post, то мы хотим изменить хлебные крошки, чтобы сказать

post > My First Post

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

Маршрутизатор Nextjs: asPath против pathname

Экземпляр маршрутизатора next/router в коде имеет два полезных свойства для нашего компонента NextBreadcrumbsasPath и pathname. Маршрутизатор asPath— это URL-адрес, отображаемый непосредственно в адресной строке браузера. pathname более внутренняя версия URL-адреса, в которой динамические части пути заменены их компонентами [parameter].

Например, рассмотрим путь /post/abc сверху.

  1.  asPath будет /post/abc как показано URL
  2. pathname будет /post/[post_id] как диктует наш каталог pages

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

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

const _defaultGetTextGenerator = (param, query) => null;
const _defaultGetDefaultTextGenerator = path => path;

// Pulled out the path part breakdown because its
// going to be used by both `asPath` and `pathname`
const generatePathParts = pathStr => {
  const pathWithoutQuery = pathStr.split("?")[0];
  return pathWithoutQuery.split("/")
                         .filter(v => v.length > 0);
}

export default function NextBreadcrumbs({
  getTextGenerator=_defaultGetTextGenerator,
  getDefaultTextGenerator=_defaultGetDefaultTextGenerator
}) {
  const router = useRouter();

  const breadcrumbs = React.useMemo(function generateBreadcrumbs() {
    const asPathNestedRoutes = generatePathParts(router.asPath);
    const pathnameNestedRoutes = generatePathParts(router.pathname);

    const crumblist = asPathNestedRoutes.map((subpath, idx) => {
      // Pull out and convert "[post_id]" into "post_id"
      const param = pathnameNestedRoutes[idx].replace("[", "").replace("]", "");

      const href = "/" + asPathNestedRoutes.slice(0, idx + 1).join("/");
      return {
        href, textGenerator: getTextGenerator(param, router.query),
        text: getDefaultTextGenerator(subpath, href)
      }; 
    })

    return [{ href: "/", text: "Home" }, ...crumblist];
  }, [router.asPath, router.pathname, router.query, getTextGenerator, getDefaultTextGenerator]);

  return ( // ...rest below
  1. Разбивка asPath была перенесена в функцию generatePathParts, поскольку для обоих используется одна и та же логика router.asPath и router.pathname.
  2. Определите param'eter that lines up with the dynamic route value, so abc post_id`  would result in.
  3. Вложенный маршрут param'eter and all associated query values (route.query ) are passed to a provided getTextGenerator which will return either a null value or a Promise`, ответ, который должен возвращать динамическую строку для использования в соответствующей хлебной крошке.
  4. В массив зависимостей useMemo добавлено больше зависимостей; router.pathnamerouter.query и getTextGenerator.

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

function Crumb({ text: defaultText, textGenerator, href, last=false }) {

  const [text, setText] = React.useState(defaultText);

  useEffect(async () => {
    // If `textGenerator` is nonexistent, then don't do anything
    if (!Boolean(textGenerator)) { return; }
    // Run the text generator and set the text again
    const finalText = await textGenerator();
    setText(finalText);
  }, [textGenerator]);

  if (last) {
    return <Typography color="text.primary">{text}</Typography>
  }

  return (
    <Link underline="hover" color="inherit" href={href}>
      {text}
    </Link>
  );
}

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

Полный пример

// NextBreadcrumbs.js

const _defaultGetTextGenerator = (param, query) => null;
const _defaultGetDefaultTextGenerator = path => path;

// Pulled out the path part breakdown because its
// going to be used by both `asPath` and `pathname`
const generatePathParts = pathStr => {
  const pathWithoutQuery = pathStr.split("?")[0];
  return pathWithoutQuery.split("/")
                         .filter(v => v.length > 0);
}

export default function NextBreadcrumbs({
  getTextGenerator=_defaultGetTextGenerator,
  getDefaultTextGenerator=_defaultGetDefaultTextGenerator
}) {
  const router = useRouter();

  const breadcrumbs = React.useMemo(function generateBreadcrumbs() {
    const asPathNestedRoutes = generatePathParts(router.asPath);
    const pathnameNestedRoutes = generatePathParts(router.pathname);

    const crumblist = asPathNestedRoutes.map((subpath, idx) => {
      // Pull out and convert "[post_id]" into "post_id"
      const param = pathnameNestedRoutes[idx].replace("[", "").replace("]", "");

      const href = "/" + asPathNestedRoutes.slice(0, idx + 1).join("/");
      return {
        href, textGenerator: getTextGenerator(param, router.query),
        text: getDefaultTextGenerator(subpath, href)
      }; 
    })

    return [{ href: "/", text: "Home" }, ...crumblist];
  }, [router.asPath, router.pathname, router.query, getTextGenerator, getDefaultTextGenerator]);

  return (
    <Breadcrumbs aria-label="breadcrumb">
      {breadcrumbs.map((crumb, idx) => (
        <Crumb {...crumb} key={idx} last={idx === breadcrumbs.length - 1} />
      ))}
    </Breadcrumbs>
  );
}


function Crumb({ text: defaultText, textGenerator, href, last=false }) {

  const [text, setText] = React.useState(defaultText);

  useEffect(async () => {
    // If `textGenerator` is nonexistent, then don't do anything
    if (!Boolean(textGenerator)) { return; }
    // Run the text generator and set the text again
    const finalText = await textGenerator();
    setText(finalText);
  }, [textGenerator]);

  if (last) {
    return <Typography color="text.primary">{text}</Typography>
  }

  return (
    <Link underline="hover" color="inherit" href={href}>
      {text}
    </Link>
  );
}

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

// MyPage.js (Parent Component)

import React from 'react';
import NextBreadcrumbs from "./NextBreadcrumbs";


function MyPageLayout() {

  // Either lookup a nice label for the subpath, or just titleize it
  const getDefaultTextGenerator = React.useCallback((subpath) => {
    return {
      "post": "Posts",
      "settings": "User Settings",
    }[subpath] || titleize(subpath);
  }, [])

  // Assuming `fetchAPI` loads data from the API and this will use the
  // parameter name to determine how to resolve the text. In the example,
  // we fetch the post from the API and return it's `title` property
  const getTextGenerator = React.useCallback((param, query) => {
    return {
      "post_id": () => await fetchAPI(`/posts/${query.post_id}/`).title,
    }[param];
  }, []);

  return () {
    <div>
      {/* ...Whatever else... */}
      <NextBreadcrumbs
        getDefaultTextGenerator={getDefaultTextGenerator}
        getTextGenerator={getTextGenerator}
      />
      {/* ...Whatever else... */}
    </div>
  }

}

Надеюсь, этот пост научил вас нескольким стратегиям или концепциям Nextjs.

Источник:

#JavaScript #React #NestJS
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

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

Попробовать

В подарок 100$ на счет при регистрации

Получить