К содержанию

Навигация

Как работает Loader

Loader находит на странице классы и теги Framework и загружает только нужные модули. Здесь — правила загрузки, зависимости, кеш, события и пакеты компонентов.

Идея

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

Отсюда главное правило для автора страницы: пишите классы и теги Framework в разметке, а не подключайте файлы. Loader прочитает разметку сам — в том числе ту, что появляется позже (см. Наблюдение за изменениями страницы).

Правила загрузки

У каждого модуля в исходниках есть файл rule.js с правилом: по какому признаку в разметке Loader узнаёт, что модуль нужен. При сборке правила собираются в общий реестр SF.RuleLoader (в поставке — distr/rule/). Правила пакета компонентов приходят из его собственного файла правил (см. Пакеты компонентов).

Правила бывают трёх видов.

Утилита — регулярное выражение, которое проверяет имя класса. Ключ правила — имя модуля и контрольная точка:

JavaScript
SF.RuleLoader["gap/default"] = new RegExp("^(?:gap-(?:0|1(?:\\/(?:2|3|4))?|2|…))(?![\\s\\S])");

(выражение сокращено). Класс gap-2 подходит — Loader подключит модуль gap/default.

Обычный компонент — объект с выражением и указанием, что грузить:

JavaScript
SF.RuleLoader['buttons'] = {
  regex: /class\s*=\s*["'][^"']*\bsf-button\b/i,
  type: 'component',
  css: true,
  js: true,
};

Smart-компонент — объект с тегами и зависимостями:

JavaScript
SF.RuleLoader['cl-datepicker'] = {
  tags: ['sf-datepicker'],
  type: 'smart',
  mode: 'smart',
  js: true,
  relation: [
    { name: 'datepicker' },
    { name: 'cl-inputs' },
    { name: 'cl-buttons' },
    { name: 'cl-icon-buttons' },
    { name: 'cl-icons' },
  ],
};

Имя правила Smart-компонента начинается с cl-, папка в поставке — без него: cl-datepicker → smart/datepicker/. Это внутреннее имя: в разметке и коде проекта используется только тег <sf-datepicker>.

Полный список правил выпуска — distr/rule/rule.json: имя, теги, вид и связи каждого правила. Ядро несёт те же правила в distr/core/js/core-rules.js.

Модули утилит и контрольные точки

Утилиты разбиты на модули по смыслу (gap, display, font-size…), а каждый модуль — на файлы по контрольным точкам: default, sm, md, lg, xl, xxl, а у части модулей ещё и по состояниям: hover, focus, focus-visible, active. Класс gap-2 подключает только utility/gap/default/, класс с условием md: — ещё и utility/gap/md/. Синтаксис условий описан в Условия действия.

У некоторых модулей есть расширенная пара (font-size-ext, background-size-ext): редкие значения вынесены отдельно и подключаются, только когда на странице встречаются именно они.

Адрес файла Loader вычисляет по имени модуля и типу:

Что Адрес
Утилита gap/md <sfPath>/utility/gap/md/css/md.min.css
Компонент buttons <sfPath>/component/buttons/css/buttons.min.css и …/js/buttons.min.js
Smart cl-datepicker <sfPath>/smart/datepicker/js/datepicker.min.js
Smart cl-catalog из пакета shop <base>/catalog/js/catalog.min.js, где <base> — каталог пакета (см. Пакеты компонентов)

В таблице — адреса сжатых файлов, которые Loader берёт по умолчанию; в режиме отладки он берёт те же адреса без .min (см. Сжатые файлы).

Smart-компонент по умолчанию не грузит свой CSS: его вид дают классы обычного компонента, который подтягивается как зависимость.

Зависимости

Поле relation перечисляет модули, без которых компонент не работает. Для <sf-datepicker> это обычный компонент календаря, Smart-поле ввода, кнопки, кнопки-иконки и иконки. Loader загружает зависимости до самого компонента и ждёт их готовности. Поэтому автор страницы пишет один тег, а не пять.

Зависимость загружается один раз на страницу, даже если её требуют несколько компонентов.

Отложенная зависимость. Часть зависимостей компонент показывает не сразу: редактор фильтра, выбор даты, редактор ячейки для типа, которого на странице может не быть. Такую связь правило отмечает lazy: true. Пример — фрагмент правила sf-table из пакета larena:

JavaScript
relation: [
  { name: "cl-checkbox" },
  { name: "cl-datepicker", lazy: true },
  { name: "cl-modal", lazy: true },
],

Компонент не ждёт отложенную зависимость, и первый показ страницы её тоже не ждёт. Loader загружает её после sf-loader-ready, когда браузер свободен, по нескольку за раз, а сразу — если её попросили: SF.Loader.ensurePlugin('cl-datepicker') или её элемент появился в разметке. Код, который создаёт такой элемент и сразу обращается к нему, сначала ждёт customElements.whenDefined('sf-datepicker').

Что происходит при открытии страницы

  1. Ядро. core.js ставит класс темы на <html> (по cookie sf-theme или системной настройке), атрибут dir, если его нет, и прячет body до готовности, чтобы не мелькнула неоформленная разметка.
  2. Правила и Loader. Ядро догружает реестр правил и сам Loader.
  3. Кеш. Если эта страница уже открывалась и заданы метки cacheVersion и pluginListVersion, Loader сразу запускает загрузку модулей из сохранённого списка (см. Кеш и версии адресов), не дожидаясь разбора разметки.
  4. Разбор разметки. После загрузки документа Loader обходит DOM: ищет теги Smart-компонентов, иконки, атрибуты и классы, сверяя их с правилами. Элементы script, style, head, meta, link и элементы с data-sf-observer="ignore" не просматриваются вместе с содержимым. Встроенный SVG просматривается: классы на <svg>, <g> и фигурах (svg-*, fill-*, stroke-*) загружают свои модули.
  5. Загрузка. Найденные модули и их зависимости грузятся параллельно; первыми идут модули раскладки (контейнер, flex, grid, отступы, размеры). С первым Smart-элементом в очереди ядро запрашивает общую основу Smart-компонентов (см. Общая основа Smart-компонентов).
  6. Готово. Когда первая партия загружена, а общая основа, если её запросили, пришла, Loader посылает событие sf-loader-ready, и body становится видимым.
  7. Отложенные зависимости. После sf-loader-ready Loader догружает отложенные зависимости (см. Зависимости).

Если загрузка идёт дольше 300 мс, показывается индикатор загрузки. Его можно выключить: SF_BOOT_CONFIG = {preloader: {enabled: false}}.

Наблюдение за изменениями страницы

После первого прохода Loader не останавливается. Он следит за документом через MutationObserver: новые узлы, изменённые классы и атрибуты. Изменения собираются пачкой (около 100 мс) и разбираются вместе, так что вставка большого фрагмента не запускает сотню отдельных проверок.

Поэтому разметка, пришедшая через AJAX, созданная шаблонизатором на клиенте или вставленная скриптом, получает свои модули так же, как исходная.

Область, которую Loader просматривать не должен (например, чужой виджет или редактор кода), помечается data-sf-observer="ignore".

Если на странице есть Hotwire Turbo, Loader при переходе между страницами сбрасывает состояние и разбирает новую страницу заново.

Кеш и версии адресов

У Loader три вида памяти, и их легко спутать.

Список модулей страницы. После загрузки Loader сохраняет в localStorage список модулей, которые понадобились странице. Список и запись об отсутствующих файлах переживают перезагрузку, только если заданы обе метки, cacheVersion и pluginListVersion: без метки Loader при запуске очищает сохранённое. Ключ строится по адресу страницы (у адресов с идентификатором в конце последний сегмент отбрасывается, чтобы все карточки одного типа делили один список). При следующем открытии модули начинают грузиться сразу. Это ускорение, а не источник истины: разбор разметки всё равно выполняется и догружает недостающее.

Метка версии в адресах. Если задан cacheVersion (или pluginListVersion), Loader добавляет к адресу каждого файла параметр sf_v=<версия>. Смена метки означает новые адреса — браузер и CDN не отдадут старый файл. При смене метки Loader очищает и свой сохранённый список.

Какое значение дать меткам. Метка — любая непустая строка; Loader сравнивает её с сохранённой как есть. Для своей копии поставки обе метки задают равными идентификатору выпуска, например ui-106d5901f8c3: метка меняется ровно тогда, когда меняется выпуск (см. Глобальные настройки).

Список отсутствующих файлов. Если JavaScript модуля не нашёлся, Loader запоминает это и не запрашивает его снова. Неудавшийся CSS не запоминается: незавершённое подключение удаляется, и после перезагрузки страницы попытка повторится.

Сохранённый список можно сбросить, открыв страницу с параметром ?loader_clear=Y. Записи кеша имеют свой формат с префиксом; записи в другом формате Loader игнорирует и строит кеш заново.

Готовый план для рабочей сборки

Динамический поиск удобен, но на первом показе страница ждёт, пока Loader прочитает разметку. Проект со своей сборкой может посчитать нужные модули заранее и подключить их сам. Для этого в поставке ui есть планировщик scripts/plan-framework-assets.py: он читает итоговый HTML и тот же реестр правил, что и Loader, и выдаёт план файлов первого показа.

Проект сообщает Loader о заранее подключённом через window.SF_PRELOADED до запуска ядра. Тогда Loader:

  • считает перечисленные модули уже загруженными;
  • не прячет body и сразу объявляет готовность;
  • не берёт список модулей из localStorage;
  • по-прежнему находит и догружает то, что появляется на странице позже.

Проекты без сборки ничего не меняют: без SF_PRELOADED работает обычный динамический поиск.

Сжатые файлы

Loader загружает сжатые версии модулей: <имя>.min.js и <имя>.min.css. Само ядро страница подключает в любой из двух форм: core/css/core.min.css и core/js/core.min.js — сжатые, core/css/core.css и core/js/core.js — читаемые; сжатое ядро загружает сжатые части Core. Модуль, у которого нет сжатой версии, загружается в обычном виде, один раз.

Читаемые файлы нужны при отладке. Их включает любой из трёх способов:

  • window.SF_BOOT_CONFIG = {minified: false} до core.js;
  • параметр ?sf-debug в адресе страницы;
  • атрибут data-sf-debug на <html>.

Эти способы переключают модули, которые загружает Loader. Форму частей ядра выбирает файл ядра на странице: core.js загружает читаемые части, core.min.js — сжатые. Файл, который страница уже подключила сама в любой из двух форм, Loader второй раз не загружает.

Читаемые модули есть только в полной поставке из репозитория ui. В пакете npm @simai-studio/ui, с которого страница подключается по CDN, у модулей утилит, компонентов и Smart-компонентов только сжатая форма, а ядро — в обеих (см. Пакет npm). С адреса пакета режим отладки не работает: читаемые модули не загрузятся, и Smart-компоненты останутся неопределёнными. Для отладки страницу подключают к своей копии поставки.

Общая основа Smart-компонентов

Smart-компоненты страницы работают на одной общей основе — core/js/smart-base.js (в сжатом виде smart-base.min.js). В ней Lit, базовый класс Smart-компонентов, язык страницы, позиционирование и отступы контролов. Ядро загружает её само, когда в очередь загрузки попадает первый Smart-элемент, в той же форме, что и свой файл: рядом с core.min.js — smart-base.min.js, рядом с core.js — smart-base.js. На странице без Smart-элементов основа не загружается.

Сами модули Smart-компонентов основу не содержат. Модуль ждёт её через SF.runtimeModule(id) и регистрирует свой элемент, когда она пришла. Поэтому элемент определяется не сразу после загрузки своего скрипта: дожидайтесь его через SF.whenDefined(имя) или customElements.whenDefined(имя), а не проверяйте сразу. Когда основа пришла, на window приходит событие sf-runtime-ready.

Страница, которая подключает скрипты сама, без Loader, ставит smart-base.js после core.js, а без Core — перед модулями Smart-компонентов.

Иконки

Шрифт иконок Material Symbols огромен, а странице нужно несколько десятков знаков. Loader находит на странице иконки (.sf-icon и <sf-icon>), запрашивает у сервиса иконок подмножество шрифта только с ними и подменяет шрифт, когда новое подмножество готово. Появились новые иконки — запрашивается расширенное подмножество. Три начертания (outlined, rounded, sharp) учитываются отдельно.

Если сервис недоступен, Loader берёт полные шрифты из самой поставки (<sfPath>/component/icons/fonts/) и пишет одно диагностическое сообщение на начертание. Адрес @latest Loader не использует. Разметка иконок от этого не меняется, обязательной настройки тоже нет. Тонкая настройка — ключ icons в SF_BOOT_CONFIG (включение, начертание по умолчанию, адрес сервиса).

Каждый запрос возвращает один шрифт со всеми иконками, найденными к этому моменту, поэтому иконка, которая появляется только вместе с данными, стоит второго запроса. Такие иконки объявляют заранее в SF_BOOT_CONFIG.icons.preload (имена или записи {icon, weight, filled, grade, opticalSize, type}), а компонент, который узнаёт их во время работы, передаёт те же записи в SF.Loader.preloadIcons([...]) — они попадают в первый запрос. Пока шрифт загружается, у корня стоит data-sf-icons="subset-pending" и иконки не рисуются. Подробно — на странице «Иконки».

События загрузчика

Событие Где Когда
sf-loader-init window Loader создан: SF.Loader, SF.ready, SF.create, SF.createAsync, SF.whenDefined, SF.loadSmartBase, SF.smartBaseReady, SF.whenFieldRenderers и SF.RuleLoader.registerPackage доступны
sf-loader-ready window Первая партия модулей загружена. Событие отправляется один раз: обработчик, добавленный позже, не сработает
sf-runtime-ready window Пришла общая основа Smart-компонентов — модули, против которых собраны их скрипты (см. Общая основа Smart-компонентов)
sf-smart-base-ready window Загружен базовый класс Smart-компонентов: есть SF.smart; один раз на страницу. Надёжнее событий — await SF.loadSmartBase(): обещание выполняется и тогда, когда база уже пришла
sf-theme-change window Сменилась тема или выбор темы; detail: {theme, preference}. Приходит и один раз при запуске, когда Loader впервые выбрал тему (см. Как выбирается тема на странице)
component:ready document Зарегистрирован обычный компонент (detail.name — имя в реестре, например Inputs). Объект SF.<Имя> к этому моменту уже назначен

Когда доступен каждый SF.*. Ядро появляется на странице по частям. Сразу после выполнения core.js есть только часть API; остальное приходит позже, и преждевременное обращение даёт undefined или TypeError.

API Когда доступен Как дождаться
SF.Composition, SF.Position, SF.Mask, SF.RegionNavigation (Навигация по областям страницы), SF.Effects (Фоновые эффекты), SF.Preferences (Персональный вид), SF.notify и SF.notifications (Всплывающие сообщения), SF.language, SF.pageLanguage, SF.pageText, SF.deprecations, SF.loadRuntime, SF.runtimeModule; объект правил SF.RuleLoader (пока пустой) Сразу после выполнения core.js Ждать не нужно
SF.Loader, SF.ready, SF.create, SF.createAsync, SF.whenDefined, SF.loadSmartBase, SF.smartBaseReady, SF.whenFieldRenderers, SF.RuleLoader.registerPackage После асинхронной загрузки Loader; приходят все вместе Событие sf-loader-init на window (пример ниже)
SF.Loader.theme (light или dark — тема на странице) и SF.Loader.themePreference (system, light или dark — выбор человека) Вместе с SF.Loader; значения появляются, когда Loader выбрал тему при запуске Событие sf-theme-change: первое приходит при запуске
SF.smart (SfBaseElement, html, nothing и помощники) Вместе с базой Smart-компонентов: с первым Smart-элементом на странице или после await SF.loadSmartBase(). На странице без Smart-элементов его нет await SF.loadSmartBase() после sf-loader-init
Объекты компонентов на классах: SF.Input, SF.Toast, SF.Tabs и другие Только после того, как Loader загрузил компонент по его разметке на странице. Без разметки компонента объекта нет После sf-loader-init: SF.Loader.ready('Inputs', () => SF.Input.setState(…)). Функция вызывается сразу, если компонент уже загружен, иначе после его регистрации, когда объект SF.<Имя> уже назначен. Первый аргумент — имя компонента в реестре (Inputs, Modal); оно может отличаться от имени объекта (SF.Input)
Smart-элемент на странице После загрузки и регистрации его тега await SF.createAsync('datepicker') для нового элемента; для элемента в разметке — await SF.whenDefined('sf-datepicker'), затем await element.whenRendered() (см. Ожидание готовности)

Пример ждёт Loader и базу Smart-компонентов. Он работает в любой момент: до и после sf-loader-init.

JavaScript
function whenLoaderReady() {
  if (window.SF?.loadSmartBase) return Promise.resolve();
  return new Promise((resolve) => {
    window.addEventListener('sf-loader-init', () => resolve(), {once: true});
  });
}

await whenLoaderReady();
await SF.loadSmartBase();   // после этого доступен SF.smart

Что важно заметить:

  • SF.whenDefined() ничего не загружает. Обещание выполнится, только когда тег определён, то есть когда элемент уже стоит в разметке или загружен другим способом. Для элемента, которого на странице ещё нет, берите SF.createAsync(): он создаёт элемент, загружает его модуль и ждёт регистрации тега.
  • Короткое имя в SF.create(), SF.createAsync() и SF.whenDefined() — имя Framework: 'button' означает sf-button. Полное имя элемента пакета (shop-catalog) сохраняется как есть, если его называет правило пакета (см. Пакеты компонентов).
  • Тег пакета можно ждать и до прихода правил пакета. SF.whenDefined('shop-catalog') до того, как правила пакета зарегистрированы, ждёт оба имени — shop-catalog и sf-shop-catalog — и выполняется, когда определено первое из них. Имя без дефиса — всегда имя Framework.
  • Скрипт, который вызывает SF.RuleLoader.registerPackage() сразу после core.js, сначала дожидается sf-loader-init.

Пакеты компонентов

Пакет компонентов — набор Smart-компонентов, который лежит в своём каталоге, несёт свои правила загрузки и подключается тем же Loader, что и остальной Framework. Компоненты пакета не входят ни в поставку ui, ни в её правила загрузки. Так подключается пакет продукта (см. Пакеты компонентов продуктов): продукт объявляет его под своим именем.

Действующий пакет продукта — larena: четыре элемента sf-table, sf-data-view, sf-property-view и sf-admin-menu. Их нет ни в поставке ui, ни в правилах ядра; страница, которой они нужны, объявляет пакет larena (см. Пакеты компонентов продуктов). Готовая сборка пакета larena публикуется выпуском репозитория simai/larena-ui (см. Пакет larena для проекта без Larena). Пример ниже показывает тот же порядок на вымышленном пакете shop.

Пакет соответствует стандарту simai.component-package (см. Пакет компонентов (simai.component-package)) и собирается инструментом sf-build (см. Сборка пакета: sf-build). Объявление пакета и файл правил — точка расширения Loader: пакет добавляет свои компоненты и не меняет файлы и код Framework (см. Принципы, принцип 8). Действующее издание стандарта — 1.1.1: манифест компонента пакета проверяется по схеме 2.5.0, той же, что у компонентов Framework. Издание 1.0.0 закрепляло схему 2.2.0 и осталось в истории; требования не изменились.

Объявление пакета. Страница объявляет, где лежит каждый пакет, в window.SF_BOOT_CONFIG.packages до подключения core.js. Пример подключает Framework и пакет shop из своих копий:

HTML
<link rel="stylesheet" href="/assets/simai-framework/ui/distr/core/css/core.min.css">
<script>
  window.sfPath = '/assets/simai-framework/ui/distr/';
  window.SF_BOOT_CONFIG = {
    packages: {
      shop: {base: '/assets/shop/smart'},
    },
  };
</script>
<script src="/assets/simai-framework/ui/distr/core/js/core.min.js"></script>

Что важно заметить:

  • файл правил пакета на страницу не подключается: Loader сам загружает <base>/rule.js до первого разбора разметки;
  • тег компонента пакета, например <shop-catalog>, пишется так же, как тег любого Smart-компонента; его файлы Loader берёт из каталога пакета;
  • компоненты пакета наследуют SfBaseElement, поэтому их слова следуют языку страницы так же, как у компонентов Framework, и перерисовываются при SF.language.use().

Префикс элементов пакета. Элементы пакета носят префикс владельца (shop-catalog), а не префикс Framework sf- (стандарт пакета, см. Пакет компонентов (simai.component-package)). Loader загружает любой тег, который перечислен в правиле пакета. Тег без префикса sf- может занять только правило пакета, поэтому имена Framework остаются за Framework. Отдельной настройки префикса нет: тег называет само правило. SF.create('shop-catalog') сохраняет полное имя, а короткое имя (SF.create('button')) означает элемент Framework sf-button. Компонент, перенесённый из Framework в пакет, сохраняет своё имя с sf-: так элементы пакета larena называются sf-table, sf-data-view, sf-property-view и sf-admin-menu.

Поля объявления:

Поле Что задаёт
Имя (ключ в packages) Строчные латинские буквы, цифры и дефис; начинается с буквы. Под этим именем хранятся правила пакета
base Каталог с папками компонентов пакета, устроенный как distr/smart/: <base>/<папка>/js/<папка>.js и <base>/<папка>/css/<папка>.css
rule Адрес файла правил. Необязательно; по умолчанию <base>/rule.js

Файл правил. Это обычный скрипт с одним вызовом SF.RuleLoader.registerPackage:

JavaScript
SF.RuleLoader.registerPackage('shop', {
  rules: {
    'cl-catalog': {
      tags: ['shop-catalog'],
      type: 'smart',
      mode: 'smart',
      js: true,
      css: true,
      relation: [{name: 'cl-icons'}, {name: 'cl-pagination'}],
    },
  },
});

Правило пакета записывается так же, как правило Smart-компонента Framework (см. Правила загрузки). Loader отмечает его именем пакета и поэтому берёт файлы компонента из каталога пакета. Зависимости разрешаются каждая из своего места: правило пакета, которое зависит от cl-icons, получает иконки из distr/smart/ поставки Framework.

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

registerPackage можно вызвать и после запуска страницы. Тогда Loader перечитывает страницу, и элемент, который стоял в разметке до прихода правила, загружается.

Когда пакет не загружается. Loader не ищет компонент пакета среди Smart-компонентов Framework. Вместо этого он один раз на правило пишет в консоль предупреждение: какой тег не загружен и почему —

  • правило называет пакет, который страница не объявила;
  • у пакета нет каталога base;
  • файл правил не загрузился или ничего не зарегистрировал.

Отсутствующие файлы. Записи об отсутствующих файлах пакета хранятся с его именем (shop::cl-catalog). Поэтому ответ 404 из каталога пакета не скрывает файл с тем же именем у Framework, и наоборот (см. Кеш и версии адресов).

Язык. Компоненты пакета читают тот же язык страницы, что и компоненты Framework: SF.language.use() меняет язык у тех и других (см. Язык интерфейса).

Справка о пакете. Выпуск Framework может содержать машиночитаемую справку о пакете distr/core/contracts/packages/<имя>.package.json. Она называет элементы пакета, их правила и папки и то, что каждому нужно от Framework. Файлов компонентов справка не содержит. Квитанция пакета сверяет с ней правила пакета: если справка есть и не совпадает, квитанция не выписывается (см. Пакеты компонентов продуктов).

Правила:

  • обязательно: объявлять пакет до core.js, в SF_BOOT_CONFIG.packages;
  • обязательно: подключать сборку пакета, квитанция которой называет выпуск Framework на странице (см. Версии: как закрепить выпуск);
  • рекомендуется: отдавать каждую сборку пакета по своему пути с коммитом исходников пакета, например /assets/larena/<коммит пакета>/smart; так путь записан во всех примерах документации. Файлы пакета получают ту же метку версии, что и файлы ядра (см. Кеш и версии адресов), поэтому новый путь гарантирует новые адреса;
  • нельзя: класть файлы пакета в distr/smart/ или ждать, что Loader найдёт их там.

Поиск по документации

Введите минимум 2 символа

    Настройки

    Тема
    Размер шрифта
    Ширина страницы