MUI и библиотека иконок Material Design

Недавно я начал узнавать и любить MUI — библиотеку компонентов пользовательского интерфейса для React, основанную (но не связанную с) Material Design.

Несмотря на то, что его готовый внешний вид очень похож на Google, его очень легко настроить, чтобы он выглядел и чувствовал себя как ваш собственный, о чем свидетельствует их собственная демонстрация темы «MUI» на домашней странице. Он великолепен не обязательно из-за своего стиля или внешнего вида, а из-за его надежной и гибкой библиотеки компонентов, которая делает создание приложений в React действительно быстрым!

Также огромный респект за 1900+ иконок Material Design в библиотеке. Что касается удобства использования и визуальной доступности, то одни из лучших из существующих. Но если мы пытаемся создать уникальный внешний вид продукта, отчетливо выраженный материальный дизайн иконографии сохраняется — и это не так просто исправить.

Для чего нам нужны некоторые пользовательские значки

Ведь мы ведь хотим быть уникальными и чувствовать себя особенными! (По крайней мере, мы хотим, чтобы наши продукты ощущались именно так).

Для этого нужна хорошая библиотека иконок SVG. Есть несколько таких, как Font Awesome, у которых есть готовая библиотека React, но у вас снова возникнет проблема выглядеть как все. Единственный другой выбор — разработать (и поддерживать, это игра с нулевой суммой) набор иконок или приобрести библиотеку/набор иконок. Конечно, что бы вы ни выбрали, вы просто получите кучу файлов SVG в папке. И вы довольно скоро увидите, почему это не совсем подходит для React.

Структура и анатомия SVG довольно проста, если только вы никогда не смотрели на SVG, и тогда вам, возможно, захочется немного поплакать. Короткое объяснение заключается в том, что SVG — это язык разметки, подобный HTML, который описывает контуры, линии и формы — и их различные цвета заливки, если применимо, в документе. Вот одна из иконок из Streamline HQ, которая мне нравится:

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
  <defs>
    <style>.a{fill:none;stroke:currentColor;stroke-linecap:round;stroke-linejoin:round;stroke-width:1.5px;}</style>
  </defs>
  <title>bin</title>
  <path class="a" d="M17.25,21H6.75a1.5,1.5,0,0,1-1.5-1.5V6h13.5V19.5A1.5,1.5,0,0,1,17.25,21Z"/>
  <line class="a" x1="9.75" y1="16.5" x2="9.75" y2="10.5"/><line class="a" x1="14.25" y1="16.5" x2="14.25" y2="10.5"/><line class="a" x1="2.25" y1="6" x2="21.75" y2="6"/>
  <path class="a" d="M14.25,3H9.75a1.5,1.5,0,0,0-1.5,1.5V6h7.5V4.5A1.5,1.5,0,0,0,14.25,3Z"/>
</svg>

Так что же произойдет, если мы просто попытаемся использовать его напрямую в React + MUI? Нам нужно было бы импортировать SVG, визуализировать его в компонент, и на странице он выглядел бы так…

Ой, это, ох, неловко. Конечно, мы могли бы обернуть эту иконку внутри <div> и придать ей немного CSS, и в конечном итоге заставить ее вести себя как надо. Хотя нам придется делать это для каждой добавляемой иконки? И что, если бы мы захотели использовать синтаксис 🌶️ sx, который поддерживает MUI v5? И, может быть, значки должны изменять размер и реагировать на важные реквизиты MUI, такие как color?

