С небольшой помощью Material UI

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

Сценарий

Допустим, у вас есть клиент, который хочет, чтобы вы создали пользовательский интерфейс со следующими пунктами меню:

  • Дом
  • Магазин
  • Панель приборов
  • Настройки
  • Счет

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

Настраивать

В качестве предварительного условия вы должны установить менеджер версий узла. Как только вы это запустите, мы собираемся использовать (по состоянию на 25 октября 2022 г.) последнюю версию Node.js с долгосрочной поддержкой:

$ nvm install 18 && nvm use 18

Далее мы собираемся загрузить наш проект React, используя наше надежное приложение create-react-app:

$ npx create-react-app navigation-drawer --template typescript

Здесь имя «navigation-drawer», но вы можете называть его как хотите. create-react-app создаст для вас каталог, поэтому перейдите в этот каталог в своем терминале:

$ cd navigation-drawer

Затем мы собираемся начать с установки некоторых пакетов, которые мы будем использовать позже:

$ npm i react-router-dom @mui/material @mui/icons-material @emotion/react @emotion/styled

Для справки вот объяснение того, что делают эти пакеты:

  • react-router-dom — (версия 6.3.0, размер в распакованном виде: 169 КБ) — React Router — это облегченная полнофункциональная библиотека маршрутизации для библиотеки React JavaScript. React Router работает везде, где работает React; в Интернете, на сервере (с помощью node.js) и в React Native
  • @mui/material — (версия 5.10.1, размер в распакованном виде: 9,51 МБ) — обширная библиотека компонентов, включающая реализацию системы Google Material Design
  • @mui/icons-material— (версия 5.8.4, размер в распакованном виде: 17,8 МБ) — пакет, предоставляющий Google значки материалов, преобразованные в компоненты SvgIcon
  • @emotion/react— (версия 11.10.0, размер в распакованном виде: 549 КБ) — производительная и гибкая библиотека CSS-in-JS. Основываясь на многих других библиотеках CSS-in-JS, он позволяет быстро стилизовать приложения с помощью стилей строк или объектов. Он имеет предсказуемый состав, чтобы избежать проблем со специфичностью CSS. Благодаря исходным картам и меткам Emotion предлагает отличные возможности для разработчиков и высокую производительность с интенсивным кэшированием в рабочей среде
  • @emotion/styled— (версия 11.10.0, размер в распакованном виде: 174 КБ) — стилизованный API для @emotion/react

Кодирование

Это всегда всеми любимая часть. Откройте загруженный проект React в вашей любимой IDE. Начните с создания 5 каталогов внутри каталога src/:

$ cd src && mkdir components media pages styles types

Теперь структура каталогов вашего проекта должна выглядеть так:

public/
src/
  --> components/
  --> media/
  --> pages/
  --> styles/ 
  --> types/
.gitignore
README.md
package-lock.json
package.json
tsconfig.json

В файле src/index.tsx замените шаблонное содержимое следующим:

import React from 'react';
import ReactDOM from 'react-dom';
import App from './components/App';

ReactDOM.render(<App />, document.getElementById('root'));

Вы должны увидеть тонну красных линий повсюду, но это нормально, потому что мы скоро это исправим. Затем мы собираемся создать файл в каталоге components/ App.tsx и заполнить его следующим образом:

import React, { FC } from 'react';
import { BrowserRouter as Router } from "react-router-dom";
import AppRouter from './AppRouter';

const App: FC = () => (
  <Router>
    <AppRouter />
  </Router>
);

export default App;

BrowserRouter (который мы назвали Router) — это компонент, который использует API истории HTML5 для синхронизации вашего пользовательского интерфейса с URL-адресом. Мы обернули это вокруг маршрутов нашего приложения, которые находятся в AppRouter. Далее нам нужно создать маршруты нашего приложения в components/AppRouter.tsx. Этот компонент будет выглядеть следующим образом:

import React, { Suspense } from 'react';
import { Route, Routes } from "react-router-dom";
import { Grid } from '@mui/material';

const DemoPage = ({ pageName }: DemoPageProps) => <h1 style={{ margin: '1rem' }}>Demo Page: {pageName}</h1>;
const DemoSpinner = () => (<p>Spinner</p>);
const AppRouter = () => (
    <Grid className="container">
      <Suspense fallback={<DemoSpinner />}>
        <Routes>
          <Route path="/" element={<DemoPage pageName="Home" />} />
          <Route path="/shop" element={<DemoPage pageName="Shop" />} />
          <Route path="/dashboard" element={<DemoPage pageName="Dashboard" />} />
          <Route path="/about" element={<DemoPage pageName="About" />} />
          <Route path="/settings" element={<DemoPage pageName="Shop" />} />
          <Route path="/account" element={<DemoPage pageName="Account" />} />
        </Routes>
      </Suspense>
    </Grid>
);

