Перейти к содержимому

Vue-приложение на странице - запуск, компоненты, данные

Собираем реактивный интерфейс на штатном Vue платформы: запускаем приложение, объявляем компоненты, забираем данные с сервера и связываем всё с Битриксом.

Механика

Платформа поставляет собственную обёртку над Vue третьей версии. Это тонкий слой, не меняющий поведение самого Vue, но решающий конфликты версий между модулями.

Сам Vue намеренно не лежит в глобальной области видимости страницы. Расширение отдаёт его импортом, и два модуля с разными задачами больше не спорят за одну глобальную переменную.

Запуск приложения отличается от обычного Vue ровно одним вызовом. Приложение создают методом обёртки, а дальше всё привычно: тот же результат с монтированием в элемент страницы.

Работать можно и без сборки проекта. Расширение подключают из PHP, а приложение создают через глобальное пространство имён прямо на странице сайта.

Выбор между двумя способами определяется размером задачи. Небольшой блок на одной странице живёт и без сборки, а приложение из десятка компонентов без транспиляции превращается в набор строковых шаблонов, который тяжело сопровождать.

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

Кастомизация чужого компонента идёт мутацией или клонированием оригинала. Правка исходников здесь так же неуместна, как правка файлов ядра: обновление вернёт всё назад.

Мост к платформе - это отдельный служебный объект в каждом компоненте. Через него доступны локализация, события приложения, общие данные и клиенты обмена с сервером.

События в приложении делятся на три разных уровня. Обычные события Vue поднимаются на уровень выше, события приложения ходят между его частями, события сайта связывают разные приложения и интерфейсы платформы.

Локализация интерфейса идёт через тот же самый объект-мост. Прямой вызов фразы в шаблоне на каждой перерисовке стоит времени, поэтому большие наборы фраз забирают один раз в вычисляемое свойство.

У штатной обёртки есть заметные ограничения по возможностям. Отрисовки на сервере нет, однофайловые компоненты не поддерживаются, а само расширение требует свежей версии модуля интерфейсов.

Шаги

  1. Убедиться, что установленная версия модуля интерфейсов поддерживает нужное расширение платформы.
  2. Завести своё расширение и положить в него все компоненты будущего приложения.
  3. Создать приложение и смонтировать его в уже готовый элемент на странице.
  4. Пробросить в приложение общий контекст: адреса действий, права и начальные данные.
  5. Забирать данные с сервера действиями контроллера, а не отдельными файлами в шаблоне.
  6. Отписываться от событий приложения при разрушении каждого компонента.

Код

Запускаем приложение со сборкой:

import { BitrixVue } from 'ui.vue3';
BitrixVue.createApp({
data() { return { counter: 0 }; },
mounted() { setInterval(() => this.counter++, 1000); },
template: `Счётчик: {{ counter }}`,
}).mount('#application');
// отличие от обычного Vue - только способ создания приложения
// шаблон здесь строкой: со сборкой его выносят в отдельный файл

Метод создания приложения приходит из расширения платформы, а не из глобального Vue. Результат ведёт себя как обычное приложение Vue, и весь остальной код пишется без оглядки на платформу.

Обходимся без сборки:

<?php \Bitrix\Main\UI\Extension::load('ui.vue3'); ?>
<div id="application"></div>
<script>
BX.Vue3.BitrixVue.createApp({
data() { return { counter: 0 }; },
template: `Счётчик: {{ counter }}`,
}).mount('#application'); // монтируют только после готовности страницы
</script>

Вариант без сборки удобен для небольшого блока на готовой странице. Как только компонентов становится больше двух, его меняют на расширение с транспиляцией: шаблоны в строках быстро становятся неподдерживаемыми.

Пробрасываем контекст приложения:

export class Widget {
constructor(context) { this.context = context; }
run(node) {
const context = this.context;
BitrixVue.createApp({
beforeCreate() { this.$Bitrix.Application.set(context); }, // адреса, права
template: `<my-list />`,
}).mount(node);
}
}

Контекст ставят до создания компонентов, а читают в любом хуке. Так адреса действий, признаки прав и начальные данные приходят в компоненты сверху, а не собираются каждым из них самостоятельно.

Забираем данные с сервера:

BX.ajax.runAction('local:catalog.list.get', { data: { sectionId: 12 } })
.then((response) => { this.items = response.data.items; })
.catch((response) => { this.errors = response.errors; });
// данные отдаёт действие контроллера: подпись запроса и права уже проверены