(Кроме того: вы можете прочитать о sx здесь: https://mui.com/system/the-sx-prop/ и Emotion, на котором он построен, но я отвлекся).

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

Генерация иконок React автоматически с использованием SVGR

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

Но в этом случае мне нужен надежный и надежный способ гарантировать, что когда значки добавляются в проект (или изменяются), они каждый раз выглядят четкими и совершенными. Введите СВГР. Это мощный и очень настраиваемый инструмент, который может автоматизировать прием значков SVG и выделение компонентов React. Похоже, это именно то, что мы ищем! Вы можете воспользоваться четырьмя различными реализациями: webpack, Next.js, Node и даже просто CLI. strong> (командная строка). Это означает, что его можно объединить практически с любым современным веб-проектом либо как часть процесса сборки, либо как отдельный процесс.

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

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

npx @svgr/cli -- my-icon.svg

(Если вы следуете инструкциям, вам сначала нужно установить интерфейс командной строки SVGR. Вам также понадобится значок SVG, чтобы проверить это).

Выдает что-то вроде этого:

import * as React from "react";

const SvgBin = (props) => (
  <svg 
    xmlns="http://www.w3.org/2000/svg" 
    viewBox="0 0 24 24" 
    {...props}
  >
    <path d="M19.452 7.5H4.547a.5.5 0 0 0-.5..." />
  </svg>
);

export default SvgBin;

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

MUI-ификация S.V.G.

Я разобрал проект @mui/icons-material, чтобы посмотреть на источник значка — Delete.js (и его TypeScript-аналог Delete.d.ts), чтобы посмотреть, смогу ли я определить де-факто способ создания значков для MUI, которые ведут себя сами по себе. Но я обнаружил, что реализация значков библиотеки MUI резко отличается от того, что выводит SVGR: MUI использует собственный JavaScript для отображения значка как JSX.

"use strict";

var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.default = void 0;

var _createSvgIcon = _interopRequireDefault(require("./utils/createSvgIcon"));

var _jsxRuntime = require("react/jsx-runtime");

var _default = (0, _createSvgIcon.default)( /*#__PURE__*/(0, _jsxRuntime.jsx)("path", {
  d: "M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"
}), 'Delete');

exports.default = _default;

Это не очень хорошо для нас сработает. SVGR генерирует JSX из шаблона, и выяснение того, как генерировать простой JavaScript из шаблона SVGR только для рендеринга JSX, кажется дополнительным шагом (и проблемой), который никому не нужен. Одна ключевая вещь, которую я заметил, глядя на значки MUI, заключается в том, что генерируемый им JSX использует компонент <SvgIcon>. На практике это означает, что визуализируемый объект не должен сильно отличаться от нашего необработанного SVG. Похоже, что просто SVG окунут в какой-то дополнительный соус JavaScript/JSX.

Если повезет, я смогу вручную создать эквивалент MUI:

import * as React from "react";
import { SvgIcon, SvgIconProps } from "@mui/material";
import { memo } from "react";
const SvgBin = (props: SvgIconProps) => {
  const { sx, ...other } = props;
  return (
    <SvgIcon 
      xmlns="http://www.w3.org/2000/svg"
      viewBox="0 0 24 24"
      preservespectRatio="xMidYMid meet" 
      style={{
        fill: "none",
      }}
      sx={sx}
      {...other}
    >
      <path d="..." />
    </SvgIcon>
  );
};
const Memo = memo(SvgBin);
export default Memo;

У нас есть React и импорт компонентов, компонент <SvgIcon> вместо <svg> и некоторые реквизиты оператора деструктурирования/распространения javascript ({ ...}).

Он работает, но написать его вручную — большая работа. Может есть способ лучше?

SVGR использует шаблоны — немного волшебства, которое использует подключаемый модуль Babel для чтения SVG-файла и генерации кода JSX — для определения того, как должен выглядеть вывод. Это означает, что мы должны иметь возможность передать инструменту CLI пользовательский шаблон, например:

npx @svgr/cli --template template.js -- my-icon.svg

… и при условии, что мы сможем понять, как его настроить, мы сможем автоматизировать создание наших MUI-совместимых компонентов значков.

Вывод SVGR по умолчанию, который мы видели ранее (сгенерированный с использованием шаблона по умолчанию), выглядит следующим образом:

const template = (variables, { tpl }) => {
  return tpl`
${variables.imports};

${variables.interfaces};

const ${variables.componentName} = (${variables.props}) => (
  ${variables.jsx}
);
 
${variables.exports};
`
}

module.exports = template

Нам не хватает некоторых импортов, кучи стилей и некоторых реквизитов, специфичных для MUI, но по крайней мере у нас есть отправная точка, так что пристегнитесь…

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

И я имею в виду пристегнуться. Код, который мне пришлось написать, чтобы заставить эту работу работать, кажется хакерским и немного 🤮, но с небольшим количеством документации или без нее (которую я мог найти), у меня нет основы, чтобы знать, каким «правильным способом» это можно было сделать.

Привет GitHub Copilot: один из комментариев, которые я написал, НА САМОМ ДЕЛЕ ВЫДАЛ ТОЧНОЕ РЕШЕНИЕ, КОТОРОЕ МНЕ НУЖНО. Спас мой 🥓, так и было.

Первое, что мы хотим сделать, это заменить тег <svg> на <SvgIcon>.

По-видимому, для этого есть плагин SVGR Babel (@svgr/babel-plugin-replace-jsx-attribute-value), но я не мог понять, как заставить этот плагин работать с CLI, который не был частью существующей цепочки сборки на основе Babel. Вместо этого я применил грязный подход к проверке входящего объекта JSX и изменению или добавлению значений там, где они мне нужны.

Внутри моего пользовательского шаблона (не спрашивайте меня, сколько раз мне пришлось console.dir(variables.jsx) понять его схему) я попытался установить открывающий и закрывающий элементы с name.name на SvgIcon, и это сработало! Вместо <svg>...</svg> получаем <SvgIcon>...</SvgIcon>.

// Change the svg container to MUI's SvgIcon  variables.jsx.openingElement.name.name = "SvgIcon";  variables.jsx.closingElement.name.name = "SvgIcon";

Далее, несмотря на то, что есть способ добавить атрибуты с помощью инструмента CLI (согласно документации), похоже, он работал только тогда, когда я не загружал шаблон. Вместо этого я просто внедрил атрибут в объект JSX:

// Add preserveAspectRatio="xMidYMid meet" to the svg container  variables.jsx.openingElement.attributes.push({    
  type: "JSXAttribute",    
  name: { type: "JSXIdentifier", name: "preserveAspectRatio" },
  value: { type: "StringLiteral", value: "xMidYMid meet" },  
});