export default AppRouter;

Здесь наш маршрут по умолчанию — домашняя страница. Обратите внимание, как мы можем указать маршрут в «корне» / над списком подмаршрутов, то есть /about. Раньше это добавляло бы ошибку в приложение, поскольку маршрут / перехватывал все, что шло после него. С тех пор это было исправлено в пакете react-router-dom. Прямо сейчас у нас есть только один компонент контента, который мы повторно используем для всех маршрутов, поэтому давайте продолжим и обновим его. В каталоге pages/ создайте Page.tsx :

import React from 'react';

interface PageProps {
  pageName: string;
}

const Page = ({ pageName }: PageProps) => (
  <h1 style={{ margin: '1rem' }}>Demo Page: {pageName}</h1>
);

export default Page;

Это будет действовать как заполнитель для целей учебника, но в идеале вы не будете делать это таким образом в профессиональном проекте. Я не буду рассказывать об этом в этой статье, но вам следует создать отдельный компонент React внутри каталога pages/ для каждой страницы, которую вы хотите разместить на своем веб-сайте. Это изолирует функциональность и является более чистой реализацией, чем включение каждой страницы в этот один компонент React. Оглядываясь назад на наш компонент AppRouter.tsx, мы видим, что также используется компонент Spinner. Это будет отображаться всякий раз, когда мы загружаем новый контент в DOM. Дадим Spinner собственный файл; в components/ создайте Spinner.tsx :

import React from 'react';
import { CircularProgress, Box } from '@mui/material';

const Spinner = () => {
  return (
    <Box sx={{ display: 'flex', color: 'grey.500' }}>
      <CircularProgress />
    </Box>
  );
};
export default Spinner;

Возвращаясь к AppRouter.tsx, теперь у нас должны быть компоненты Page и Spinner, готовые к использованию. Обновленная версия этого компонента теперь выглядит так:

import React, { Suspense, lazy } from 'react';
import { Route, Routes } from "react-router-dom";
import { Grid } from '@mui/material';
import SiteNavigation from './SiteNavigation';
import Spinner from './Spinner';

const About = React.lazy(() => import("../pages/DemoPage"));
const Dashboard = React.lazy(() => import("../pages/DemoPage"))
const Account = lazy(() => import('../pages/DemoPage'));
const Shop = lazy(() => import('../pages/DemoPage'));
const Home = lazy(() => import('../pages/DemoPage'));

const AppRouter = () => (
  <SiteNavigation>
    <Grid className="container">
      <Suspense fallback={<Spinner />}>
        <Routes>
          <Route path="/" element={<Home pageName="Home" />} />
          <Route path="/shop" element={<Shop pageName="Shop" />} />
          <Route path="/dashboard" element={<Dashboard pageName="Dashboard" />} />
          <Route path="/about" element={<About pageName="About" />} />
          <Route path="/settings" element={<Shop pageName="Shop" />} />
          <Route path="/account" element={<Account pageName="Account" />} />
        </Routes>
      </Suspense>
    </Grid>
  </SiteNavigation>
);

export default AppRouter;

Обратите внимание, как мы лениво загружаем наши страницы, чтобы принимать запросы на соответствующий маршрут. Ленивая загрузка позволяет отложить загрузку связанного кода для определенного компонента до момента его вызова. Также есть компонент SiteNavigation, внутри которого мы заключаем маршруты. Вот где становится интересно. Чтобы создать SiteNavigation.tsx, давайте начнем со следующего кода в каталоге components/:

import React, { useState } from 'react';
import { useNavigate, useLocation, NavigateFunction } from 'react-router-dom';
import { Grid } from '@mui/material';
import * as H from 'history';
import ErrorBoundary from './ErrorBoundary';
import '../styles/SiteNavigation.css';

interface SiteNavigationProps {
  children: any,
}

function setInitialRoute(
  location: H.Location,
  navigate: NavigateFunction,
): void {
  // eslint-disable-next-line @typescript-eslint/no-unused-vars        
  const [_root, route] = location.pathname.split('/');
  if (route && route === 'logout') navigate('/');
}

