Создание динамических хлебных крошек в NextJS
Хлебные крошки — это инструмент навигации по веб-сайту, который позволяет пользователям видеть «стек» их текущей страницы и то, как она вложена в любые родительские страницы. Затем пользователи могут вернуться на родительскую страницу, щелкнув соответствующую ссылку в навигационной цепочке. Эти «крошки» повышают удобство работы пользователя с приложением, облегчая пользователям эффективную и действенную навигацию по вложенным страницам.
Хлебные крошки достаточно популярны при создании веб-панелей или приложений, и вы, возможно, подумали об их добавлении. Эффективное создание этих навигационных ссылок с соответствующим контекстом является ключом к улучшению взаимодействия с пользователем.
Давайте создадим интеллектуальный компонент React NextBreadcrumbs
, который проанализирует текущий маршрут и создаст динамический дисплей breadcrumbs, который может эффективно обрабатывать как статические, так и динамические маршруты.
Мои проекты обычно вращаются вокруг Nextjs и MUI (ранее Material-UI), поэтому я собираюсь подойти к этой проблеме с этой точки зрения, хотя решение должно работать для любого приложения, связанного с Nextjs.
Хлебные крошки статического маршрута
Для начала наш компонент NextBreadcrumbs
будет обрабатывать только статические маршруты, а это означает, что наш проект имеет только статические страницы, определенные в каталоге pages
.
Ниже приведены примеры статических маршрутов, поскольку они не содержат ['s and
] в именах маршрутов, что означает, что структура каталогов точно соответствует ожидаемым URL-адресам, которые они обслуживают.
pages/index.js
-->/
pages/about.js
-->/about
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
в коде имеет два полезных свойства для нашего компонента NextBreadcrumbs
; asPath
и pathname
. Маршрутизатор asPath
— это URL-адрес, отображаемый непосредственно в адресной строке браузера. pathname
более внутренняя версия URL-адреса, в которой динамические части пути заменены их компонентами [parameter]
.
Например, рассмотрим путь /post/abc
сверху.
-
asPath
будет/post/abc
как показано URL 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
- Разбивка
asPath
была перенесена в функциюgeneratePathParts
, поскольку для обоих используется одна и та же логикаrouter.asPath
иrouter.pathname
. - Определите
param'eter that lines up with the dynamic route value, so
abc post_id`would result in
. - Вложенный маршрут
param'eter and all associated query values (
route.query) are passed to a provided
getTextGeneratorwhich will return either a
nullvalue or a
Promise`, ответ, который должен возвращать динамическую строку для использования в соответствующей хлебной крошке. - В массив зависимостей
useMemo
добавлено больше зависимостей;router.pathname
,router.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.