Как стать автором
Обновить
1782.89
Timeweb Cloud
То самое облако

Анатомия StyleX

Уровень сложностиСредний
Время на прочтение16 мин
Количество просмотров3.1K



Hello world!


По данным 2023 JavaScript Rising Stars библиотека StyleX заняла второе место в разделе Styling / CSS in JS (первое место вполне ожидаемо занял TailwindCSS).


stylex — это решение CSS в JS от Facebook, которое недавно стало открытым и быстро набрало популярность (на сегодняшний день у библиотеки 7500 звезд на Github). Это обусловлено ее легковесностью, производительностью и небольшим размером итоговой таблицы стилей.


В этой статье мы разберемся, как stylex работает. Но давайте начнем с примера ее использования.


Код проекта, который мы создадим, включая выдержки из исходного кода stylex (директория stylex), можно найти здесь.


Пример


В качестве примера создадим простой компонент кнопки, предусматривающий разные варианты стилизации.


Создаем шаблон приложения с помощью Vite:


# stylex-testing - название проекта
# react-ts - используемый шаблон
yarn create vite stylex-testing --template react-ts
# или
npm create vite@latest stylex-testing -- --template react-ts

Переходим в директорию и устанавливаем зависимости:


cd stylex-testing
yarn add @stylexjs/stylex
yarn add -D vite-plugin-stylex-dev
# или
npm i @stylexjs/stylex
npm i -D vite-plugin-stylex-dev

vite-plugin-stylex-dev — неофициальный плагин stylex для vite.


Редактируем файл vite.config.ts:


import react from '@vitejs/plugin-react'
import { defineConfig } from 'vite'
import { stylexPlugin } from 'vite-plugin-stylex-dev'

export default defineConfig({
  plugins: [react(), stylexPlugin()],
})

Определяем минимальные стили в файле index.css:


html,
body,
#root {
  height: 100%;
}
body {
  margin: 0;
}

Переходим к stylex.


Начнем с определения переменных/токенов. Создаем файл tokens.stylex.ts следующего содержания:


import stylex from '@stylexjs/stylex'

// Медиа-запрос темной цветовой схемы
const DARK = '@media (prefers-color-scheme: dark)'

// Цвета
export const colors = stylex.defineVars({
  light: { default: '#FBFBFB', [DARK]: '#332D2D' },
  dark: { default: '#332D2D', [DARK]: '#FBFBFB' },
  primary: '#3B71CA',
  success: '#14A44D',
})

// Отступы
export const spacing = stylex.defineVars({
  none: '0px',
  xsmall: '4px',
  small: '8px',
  medium: '12px',
  large: '16px',
})

Создаем файл components/Button.tsx для компонента кнопки. Определяем стили кнопки:


import stylex, { type StyleXStyles } from '@stylexjs/stylex'
import type { HTMLAttributes, PropsWithChildren } from 'react'
import { colors, spacing } from '../tokens.stylex'

// 3 варианта стилизации
const styles = stylex.create({
  default: {
    // Используем переменные
    // https://stylexjs.com/docs/learn/theming/using-variables/
    padding: `${spacing.small} ${spacing.large}`,
    backgroundColor: colors.light,
    border: `1px solid ${colors.dark}`,
    outline: 'none',
    borderRadius: spacing.small,
    boxShadow: {
      default: '0 2px 3px rgba(0, 0, 0, 0.25)',
      ':active': 'none',
    },
    cursor: 'pointer',
    transition: 'all 0.25s ease-in-out',
    ':hover': {
      backgroundColor: colors.dark,
      color: colors.light,
    },
  },
  primary: {
    backgroundColor: colors.primary,
    color: colors.light,
    ':hover': {
      backgroundColor: null,
      color: null,
    },
  },
  dark: {
    backgroundColor: colors.dark,
    color: colors.light,
    ':hover': {
      backgroundColor: colors.light,
      color: colors.dark,
    },
  },
})

Определение стилей с помощью stylex сильно напоминает то, как это делается в React Native.


Определяем типы пропов и компонент кнопки:


type Props = PropsWithChildren<
  HTMLAttributes<HTMLButtonElement> & {
    // Вариант перезаписывает стилизацию `default`
    variant?: 'primary' | 'dark'
    // Кастомные цвета фона и текста, определенные с помощью `stylex`
    // С точки зрения типизации стилей `stylex` лучше `tailwindcss`
    customStyles?: StyleXStyles<{
      backgroundColor?: string
      color?: string
    }>
  }