const SiteNavigation = (props: SiteNavigationProps) => {
  const { children } = props;
  const location = useLocation();
  const navigate = useNavigate();
  setInitialRoute(location, navigate);
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  const [isSideBarOpened, setIsSidebarOpened] = useState(false);

  return (
    <Grid className="SiteNavigation">
      <Grid className="top">

      </Grid>
      <Grid className="bottom">
        <Grid className={isSideBarOpened ? 'left opened' : 'left'}>
          <Grid className="bottom-left-top">
            
          </Grid>
          <Grid className="bottom-left-bottom">

          </Grid>
        </Grid>
        <Grid className="right">
          <ErrorBoundary>
            {children}
          </ErrorBoundary>
        </Grid>
      </Grid>
    </Grid>
  );
};

export default SiteNavigation;

Потратьте минуту, чтобы сделать паузу и посмотреть на приложение до сих пор; это выглядит очень просто:

Если бы я кодировал это с нуля, я бы начал добавлять стили ко всем компонентам с этого момента. Начнем с styles/index.css и styles/App.css :

Для целей руководства я пишу стили в простом CSS. Для профессионального проекта вы можете использовать предпочтительный способ стилизации с Material-UI, используя Theming и CSS-in-JS. Далее давайте стилизуем компонент SiteNavigation (styles/SiteNavigation.css):

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

  • Он использует адаптивный дизайн с макетом flexbox. Строка 118 из styles/SiteNavigation.css показывает медиа-запрос, используемый для скрытия второстепенных функциональных элементов на мобильных экранах.
  • Мы придерживаемся стандартной цветовой схемы с шестнадцатеричными кодами основного и дополнительного цветов (#61DAFB для голубых бликов, #FFFFFF для белого фона и #000000 для черного текста и рамки).

  • Щелчок по значку со стрелкой управляет состоянием открытия/закрытия ящика. Он также вращается в зависимости от этого состояния. Строки со 154 по 172 в styles/SiteNavigation.css показывают, как это делается.
  • Существует разделение между значками верхнего меню и значками нижнего меню.
  • Выделение текущей страницы/маршрута также анимировано, как показано в строках с 135 по 151 в styles/SiteNavigation.css.

Последнее, что нам нужно сделать сейчас, это добавить опции меню. Мы можем обновить компонент components/SiteNavigation.tsx следующим образом:

import React, { useState } from 'react';
import { useNavigate, useLocation, NavigateFunction, Link } from 'react-router-dom';
import { Alert, Badge, Grid, Typography } from '@mui/material';
import { NotificationsOutlined } from '@mui/icons-material';
import * as H from 'history';
import { bottomIcons, SiteNavigationIcon, topIcons } from '../types/SiteNavigationIcon';
import { NotificationItem } from '../types/NotificationItem';
import NotificationsList from './NotificationsList';
import RotatingArrow from './RotatingArrow';
import ErrorBoundary from './ErrorBoundary';
import Search from './Search';
import Logo from '../media/logo.svg';
import '../styles/SiteNavigation.css';

interface SiteNavigationProps {
  children: any,
}

function setInitialRoute(
  location: H.Location,
  navigate: NavigateFunction,
): void {
  const [, route] = location.pathname.split('/');
  if (route && route === 'logout') navigate('/');
}

const SiteNavigation = ({ children }: SiteNavigationProps) => {
  const location = useLocation();
  const navigate = useNavigate();
  setInitialRoute(location, navigate);

  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  const [error, setError] = useState<Error | undefined>(undefined);
  const [pageName, setPageName] = useState('');
  const [isSideBarOpened, setIsSidebarOpened] = useState(false);
  const [numNotifications] = useState<number>(0);
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  const [showNotifications, setShowNotifications] = useState<boolean>(false);
  const [notifications] = useState<NotificationItem[]>([]);

  const onClickShowNotifications = () => {
    // Fill in your code for displaying the notifications list here!
  };
  
  const onSearch = (searchString: string) => { 
    // Fill in your code for calling the backend API here!
  };

  const toNavLink = (icon: SiteNavigationIcon) => {
    const IconComponent = icon.component;
    return (
      <Link
        to={icon.path}
        onClick={() => setPageName(icon.key)}
        key={icon.key}
        className={pageName === icon.key ? 'underlined icon' : 'icon'}
      >
        <IconComponent
          fontSize="large"
          htmlColor="#000"
        />
        {!!isSideBarOpened && (
          <Typography variant="body1">{icon.key}</Typography>
        )}
      </Link>
    );
  };

  return (
    <Grid className="SiteNavigation">
      <Grid className="top">
        {error && <Alert severity="error">{error.message}</Alert>}
        {!error && (
          <>
            <Grid className="left">
              <img className="logo" src={Logo} alt="logo" />
            </Grid>
            <Grid className="middle">
              <Search onSearch={onSearch} />
            </Grid>
            <Grid className="right">
              <Badge
                badgeContent={numNotifications}
                color="error"
                anchorOrigin={{
                  vertical: 'bottom',
                  horizontal: 'right',
                }}
                overlap="circular"
              >
                <NotificationsOutlined
                  fontSize="large"
                  htmlColor="#000"
                  className="icon"
                  onClick={onClickShowNotifications}
                />
              </Badge>
              {(showNotifications && notifications.length > 0) && (
                <NotificationsList
                  notifications={notifications}
                  onClickShowNotifications={onClickShowNotifications}
                />
              )}
            </Grid>
          </>
        )}
      </Grid>
      <Grid className="bottom">
        <Grid className={isSideBarOpened ? 'left opened' : 'left'}>
          <Grid className="bottom-left-top">
            {topIcons.map(toNavLink)}
          </Grid>
          <Grid className="bottom-left-bottom">
            {bottomIcons.map(toNavLink)}
            <Grid className="rotating-arrow">
              <RotatingArrow onOpen={() => setIsSidebarOpened(!isSideBarOpened)} />
              {!!isSideBarOpened && (
                <Typography variant="body1">Close</Typography>
              )}
            </Grid>
          </Grid>
        </Grid>
        <Grid className="right">
          <ErrorBoundary>
            {children}
          </ErrorBoundary>
        </Grid>
      </Grid>
    </Grid>
  );
};

export default SiteNavigation;

Вам понадобятся Иконки для отображения в меню (types/SiteNavigationIcon.ts):

import React from 'react';
import HomeIcon from '@mui/icons-material/HomeOutlined';
import SettingsIcon from '@mui/icons-material/SettingsOutlined';
import AccountIcon from '@mui/icons-material/AccountCircleOutlined';
import ShopIcon from '@mui/icons-material/StorefrontOutlined';
import DashboardIcon from '@mui/icons-material/BarChartOutlined';
import AboutIcon from '@mui/icons-material/InfoOutlined';

export type SiteNavigationIcon = {
  key: string;
  component: React.ComponentType<any>;
  path: string;
}

export const topIcons: SiteNavigationIcon[] = [
  {
    key: 'Home',
    path: '/',
    component: HomeIcon,
  },
  {
    key: 'Shop',
    path: '/shop',
    component: ShopIcon,
  },
  {
    key: 'Dashboard',
    path: '/dashboard',
    component: DashboardIcon,
  },
  {
    key: 'About',
    path: '/about',
    component: AboutIcon,
  },
];

export const bottomIcons: SiteNavigationIcon[] = [
  {
    key: 'Settings',
    path: '/settings',
    component: SettingsIcon,
  },
  {
    key: 'Account',
    path: '/account',
    component: AccountIcon,
  },
];

Заключение

После добавления опций меню вы сможете вернуться в терминал и запустить сервер:

$ npm start
Compiled successfully!

You can now view navigation-drawer in the browser.

  Local:            http://localhost:3000
  On Your Network:  http://xxx.xxx.x.xxx:3000

Note that the development build is not optimized.
To create a production build, use npm run build.

webpack compiled successfully
No issues found.

Откройте браузер по адресу http://localhost:3000 и посмотрите на свой удивительный навигационный ящик в действии:

Подведем итог того, что мы построили:

  • Отзывчивое меню навигации (точка останова при ширине экрана 768 пикселей). Например, это скрывает панель поиска на мобильных экранах.
  • Маршрутизация для нашего одностраничного приложения, которое направляет запросы на нужную страницу, т.е. /shop показывает страницу магазина.
  • Компонент более высокого порядка, который теперь мы можем обернуть вокруг содержимого каждой страницы нашего сайта. Это более эффективно, потому что меню навигации является постоянным, и мы можем лениво загружать и заменять основное содержимое страницы только при необходимости.

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

  1. Аутентификация пользователя и Route Guards
  2. Добавление закрываемых всплывающих уведомлений со списком уведомлений под значком уведомлений
  3. Отклонить запросы к внутреннему API для поиска (используя fetch API на внешнем интерфейсе)
  4. Представление «Сведения о продукте» на странице «Магазин» для данного идентификатора продукта, отображаемого в URL-адресе в виде переменной запроса (http://localhost:3000/shop?productId=xxxx)

Чтобы увидеть завершенный код, вы можете посетить общедоступный репозиторий GitHub ниже.



Если вам нравится мой стиль письма, пожалуйста, следуйте. Я публикую ~ 1 раз в месяц, и мне нравится общаться в сети, спасибо!