Перейти к основному содержимому
Версия: 7.0

How-to: Пользовательские клиентские модули JS

Когда приложению нужен собственный браузерный JavaScript — пользовательское представление объекта или свойства, функция, привязанная действием INTERNAL CLIENT, или любой другой клиентский код — этот код можно хранить как обычные исходные файлы в проекте и собирать в веб-клиент на этапе сборки. Платформа собирает каждый основной файл и регистрирует то, что он экспортирует, поэтому приложению не нужно писать явную загрузку или привязку для него. Это относится только к веб-клиенту.

Где размещаются файлы

Браузерный JavaScript размещается в папке src/main/web модуля логики, например src/main/web/OrderBoard.jsx или src/main/web/util.js. Файлы могут иметь расширение .js, .jsx, .ts или .tsx.

Во время сборки каждый файл в src/main/web (кроме вложенной папки lib) собирается (при помощи esbuild) в один файл web/.compiled/<имя>.js в classpath, где <имя> берётся из пути к исходному файлу (файл во вложенной папке становится <папка>_<имя>.js). Собранный файл загружается автоматически при открытии страницы — запись в действии onWebClientInit для него не нужна.

Вложенная папка src/main/web/lib считается общим вспомогательным кодом: её файлы не собираются в отдельные бандлы, но их можно импортировать из основных файлов, и они включаются в их сборку.

Без сборки

Проект без настроенной сборки — без node, без esbuild, без зависимостей org.mvnpm — всё равно может подключить пользовательский клиентский JS обычным файлом в папке src/main/resources/web, который используется как есть: без сборки, без JSX, без разрешения сторонних библиотек. (Этот же путь использует eval.) Поэтому представление React пишется через React.createElement с использованием предоставляемого платформой window.React, а не через JSX, и компонент задаётся на глобальном объекте window (запасной вариант, описанный ниже), а не как именованный экспорт. Имя custom, совпадающее с [A-Z][A-Za-z0-9_$]*, по-прежнему распознаётся как React:

function HelloBoard(props) {
var React = window.React;
var rows = (props.data.o || {}).list || [];
return React.createElement("div", { className: "hello-board" }, rows.length + " orders");
}

Такой файл загружается одним из двух способов — то же разделение, что отличает автоматически загружаемые сборки со сборочного пути от всего, что приложение перечисляет в onWebClientInit.

Автоматически — поместите его в resources/web/init. Файл в папке src/main/resources/web/init регистрируется автоматически при открытии страницы, без записи в onWebClientInit — это аналог без сборки того, как сборки из src/main/web загружаются автоматически после сборки. Подходит для самодостаточного компонента или таблицы стилей:

DESIGN orders {
BOX(o) { custom = 'HelloBoard'; } // из resources/web/init/helloBoard.js — больше ничего подключать не нужно
}

Файлы в web/init должны быть независимыми от порядка загрузки: сканирование даёт им всем один порядок загрузки, поэтому каждый должен регистрировать или определять что-либо при загрузке и обращаться к другой библиотеке отложенно (при отрисовке или по событию), а не обращаться к другому скрипту в момент загрузки. Вложенная папка web/init/lib исключается из автозагрузки (как src/main/web/lib из сборки) — для вспомогательных или включённых в проект файлов, на которые ссылаются явно, а не внедряют автоматически.

Явно — перечислите его в onWebClientInit. Любой другой файл в src/main/resources/web — вне web/init или внутри исключённой web/init/lib — загружается указанием его имени в действии onWebClientInit с целочисленным порядком. Используйте это, когда важен порядок загрузки — сторонняя библиотека должна загрузиться раньше использующего её компонента — или для условной загрузки файла:

onWebClientInit() + {
onWebClientInit('helloBoard.js') <- 1;
}

Сборочный путь и пути без сборки соотносятся так:

Со сборкой (src/main/web)Без сборки, авто (resources/web/init)Без сборки, явно (resources/web)
Загрузкасобирается в web/.compiled, загружается автоматическиавтозагрузка по сканированию папкиуказывается в onWebClientInit
Исходный код.js/.jsx/.ts/.tsx, допустим JSXобычный .jsобычный .js
Регистрацияименованный экспортимя на windowимя на window
Порядок загрузкиодин порядок (сборки самодостаточны)один порядок (файлы должны быть независимы от порядка)явный целочисленный порядок
Сторонние библиотекивключаются в сборку через org.mvnpmзагружаются отдельно, берутся из windowзагружаются отдельно, берутся из window

Именованные экспорты и автоматическая регистрация

Каждый модуль предоставляет свои компоненты и функции как именованные экспорты. При загрузке каждый именованный экспорт регистрируется в реестре window.lsfusion.custom под своим именем, и клиент сначала ищет пользовательское имя в этом реестре. Поэтому custom = 'OrderBoard' в DESIGN, представление объекта CUSTOM 'orderBoard' или действие INTERNAL CLIENT 'formatSum' находят экспорт с совпадающим именем:

export function OrderBoard(element, controller, list) {
// отрисовка списка объектов внутри element
}

Имя, заданное прямо на глобальном объекте window, по-прежнему работает как запасной вариант, поэтому существующие скрипты, объявляющие window.OrderBoard = ..., продолжают работать, но именованные экспорты предпочтительнее.

React и ReactDOM предоставляются платформой: единственная встроенная производственная сборка загружается до любого пользовательского скрипта, и импорты react, react-dom и react-dom/client в модуле разрешаются в неё. Приложение не должно включать в сборку свою копию React или ReactDOM.

Подключение сторонней библиотеки

Сторонняя браузерная библиотека подключается как обычная офлайн-зависимость Maven через координаты mvnpm (org.mvnpm:*, зеркало npm в Maven Central) — без шага npm или yarn. Зависимость разрешается из локального репозитория Maven, как и любая другая, и библиотека вместе с npm-пакетами, от которых она зависит, включается в сборку тех модулей, которые её импортируют:

<dependency>
<groupId>org.mvnpm</groupId>
<artifactId>apexcharts</artifactId>
<version>3.54.1</version>
</dependency>

Пакет npm с областью видимости (@scope/name) использует группу org.mvnpm.at.<scope> и простое имя как artifact id. Затем модуль импортирует библиотеку по её имени в npm:

import ApexCharts from "apexcharts";

Подключение сторонней библиотеки без сборки

Без сборки нет упаковки через org.mvnpm, поэтому сторонняя библиотека загружается отдельным браузерным скриптом и используется по тому глобальному имени, которое она задаёт, — сборкой, присваивающей себя в window (сборка UMD или обычный скрипт). Компонент берёт её оттуда (window.confetti, window.dayjs, …), а не импортирует. Подать этот скрипт можно двумя способами.

С библиотекой, включённой в проект, — для офлайн-проекта. Поместите браузерную сборку библиотеки (тот .js, который задаёт глобальное имя) статическим файлом в src/main/resources/web/init рядом с компонентом. Оба файла загрузятся автоматически, и компонент работает, потому что обращается к глобальному имени отложенно, при отрисовке или по событию, — поэтому неважно, какой из двух скриптов сканирование внедрит первым. Нужны только закоммиченные файлы — без интернета, без зависимости org.mvnpm, без сборки:

function ConfettiBoard(props) {                        // resources/web/init/confettiBoard.js
var React = window.React;
function celebrate() { window.confetti({ particleCount: 200, spread: 120 }); }
return React.createElement("button", { onClick: celebrate }, "Celebrate");
}
// resources/web/init/confetti.umd.js задаёт window.confetti — оба файла загружаются автоматически, без onWebClientInit

По URL, когда во время выполнения доступен интернет. URL — это не файл, который можно положить в web/init, поэтому он передаётся прямо в onWebClientInit: значение, которое не является локальным ресурсом web/ и при этом является абсолютным URL, загружается как <script src> на странице (правило разрешения описано в INTERNAL), перед использующим её компонентом:

onWebClientInit() + {
onWebClientInit('https://cdn.jsdelivr.net/npm/canvas-confetti@1.9.3/dist/confetti.browser.min.js') <- 1;
onWebClientInit('confettiBoard.js') <- 2;
}

React Compiler

По умолчанию исходный код собирается как есть. Необязательный проход React Compiler (автоматическая мемоизация компонентов React) включается установкой опции модуля/плагина reactCompiler = true; он пропускает каждый исходный файл через компилятор перед сборкой. В отличие от обычной сборки, этому проходу нужен node на сборочной машине. Используйте его, когда пользовательским представлениям React полезна автоматическая мемоизация; иначе оставьте его выключенным (значение по умолчанию), и обычная сборка по-прежнему работает.

Пример

Поместим небольшой модуль в src/main/web/orderUtil.js, экспортирующий функцию форматирования:

export function formatSum(amount, currency) {
return new Intl.NumberFormat("en-US", { style: "currency", currency }).format(amount);
}

Привяжем его к клиентскому действию и вызовем:

formatOrderSum 'Format' (Order o) {
INTERNAL CLIENT 'formatSum' (sum(o), 'USD');
}

Сборка компилирует orderUtil.js в web/.compiled/orderUtil.js и регистрирует formatSum; при открытии формы действие находит имя в реестре и вызывает функцию. Запись в onWebClientInit для модуля не пишется.

Стили

Модуль может импортировать CSS. esbuild собирает весь CSS, достижимый из модуля — и собственный import "./styles.css", и CSS любой подключённой сторонней библиотеки — в соседний файл web/.compiled/<имя>.css, который загружается автоматически вместе с бандлом (без записи в onWebClientInit и без отдельной регистрации CSS библиотеки). Шрифты и изображения, на которые этот CSS ссылается через url(), встраиваются в скомпилированную таблицу стилей как data-URL, поэтому она самодостаточна; крупные изображения загружайте отдельно (например, через onWebClientInit), чтобы таблица стилей не разрасталась.

Рекомендуемый способ оформления:

  • CSS-модули (import styles from "./Component.module.css", применяется как className={styles.root}) для собственных стилей компонента — имена классов локализуются по модулю, поэтому стили разных компонент на одной форме не конфликтуют.
  • Inline-стили style={{ ... }} для значений, вычисляемых из данных (цвета и размеры по строке, условное оформление).
  • Обычный import "./Component.css" (или CSS сторонней библиотеки) — глобальный; используйте его для библиотечных или намеренно глобальных стилей и задавайте именам классов префикс. Скомпилированный .css не нужно регистрировать вручную — он уже загружается автоматически.

Для полноценной системы стилей помимо статических классов подходит CSS-in-JS времени выполнения (например, styled-components или @emotion): такая библиотека подключается обычной зависимостью org.mvnpm, собирается вместе с модулем и добавляет свои стили во время выполнения. Используйте API styled или className={css(...)}; prop css у Emotion (<div css={...} />) требует JSX-преобразования, которого в сборке нет, поэтому он недоступен.

Препроцессоры CSS (Sass/SCSS, Less, Stylus) и утилитарные фреймворки, генерирующие CSS на этапе сборки (Tailwind, UnoCSS), не входят в эту сборку — она запускает только бинарник esbuild, без шага Node или плагинов. Нативный CSS (вложенность, пользовательские свойства) и CSS-модули покрывают большую часть того, ради чего применялись препроцессоры; если такой инструмент всё же нужен, сгенерируйте CSS отдельным шагом и поставьте результат обычной таблицей стилей через onWebClientInit.

Отдельную таблицу стилей, не входящую в сборку, по-прежнему можно поставлять обычным файлом и загружать через действие onWebClientInit, как CSS классического пользовательского компонента (см. How-to: Пользовательские компоненты (объекты)).

Эта статья описывает общую упаковку любого пользовательского браузерного JavaScript. О представлениях, специфичных для React, и о вызовах сервера, которые пользовательское представление делает через контроллер, см. How-to: Пользовательские представления формы на React и How-to: API контроллера пользовательского представления.