>

export default function Button({
  children,
  variant,
  customStyles,
  ...rest
}: Props) {
  return (
    <button
      // Применяем стили
      // https://stylexjs.com/docs/learn/styling-ui/using-styles/
      {...stylex.props(
        styles.default,
        variant && styles[variant],
        customStyles,
      )}
      {...rest}
    >
      {children}
    </button>
  )
}

Определяем стили контейнера и кастомной кнопки и рендерим несколько кнопок в файле App.tsx:


import stylex from '@stylexjs/stylex'
import Button from './components/Button'
import { colors, spacing } from './tokens.stylex'

// Стили контейнера и кастомной кнопки
const styles = stylex.create({
  app: {
    width: '100%',
    height: '100%',
    display: 'flex',
    justifyContent: 'center',
    alignItems: 'center',
    gap: spacing.medium,
  },
  customButton: {
    backgroundColor: colors.success,
    color: colors.light,
  },
})

export default function App() {
  return (
    <div {...stylex.props(styles.app)}>
      <Button>Default</Button>
      <Button variant='dark'>Dark</Button>
      <Button variant='primary'>Primary</Button>
      <Button customStyles={styles.customButton}>
        Custom
      </Button>
    </div>
  )
}

Запускаем сервер для разработки:


yarn dev
# или
npm run dev

Результат:





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


Давайте взглянем на стили и пропы, генерируемые stylex. Редактируем App.tsx следующим образом:


import stylex from '@stylexjs/stylex'

const stylesForLog = stylex.create({
  root: {
    display: 'flex',
    flexDirection: 'column',
  },
})
const stylesForLog2 = stylex.create({
  root2: {
    display: 'flex',
  },
})

// Утилита для красивого вывода
const stringify = (obj: Record<string, unknown>) => JSON.stringify(obj, null, 2)

export default function App() {
  console.log('result of create:', stringify(stylesForLog))
  console.log('result of create 2:', stringify(stylesForLog2))
  console.log(stylesForLog.root.display === stylesForLog2.root2.display)
  console.log(
    'result of props:',
    stringify(stylex.props(stylesForLog.root, stylesForLog2.root2)),
  )
  console.log(
    'result of props 2:',
    stringify(
      // @ts-ignore
      stylex.props(stylesForLog.root, stylesForLog2.root2, { display: 'flex' }),
    ),
  )

  return (
    <div {...stylex.props(stylesForLog.root)}></div>
  )
}

Вот что мы видим в консоли:





Строки типа App__stylesForLog.root нужны для отладки, поэтому их можно игнорировать.


display: "flex" превратилось в display: "x78zum5", причем поле display в обоих случаях имеет одно и тоже значение, что подтверждается сравнением stylesForLog.root.display === stylesForLog2.root2.display. Это наводит на мысль о том, что строки типа x78zum5 — это хеш правил CSS (свойство: значение).


Значения полей объекта styles объединяются и становятся значением поля className объекта props (например, className: "xdt5ytf x78zum5"). className путем распаковки объекта props передается компоненту (устанавливается элементу button).


При передаче display: flex в виде простого объекта возвращается поле style со значением в виде этого объекта. При этом, соответствующий хеш-класс опускается.


Запомните поле $$css: true, мы вернемся к нему позже.


Взглянем на разметку:





В таблице стилей с data-vite-dev-id=" vite-plugin:stylex.css" мы видим "хеш-классы" и соответствующие им стили: .x78zum5{display:flex}.xdt5ytf{flex-direction:column}. Те же хеш-классы мы видим у контейнера: <div class="x78zum5 xdt5ytf"></div>.


Выполняем сборку приложения:


yarn build
# или
npm run build

Открываем файл dist/assets/index-[hash].css:


/* stylex */
.x78zum5 {
  display: flex;
}
.xdt5ytf {
  flex-direction: column;
}
/* index.css */
html,
body,
#root {
  height: 100%;
}
body {
  margin: 0;
}

Отлично. Начнем погружаться в исходный код stylex.


Реверс-инжиниринг


Обратите внимание: дальнейший разбор кода актуален для stylex@0.4.1. В будущем код может и наверняка изменится, возможно, до неузнаваемости 😊