Это тоже работает! Отлично, мы в ударе.

Теперь мне нужно принудительно добавить некоторые отступы в вывод для этих конкретных значков, потому что у SVG нет верхнего/нижнего поля (в отличие от значков материалов). Чтобы сделать это с помощью MUI в моем примере, созданном вручную, я использовал реквизит sx. Мы могли бы просто добавить изменения, которые хотим сделать…

<SvgIcon
  xmlns="http://www.w3.org/2000/svg"
  viewBox="0 0 24 24"
  preserveAspectRatio="xMidYMid meet"
  sx={{ padding: "2px" }}
>

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

const { sx, ...other } = props;
  return (
    <SvgIcon
      xmlns="http://www.w3.org/2000/svg"
      viewBox="0 0 24 24"
      preserveAspectRatio="xMidYMid meet"
      sx={{ ...sx, padding: "2px" }}
      {...other}
    >
    ...

(Если вы не знакомы с операторами распространения, это может показаться сложным — мы расширяем и объединяем как входящий реквизит sx, так и любые другие реквизиты в компонент SvgIcon).

Одно дизайнерское решение, которое я здесь принял, заключалось в том, чтобы поместить отступы справа, что означает, что отступы, установленные кем-то, кто использует компонент, будут перезаписаны нашей версией 2px. Я думаю, это нормально, но если это станет проблемой, мы всегда сможем отменить их ({{ padding: "2px", ...sx}}) позже.

Теперь, когда мы знаем, чего хотим, как мы собираемся поместить это в наш объект генератора JSX?! Мы не можем просто добавить это как обычный текст (поверьте мне, я пытался). На самом деле его нужно добавить как элемент подкомпонента, и это УЖАСНО. Я пробовал все, чтобы заставить это работать, искал документацию, рыскал по Stack Overflow, но безрезультатно. В конце концов, магия второго пилота заставила его работать (я до сих пор в восторге) с помощью простого комментария:

// Append sx={{ ...sx, padding: '2px' }} to the JSXOpeningElement's attributes

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

// Append sx={{ ...sx, padding: '2px' }} to the JSXOpeningElement's
// attributes
 
variables.jsx.openingElement.attributes.push({
  type: "JSXAttribute",
  name: { type: "JSXIdentifier", name: "sx" },
  value: {
    type: "JSXExpressionContainer",
    expression: {
      type: "ObjectExpression",
      properties: [{
        type: "ObjectProperty",
        key: { type: "Identifier", name: "padding" },
        value: { type: "StringLiteral", value: "2px" },
      }],
    },
  },
});

Удивительный! По крайней мере, теперь я знаю, как можно добавлять объекты. Затем мне все равно нужно было выяснить оператор распространения для реквизита ...other, поэтому я попробовал:

// Append a {...other} to the opening element's attributes

И это дало мне:

variables.jsx.openingElement.attributes.push({
  type: "JSXSpreadAttribute",
  argument: {
    type: "Identifier",
    name: "other",
  },
});

😍 Не верю своим глазам. Это сработало! Пять 👏 второму пилоту. Теперь я смог модифицировать это и в свой объект previoussx с одной небольшой поправкой (я нашел некоторые документы Babel на SpreadElement):

// Append sx={{ ...sx, padding: '2px' }} to the JSXOpeningElement's // attributes
variables.jsx.openingElement.attributes.push({
  type: "JSXAttribute",
  name: { type: "JSXIdentifier", name: "sx" },
  value: {
    type: "JSXExpressionContainer",
    expression: {
      type: "ObjectExpression",
      properties: [
        {
          type: "SpreadElement",
          argument: {
            type: "Identifier",
            name: "sx",
          },
        },
        // ^ new block
        {
          type: "ObjectProperty",
          key: { type: "Identifier", name: "padding" },
          value: { type: "StringLiteral", value: "2px" },
        },
      ],
    },
  },
});

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

Теперь все вместе, я могу создать шаблон с моим обновленным variables.jsx:

return tpl`
// template: streamline-solid
import * as React from "react";
import { SvgIcon, SvgIconProps } from "@mui/material";
import { memo } from "react";
const ${variables.componentName} = (props: SvgIconProps) => {
  const { sx, ...other } = props;
  return (
    ${variables.jsx}
  );
};
 
${variables.exports};
`;
}

