javascript

Анатомия StyleX

  • четверг, 7 марта 2024 г. в 00:00:20
https://habr.com/ru/companies/timeweb/articles/795473/



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-канале