Также обратите внимание, что с целью упрощения кода для облегчения его восприятия я беспощадно удалял строки и даже целые блоки кода 😁


Мы ограничимся изучением работы методов create (создание стилей) и props (применение стилей).


Выдержки из кода соответствуют порядку применения переменных и функций, а не порядку их определения.


Начнем с файла packages/stylex/src/stylex.js:


// 294 - номер строки кода
export default _stylex

// 239
function _stylex(...styles) {
  const [className] = styleq(styles)
  return className
}
_stylex.props = props
_stylex.create = create

// 36
// Об этой библиотеке поговорим позже
import { styleq } from 'styleq'

// 136
export const create = stylexCreate

// 76
function stylexCreate(styles) {
  if (__implementations.create != null) {
    const create = __implementations.create
    return create(styles)
  }
  throw new Error(
    'stylex.create should never be called. It should be compiled away.',
  )
}

// 280
const __implementations = {}

// 282
export function __monkey_patch__(key, implementation) {
  __implementations[key] = implementation
}

Метод create извлекается из объекта __implementations, который инициализируется с помощью функции __monkey_patch__.


Следуем за __monkey_patch__() в файл packages/dev-runtime/src/index.js:


// 10
import { __monkey_patch__ } from '@stylexjs/stylex'
import { styleSheet } from '@stylexjs/stylex/lib/StyleXSheet';

// 20
import getStyleXCreate from './stylex-create';

// 45
export default function inject({
  insert = defaultInsert,
  ...config
}) {
  // Инициализация `create()`
  __monkey_patch__('create', getStyleXCreate({ ...config, insert }));
}

// 24
const defaultInsert = (
  key,
  ltrRule,
  priority,
  // На это можно не обращать особого внимания,
  // это стили для направления текста "справа налево"
  rtlRule,
) => {
  if (priority === 0) {
    if (injectedVariableObjs.has(key)) {
      throw new Error('A VarGroup with this name already exists: ' + key);
    } else {
      injectedVariableObjs.add(key);
    }
  }
  styleSheet.insert(ltrRule, priority, rtlRule);
};

// 22
const injectedVariableObjs = new Set();

Метод create — это функция, возвращаемая getStyleXCreate({ ...config, defaultInsert }).


В функции defaultInsert вызывается метод insert объекта styleSheet. Этот объект создается в файле packages/stylex/src/StyleXSheet.js:


// 364
export const styleSheet = new StyleXSheet({
  supportsVariables: true,
  rootTheme: {},
  rootDarkTheme: {},
});

// 85
export class StyleXSheet {
  static LIGHT_MODE_CLASS_NAME = LIGHT_MODE_CLASS_NAME;
  static DARK_MODE_CLASS_NAME = DARK_MODE_CLASS_NAME;

  constructor(opts) {
    this.tag = null;
    this.injected = false;
    this.ruleForPriority = new Map();
    this.rules = [];

    this.rootTheme = opts.rootTheme;
    this.rootDarkTheme = opts.rootDarkTheme;
  }

  // Цветовые схемы
  rootTheme;
  rootDarkTheme;

  // Массив, содержащий все добавленные правила. Используется для
  // отслеживания индексов правил в таблице стилей
  rules;

  // Индикатор добавления тега `style` в `document`
  injected;

  // Элемент `style` для добавления правил
  tag;

  // Для поддержки приоритетов необходимо хранить правило,
  // которое находится в начале приоритета
  ruleForPriority;

  /**
   * Извлекает тег `style`
   */
  getTag() {
    const { tag } = this;
    invariant(tag != null, 'expected tag');
    return tag;
  }

  /**
   * Добавляет тег `style` в `head`
   */
  inject() {
    if (this.injected) {
      return;
    }

    this.injected = true;

    // Создаем тег `style`
    this.tag = makeStyleTag();
    this.injectTheme();
  }

  /**
   * Вставляет стили темы - переменные/токены
   */
  injectTheme() {
    if (this.rootTheme != null) {
      this.insert(
        buildTheme(`:root, .${LIGHT_MODE_CLASS_NAME}`, this.rootTheme),
        0,
      );
    }
    if (this.rootDarkTheme != null) {
      this.insert(
        buildTheme(
          `.${DARK_MODE_CLASS_NAME}:root, .${DARK_MODE_CLASS_NAME}`,
          this.rootDarkTheme,
        ),
        0,
      );
    }
  }