Данные забирают действиями контроллера, а не отдельными файлами в каталоге шаблона. Контроллер проверяет подпись запроса и права, а ответ приходит в едином формате, который не нужно разбирать руками.

Локализуем интерфейс:

computed: {
phrases() { return this.$Bitrix.Loc.getFilteredPhrases('MY_WIDGET_'); },
},
// прямой вызов фразы в шаблоне пересчитывается на каждой перерисовке

Набор нужных фраз забирают один раз в вычисляемое свойство компонента. На тысяче фраз прямые вызовы в шаблоне складываются в заметную задержку при каждой перерисовке списка.

Подписываемся на события и отписываемся:

created() {
this.$Bitrix.eventEmitter.subscribe('local:list:changed', this.onChanged);
},
beforeUnmount() {
this.$Bitrix.eventEmitter.unsubscribe('local:list:changed', this.onChanged);
},
// без отписки обработчики накапливаются и срабатывают по нескольку раз

Подписку делают при создании компонента, отписку - перед разрушением. Забытая отписка проявляется не сразу: сначала обработчик срабатывает дважды, потом десятки раз, а причина ищется долго.

Бережёмся лишней реактивности на больших списках:

data() {
return {
statuses: Object.freeze(window.pageStatuses), // константы не реактивны
// реактивным делают только то, что действительно меняется на экране
items: [],
};
},
// в списках всегда указывают ключ элемента, иначе перерисовка идёт целиком

Замороженные данные Vue не отслеживает, и на длинных справочниках это экономит заметную часть времени отрисовки. Ключ элемента списка решает вторую половину той же задачи: без него перерисовывается весь список целиком.

Ограничения

Отрисовки готовой страницы на стороне сервера у этой обёртки нет вовсе. Поисковые системы получат на месте приложения пустой блок, поэтому важный для поиска текст выводят обычным шаблоном компонента, а не реактивным приложением.

Однофайловые компоненты обёрткой не поддерживаются. Шаблон, стили и код живут раздельно по файлам, и привычный по обычным проектам формат придётся оставить за порогом этого проекта.

Расширение требует достаточно свежей версии модуля интерфейсов платформы. На старом продукте задачу решают либо обновлением, либо своей сборкой, подключённой как обычное расширение.

Режим правки страницы и реактивные приложения уживаются друг с другом довольно плохо. Блок, собранный скриптом, редактору содержимого недоступен, и правки в нём делает разработчик.

Типичные проблемы

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

Нужное расширение платформы не подключено на этой странице. Без подключения глобальное пространство имён с обёрткой Vue на странице не появляется.

Монтирование срабатывает не всегда.

Приложение монтируется до готовности страницы. Элемент страницы, в который идёт монтирование, к этому моменту ещё не существует.

Обработчик события срабатывает по нескольку раз.

Компонент подписывается на событие при создании и не отписывается при своём разрушении. Подписки накапливаются заново при каждой перерисовке этого списка.

Правки чужого компонента исчезли после обновления.

Компонент правили прямо в его исходниках вместо мутации или клонирования оригинала. Очередное обновление продукта возвращает эти файлы к их исходному виду.

Интерфейс заметно тормозит на длинных списках.

Фразы локализации вызываются прямо в шаблоне, а нереактивные данные никак не заморожены. И то, и другое пересчитывается заново при каждой перерисовке длинного списка.

Частые вопросы

Можно ли взять обычный Vue вместо штатного?

Можно, собрав его своим бандлом и подключив как расширение. Штатный вариант выигрывает тем, что решает конфликты версий между модулями и даёт мост к платформе.

Где хранить компоненты приложения?

В своём расширении, в отдельной папке компонентов. Расширение подключается на страницу как обычно и приносит с собой зависимости.

Как передать в приложение начальные данные?

Контекстом приложения, который ставят до создания компонентов. Складывать данные в глобальные переменные страницы не стоит: их видит и меняет кто угодно.

Подходит ли это для витрины магазина?

Для интерактивных блоков - да, для содержимого, которое должно попасть в поиск, - нет. Отрисовки на сервере нет, и поисковый робот увидит пустой блок.

Чем мутабельный компонент отличается от обычного?

Его можно менять и клонировать без правки исходников: это нужно решениям, которые дорабатывают сторонние разработчики. Для внутренних задач достаточно обычного компонента.

Смежное

Первоисточник