Потрясающий!

А как насчет причудливых свойств SVG, таких как ширина обводки и прочее?

И последняя задача, которую я хотел решить, — это то, как я могу работать с несколькими наборами значков, один из которых представляет собой набор значков контура. SVG могут использовать свойство ширины обводки, чтобы придать контурным значкам толщину:

<defs>
  <style>
    {
      ".bin_svg__a{fill:none;stroke:currentColor;stroke-linecap:round;stroke-linejoin:round;stroke-width:1.5px}"
    }
  </style>
</defs>

Значение обводки, которое мне нужно изменить, находится в конце: я хочу, чтобы 1,5 пикселя были равны 2 пикселям! Достаточно легко? О, нет. Если я думал, что предыдущий код, который я написал, был грязным, хак для этого заставил меня почувствовать, что мне нужен звуковой душ на самых высоких настройках.

Дочерний элемент style живет глубоко в объекте шаблона JSX (и куча дочерних элементов в дочерних массивах). Я не только не доверяю пути, по которому он сейчас существует, я особенно не доверяю синтаксису строки стиля. Совершенно очевидно, что это пары ключ:значение, разделенные точкой с запятой, но кто знает, могут ли некоторые из них иметь пробел между ними или какой-то другой мусор… Вместо того, чтобы писать синтаксический анализатор (который профессионально является правильным способом сделать это), Я прибегнул к позору регулярных выражений, чтобы решить свою проблему (теперь у меня две проблемы?)

Это работает, так что укуси меня! 💀

(Я ужасно извиняюсь за такое форматирование вложенных объектов, но путь — чистое безумие, а Medium не делает блоки кода широкими, так что получается очень длинный блок кода.)

Обратите особое внимание на мое использование ? необязательных цепочек здесь — это новый (наконец-то!) помощник в JavaScript, который позволяет нам глубоко проверять свойства объекта, чьи свойства могут существовать или не существовать, без выбрасывания Uncaught ReferenceError: _ is not defined повсюду.

if (
  variables
  .jsx
  .children?.[0]
  .children?.[0]
  .openingElement
  .name
  .name === "style" &&
  variables
  .jsx
  .children?.[0]
  .children?.[0]
  .children?.[0]
  .expression?.value.length > 0
) {
  // Replace 'stroke-width:__px' with 'stroke-width:2px'
  variables
  .jsx
  .children[0]
  .children[0]
  .children[0]
  .expression.value =
    variables
    .jsx
    .children[0]
    .children[0]
    .children[0]
    .expression
    .value.replace(
      /stroke-width:\s?[\d.]+px/g,
      "stroke-width:2px"
    );
}

Серьезно, загрузите эти пути! Благодарен за необязательную цепочку. И вы заметите, что суть в том, чтобы просто заменить stroke-width:2px:

.value.replace(
  /stroke-width:\s?[\d.]+px/g, // That's a regex, folks
  "stroke-width:2px"
);

Это сработало? Да, да, это было. Сделал бы я это снова? Да, да, я бы 🤔

Попробуйте!

Если вам понравилось это путешествие, и вы не любите сложных вещей и предпочли бы, чтобы кто-то сделал всю работу, создавая для вас пользовательские значки MUI (я шучу, я шучу), я собрал все это для вас на GitHub!

👉 https://github.com/brandonscript/mui-custom-svg-icons

А также, если у вас есть лучший способ сделать это, есть мысли, идеи или можете заставить это работать с ES6 import и export, пишите мне в репозиторий!

плавник
— Б

подробнее читайте на brandonscript.design
подписывайтесь на меня в Twitter, Polywork или LinkedIn