  /**
   * Добавляет правила в таблицу стилей
   */
  insert(rawLTRRule, priority) {
    // Добавляем таблицу стилей при отсутствии
    if (this.injected === false) {
      this.inject();
    }

    const rawRule = rawLTRRule;

    // Не добавляем правило при наличии (исключаем дубликаты)
    if (this.rules.includes(rawRule)) {
      return;
    }

    // Нормализованное правило с определенной специфичностью,
    // это нас не интересует
    const rule = this.normalizeRule(
      addSpecificityLevel(rawRule, Math.floor(priority / 1000)),
    );

    // Получаем позицию для вставки правила по его приоритету,
    // это нас не интересует
    const insertPos = this.getInsertPositionForPriority(priority);
    this.rules.splice(insertPos, 0, rule);

    // Устанавливаем правило как конец группы приоритета
    this.ruleForPriority.set(priority, rule);

    const tag = this.getTag();
    const sheet = tag.sheet;

    if (sheet != null) {
      try {
        // https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleSheet/insertRule
        sheet.insertRule(rule, Math.min(insertPos, sheet.cssRules.length));
      } catch (err) {
        console.error('insertRule error', err, rule, insertPos);
      }
    }
  }
}

// 344
/**
 * Функция добавления `:not(#\#)` для повышения специфичности; полифилл для @layer
 */
function addSpecificityLevel(selector, index) {
  if (selector.startsWith('@keyframes')) {
    return selector;
  }
  // Чем больше `not(#\\#)`, тем выше специфичность
  const pseudo = Array.from({ length: index })
    .map(() => ':not(#\\#)')
    .join('');

  const lastOpenCurly = selector.includes('::')
    ? selector.indexOf('::')
    : selector.lastIndexOf('{');
  const beforeCurly = selector.slice(0, lastOpenCurly);
  const afterCurly = selector.slice(lastOpenCurly);

  return `${beforeCurly}${pseudo}${afterCurly}`;
}

// 45
/**
 * Создает тег `style` и добавляет его в `head`
 */
function makeStyleTag() {
  const tag = document.createElement('style');
  tag.setAttribute('type', 'text/css');
  tag.setAttribute('data-stylex', 'true');

  const head = document.head || document.getElementsByTagName('head')[0];
  invariant(head, 'expected head');
  head.appendChild(tag);

  return tag;
}

// 28
/**
 * Принимает тему и генерирует переменные CSS
 */
function buildTheme(selector, theme) {
  const lines = [];
  lines.push(`${selector} {`);

  for (const key in theme) {
    const value = theme[key];
    lines.push(`  --${key}: ${value};`);
  }

  lines.push('}');

  return lines.join('\n');
}

Здесь основной является строка sheet.insertRule(rule, position);, отвечающая за однократное добавление переданного правила в таблицу стилей с учетом его приоритета.


Возвращаемся в файл packages/dev-runtime/src/index.js и следуем за getStyleXCreate в файл packages/dev-runtime/src/stylex-create.js:


// 177
export default function getStyleXCreate(
  config,
) {
  const stylexCreate = (
    styles,
  ) => {
    return createWithFns(styles, config);
  };

  return stylexCreate;
}

// 106
// Значением поля объекта стилей может быть функция,
// мы не будем рассматривать такой вариант
function createWithFns(
  styles,
  { insert, ...config },
) {
  const stylesWithoutFns = {};
  for (const key in styles) {
    const value = styles[key];
    stylesWithoutFns[key] = value;
  }
  // Одна из самых важных строк!
  const [compiledStyles, injectedStyles] = create(stylesWithoutFns, config);
  // Добавляем хеш-классы и стили в таблицу стилей
  for (const key in injectedStyles) {
    const { ltr, priority, rtl } = injectedStyles[key];
    insert(key, ltr, priority, rtl);
  }

  const temp = compiledStyles;

  const finalStyles = { ...temp };

  // Возвращаем скомпилированные стили
  return finalStyles;
}

// 16
import { create, IncludedStyles, utils } from '@stylexjs/shared';

Функция getStyleXCreate возвращает функцию stylexCreate, которая возвращает функцию createWithFns, которая вызывает функцию create, добавляет одни стили (injectedStyles) в таблицу стилей и возвращает другие (finalStyles). finalStyles — это стили, которые впоследствии передаются в метод stylex.props, которая находится в файле packages/stylex/src/stylex.js:


// 47
export function props(
  this,
  ...styles
) {
  const options = this;
  if (__implementations.props) {
    return __implementations.props.call(options, styles);
  }
  // Мы подробнее поговорим об этой библиотеке в конце
  const [className, style] = styleq(styles);
  const result = {};
  if (className != null && className !== '') {
    result.className = className;
  }
  if (style != null && Object.keys(style).length > 0) {
    result.style = style;
  }
  // result = { className: "хеш-классы", style: { ...стили } }
  return result;
}

Следуем за функцией create в файл packages/shared/src/index.js:


// 43
export const create = styleXCreateSet;

// 23
import styleXCreateSet from './stylex-create';

Без комментариев 😊 Следуем за функцией styleXCreateSet в файл packages/shared/src/stylex-create.js:


// 24
// Эта функция принимает объект со стилями, передаваемый в метод `stylex.create` и преобразует его.
// Преобразование заключается в замене значений стилей на названия CSS-классов.
//
// Функция также собирает все внедряемые (injected) стили.
// Возвращает кортеж с преобразованным объектом стилей и объектом внедряемых стилей.
//
// Перед возвратом выполняется проверка отсутствия дубликатов во внедряемых стилях.
export default function styleXCreateSet(
  namespaces,
  options = defaultOptions,
) {
  const resolvedNamespaces = {};
  const injectedStyles = {};

  for (const namespaceName of Object.keys(namespaces)) {
    const namespace = namespaces[namespaceName];

    const flattenedNamespace = flattenRawStyleObject(namespace, options);
    const compiledNamespaceTuples = flattenedNamespace.map(([key, value]) => {
      return [key, value.compiled(options)];
    });

    const compiledNamespace = objFromEntries(compiledNamespaceTuples);

    const namespaceObj = {};
    for (const key of Object.keys(compiledNamespace)) {
      const value = compiledNamespace[key];
      if (value instanceof IncludedStyles) {
        namespaceObj[key] = value;
      } else {
        const classNameTuples =
          value.map((v) => (Array.isArray(v) ? v : null)).filter(Boolean);
        const className =
          classNameTuples.map(([className]) => className).join(' ') || null;
        namespaceObj[key] = className;
        for (const [className, injectable] of classNameTuples) {
          if (injectedStyles[className] == null) {
            injectedStyles[className] = injectable;
          }
        }
      }
    }
    // `$$css: true` требуется для `styleq`
    resolvedNamespaces[namespaceName] = { ...namespaceObj, $$css: true };
  }

  return [resolvedNamespaces, injectedStyles];
}

Весь рассмотренный код можно найти в файле stylex/source-code.js нашего проекта.


Код функции styleXCreateSet и всех используемых ей утилит занимает почти 800 строк. Я вынес его в отдельный файл stylex/create-props.js. Вы можете внимательно изучить его самостоятельно, я же остановлюсь только на основных моментах.


Посмотрим на результат, возвращаемый функцией styleXCreateSet:


const [compiledStyles, injectedStyles] = styleXCreateSet({
  root: {
    display: 'flex',
    flexDirection: 'column',
  },
})
console.log(stringify({ compiledStyles, injectedStyles }))

Результат:


{
  "compiledStyles": {
    "root": {
      "display": "x78zum5",
      "flexDirection": "xdt5ytf",
      "$$css": true
    }
  },
  "injectedStyles": {
    "x78zum5": {
      "priority": 3000,
      "ltr": ".x78zum5{display:flex}",
      "rtl": null
    },
    "xdt5ytf": {
      "priority": 3000,
      "ltr": ".xdt5ytf{flex-direction:column}",
      "rtl": null
    }
  }
}

injectedStyles добавляются в таблицу стилей через метод styleSheet.insert. compiledStyles пропускаются через библиотеку styleq и передаются компоненту.


Вызовем функцию props:


const result = props(compiledStyles.root)
console.log(stringify({ result }))
const result2 = props(compiledStyles.root, { display: 'flex' })
console.log(stringify({ result2 }))

Результат:


{
  "result": {
    "className": "x78zum5 xdt5ytf"
  }
}

{
  "result2": {
    // Обратите внимание на отсутствие хеш-класса для `display: flex`
    "className": "xdt5ytf",
    "style": {
      "display": "flex"
    }
  }
}

Посмотрим на цепочку преобразований такого объекта:


{
  root: {
    display: 'flex',
  },
}

в такие:


{
  "compiledStyles": {
    "root": {
      "display": "x78zum5",
      "$$css": true
    }
  },
  "injectedStyles": {
    "x78zum5": {
      "priority": 3000,
      "ltr": ".x78zum5{display:flex}",
      "rtl": null
    }
  }
}

function styleXCreateSet(namespaces, options = defaultOptions) {
  const resolvedNamespaces = {}
  const injectedStyles = {}

  for (const namespaceName of Object.keys(namespaces)) {
    const namespace = namespaces[namespaceName]
    // !
    console.log(stringify({ namespace }))

    const flattenedNamespace = flattenRawStyleObject(namespace, options)
    // !
    console.log(stringify({ flattenedNamespace }))

    const compiledNamespaceTuples = flattenedNamespace.map(([key, value]) => {
      return [key, value.compiled(options)]
    })
    // !
    console.log(stringify({ compiledNamespaceTuples }))

    const compiledNamespace = objFromEntries(compiledNamespaceTuples)
    // !
    console.log(stringify({ compiledNamespace }))

    const namespaceObj = {}
    for (const key of Object.keys(compiledNamespace)) {
      const value = compiledNamespace[key]
      const classNameTuples = value
        .map((v) => (Array.isArray(v) ? v : null))
        .filter(Boolean)
      const className =
        classNameTuples.map(([className]) => className).join(' ') || null
      namespaceObj[key] = className
      for (const [className, injectable] of classNameTuples) {
        if (injectedStyles[className] == null) {
          injectedStyles[className] = injectable
        }
      }
    }
    resolvedNamespaces[namespaceName] = { ...namespaceObj, $$css: true }
  }

  return [resolvedNamespaces, injectedStyles]
}

Результат:


{
  "namespace": {
    "display": "flex"
  }
}

{
  "flattenedNamespace": [
    [
      "display",
      {
        "property": "display",
        "value": "flex",
        "pseudos": [],
        "atRules": []
      }
    ]
  ]
}

{
  "compiledNamespaceTuples": [
    [
      "display",
      [
        [
          "x78zum5",
          {
            "priority": 3000,
            "ltr": ".x78zum5{display:flex}",
            "rtl": null
          }
        ]
      ]
    ]
  ]
}

{
  "compiledNamespace": {
    "display": [
      [
        "x78zum5",
        {
          "priority": 3000,
          "ltr": ".x78zum5{display:flex}",
          "rtl": null
        }
      ]
    ]
  }
}

За генерацию хеша на основе правила CSS отвечает функция convertStyleToClassName:


function convertStyleToClassName(
  objEntry,
  pseudos,
  atRules,
  options = defaultOptions,
) {
  // !
  console.log(stringify({ objEntry, pseudos, atRules }))

  const { classNamePrefix = 'x' } = options
  const [key, rawValue] = objEntry
  const dashedKey = dashify(key)

  const value = Array.isArray(rawValue)
    ? rawValue.map((eachValue) => transformValue(key, eachValue, options))
    : transformValue(key, rawValue, options)

  const sortedPseudos = arraySort(pseudos ?? [])
  const sortedAtRules = arraySort(atRules ?? [])

  const atRuleHashString = sortedPseudos.join('')
  const pseudoHashString = sortedAtRules.join('')

  const modifierHashString = atRuleHashString + pseudoHashString || 'null'

  const stringToHash = Array.isArray(value)
    ? dashedKey + value.join(', ') + modifierHashString
    : dashedKey + value + modifierHashString
  // !
  console.log(stringify({ dashedKey, value, modifierHashString, stringToHash }))

  // Обратите внимание: `<>` используется для обеспечения стабильности хешей.
  // Это должно быть удалено в будущих версиях
  const className = classNamePrefix + createHash('<>' + stringToHash)

  const cssRules = generateRule(className, dashedKey, value, pseudos, atRules)
  // !
  console.log(stringify({ key, className, cssRules }))
  return [key, className, cssRules]
}

Результат:


{
  "objEntry": [
    "display",
    "flex"
  ],
  "pseudos": [],
  "atRules": []
}

{
  "dashedKey": "display",
  "value": "flex",
  "modifierHashString": "null",
  // Строка для хеширования
  "stringToHash": "displayflexnull"
}

{
  "key": "display",
  // Результат хеширования
  "className": "x78zum5",
  "cssRules": {
    "priority": 3000,
    "ltr": ".x78zum5{display:flex}",
    "rtl": null
  }
}

Строка "displayflexnull" преобразуется/хешируется в x78zum5


Настоящая магия происходит в функции createHash:


function createHash(str) {
  return murmurhash2_32_gc(str, 1).toString(36)
}

function murmurhash2_32_gc(str, seed = 0) {
  let l = str.length,
    h = seed ^ l,
    i = 0,
    k

  while (l >= 4) {
    k =
      (str.charCodeAt(i) & 0xff) |
      ((str.charCodeAt(++i) & 0xff) << 8) |
      ((str.charCodeAt(++i) & 0xff) << 16) |
      ((str.charCodeAt(++i) & 0xff) << 24)

    k = (k & 0xffff) * 0x5bd1e995 + ((((k >>> 16) * 0x5bd1e995) & 0xffff) << 16)
    k ^= k >>> 24
    k = (k & 0xffff) * 0x5bd1e995 + ((((k >>> 16) * 0x5bd1e995) & 0xffff) << 16)

    h =
      ((h & 0xffff) * 0x5bd1e995 +
        ((((h >>> 16) * 0x5bd1e995) & 0xffff) << 16)) ^
      k

    l -= 4
    ++i
  }

  switch (l) {
    case 3:
      h ^= (str.charCodeAt(i + 2) & 0xff) << 16
    case 2:
      h ^= (str.charCodeAt(i + 1) & 0xff) << 8
    case 1:
      h ^= str.charCodeAt(i) & 0xff
      h =
        (h & 0xffff) * 0x5bd1e995 + ((((h >>> 16) * 0x5bd1e995) & 0xffff) << 16)
  }

  h ^= h >>> 13
  h = (h & 0xffff) * 0x5bd1e995 + ((((h >>> 16) * 0x5bd1e995) & 0xffff) << 16)
  h ^= h >>> 15

  return h >>> 0
}

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


В заключение, поговорим о функции styleq, которая вызывается в функции props:


const [className, style] = styleq(styles);

styleq — это быстрая и небольшая среда выполнения JavaScript для объединения названий классов HTML, созданных компиляторами CSS. Вызов styleq(...styles) объединяет объекты стилей и генерирует строку className и объект встроенных (inline) стилей (с названиями свойств в стиле camelCase):


const [className, inlineStyle] = styleq(styles.root, { opacity });

Функция styleq эффективно объединяет глубоко вложенные массивы как извлеченных (extracted), так и встроенных объектов стилей:


  • компилируемые стили должны содержать свойство $$css со значением true (помните resolvedNamespaces[namespaceName] = { ...namespaceObj, $$css: true }?)
  • объект стилей без свойства $$css считается встраиваемым стилем
  • компилируемые стили должны быть статичными для лучшей производительности
  • ключи объектов компилируемых стилей не обязательно должны совпадать с названиями свойств CSS; разрешена любая строка
  • значения компилируемого объекта стилей должны быть строками классов HTML

const styles = {
  root: {
    // Обязательное поле
    $$css: true,
    // Классы для отладки
    'debug::file:styles.root': 'debug::file:styles.root',
    // Атомарные классы
    display: 'display-flex-class',
    alignItems: 'alignItems-center-class'
  }
};

const [className, inlineStyle] = styleq(styles.root, props.style);

Таким образом, styleq обеспечивает эффективное объединение хеш-классов в одну строку className без дублирования и с учетом встроенных стилей, которые возвращаются в виде объекта style. className и style возвращаются в виде, пригодном для прямой передачи компоненту React для установки элементу HTML в качестве соответствующих атрибутов.


Пожалуй, это все, о чем я хотел рассказать вам в этой статье.


Happy coding!




Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud — в нашем Telegram-канале

Теги:
Хабы:
Всего голосов 15: ↑15 и ↓0+15
Комментарии4

Публикации

Информация

Сайт
timeweb.cloud
Дата регистрации
Дата основания
Численность
201–500 человек
Местоположение
Россия
Представитель
Timeweb Cloud