В данной статье будет показана нестандартная реализация компонентов Vue + Bitrix, которая не была показана в этом видео, и в других подобных.

Спасибо большое разработчикам за интеграцию, но как многие уже отметили:

"Данная интеграция находится лишь в зачаточном виде"

В целом да, я согласен, без Bitrix CLI будет трудно написать полноценный SPA, да и тем более кому это надо? Это же будет сущий ад для любого Frontend разработчика. Проще будет использовать Bitrix как API, но не суть.

Я нашел способ нормально писать без костылей SFC Vue компоненты. Возможно кто то уже так делал. Но информации и статей по этой теме я не нашел (возможно я первый кто так попробовал).

Обойдемся без CDN, будем использовать лишь интегрированный Vue, как минимум потому что:

  1. Он присутствует в ядре в качестве extension, и будет поддерживаться разработчиками.

  2. Для наших целей идеально подойдет.

  3. Дополнительно можем подключить Vuex/Pinia, но тут этого не будет.

Если соберу много положительного фидбэка, то сделаю более подробную реализацию на данную тему.

Для начала подключим Vue из коробки.

<?Bitrix\Main\UI\Extension::load("ui.vue3");

Сразу хотелось бы оговорится для тех кто не в курсе, Composition API появился только в 3 версии Vue. По этому используем Vue3.

Vue 3 из коробки появился примерно когда Bitrix перешел на 8 версию PHP (могу ошибаться), по этому не забываем обновляться!

После того как мы подключили Vue - далее начинаем разрабатывать наш компонент.

P.S.

Данный способ подходит и для второй версии Vue. Только вместо Composition придется использовать старое API. Опять же могу написать еще одну статью по использованию старого API если соберу положительный фидбэк.


SCRIPT.JS

/** This function is called when the DOM is ready. */
BX.ready(() => {
    /**
     * Returns an element by its ID
     * @param {string} id - The ID of the element to be returned
     * @returns {HTMLElement} The element with the specified ID, or null if no element with the specified ID exists
     */
    const node = BX('app')
    const application = BX.Vue3.createApp({
      /**
       * This function is a lifecycle hook that is called when the component is first created.
       * It sets up the component's state by returning an object that contains the component's data.
       * @returns {object} An object containing the component's data
       */
      setup() {
        /**
         * Returns a reactive reference to a value.
         * @param {any} value - The value to be referenced
         * @returns {import('vue').Ref<any>} A reactive reference to the value
         */
        const counter = BX.Vue3.ref(0);

        setInterval(() => {
          counter.value++;
        }, 300);

        return {
          counter,
        };
      },
    });
    /**
     * Mounts the Vue 3 application instance to the DOM element.
     * @param {HTMLElement|String} node - The DOM element to mount Vue 3 application instance
     */
    application.mount(node);
})

Те кто видел статьи или видео по данной теме или пробовали разрабатывать на Vue знают что в функцию createApp должен передаваться объект с функциями и нашей версткой, которую уже потом рендерит JS, и делает её реактивной. Писать верстку и логику Vue в строке не очень приятно и удобно, по этому мы не будем передавать свойство template с версткой и логикой в объект . Наш код будет находиться в файле template.php


TEMPLATE.PHP

<div id="app">
    {{ counter }}
</div>

Да все верно, мы пишем в template.php разметку как будто мы работаем во Vue файле. Ну и соответственно в style.css пишем стили для нашего компонента которые так же подтянутся.

Уже получилось довольно красиво:

  1. Вывели верстку и логику Vue в отдельный файл.

  2. JS функции и объекты с переменными храним в JS.

  3. Стили храним в style.css.

И казалось бы вот проблема с нормальной разработкой на Vue в Bitrix решена. Пускай хоть и не полностью, но решена.

А что если я скажу и даже покажу что можно еще красивее.


Еще красивее...

100% все заметили что мы обращаемся через объект BX.Vue3 и выглядит это слишком громоздко. И хотелось бы как это сократить.

Ну что-ж давайте сокращать.

Если на проекте используется Vue везде, значит подключим его глобально. Условно в header.php

<?Bitrix\Main\UI\Extension::load("ui.vue3");

Теперь наш объект BX.Vue3 есть везде, следовательно мы можем подключить другой скрипт глобально. И сделаем следующие операции в данном скрипте.

for (const key in BX.Vue3) window[key] = BX.Vue3[key]

Да мы просто вынесем функции из BX.Vue3 в window, и нам станет доступен "нативный" синтаксис.

А теперь мы можем переписать свой script.js

BX.ready(() => {
    const node = BX('app')
    const application = createApp({
      setup() {
        const counter = ref(0);

        setInterval(() => {
          counter.value++;
        }, 300);

        return {
          counter,
        };
      },
    });
    application.mount(node);
})

Ну вот, это уже лучше, но все же это плохое решение. Ведь мы засоряем объект глобальный объект window. Но как решение проблемы - сойдет. После чего можно еще по желанию удалить из window BX.Vue3, так как все функции мы уже вынесли.

Но многие спросят. А что если у меня Vue используется не везде, и я не хочу засорять window, но хочу использовать функции Vue без BX.Vue3?

Так вот (кто-то уже догадался) и тут выход тоже есть, и причем весьма простой.

Мы используем деструктуризацию для этого.

const { ref, createApp } = BX.Vue3

И у нас все так же будет замечательно работать, только теперь данные функции у нас не в объекте window

И да этот способ тоже подойдет для глобального скрипта, и ваши функции будут константами в объекте window.

Вот полный код скрипта как это будет выглядеть внутри компонента

BX.ready(() => {
    const { ref, createApp } = BX.Vue3
    const node = BX('app')
    const application = createApp({
      setup() {
        const counter = ref(0);

        setInterval(() => {
          counter.value++;
        }, 300);

        return {
          counter,
        };
      },
    });
    application.mount(node);
})

Окей, разобрались с компонентом. Давайте перейдем к вещам посложнее. Связка front + back.

Первый вопрос у многих а как нам получить данные с бэка которые у нас были получены в component.php.

Есть вариант вот такой вот. Не самый красивый, но работать будет.

Нам нужно в template.php добавить тег script и поместить туда следующий код.

const arResult = <?=Bitrix\Main\Web\Json::encode($arResult)?>

или же вот так, кому как удобнее, мне лично нравится первый способ.

const arResult = <?=json_encode($arResult, true)?>

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

<?CUtil::PhpToJSObject

Так же есть вариант использовать BX.message, но как по мне это не очень безопасно что ли...


А теперь самый крутой вариант. Буквально NEXT GEN

Раз мы все равно написали свой компонент. т.к. как нативных компонентов Vue - нет. Предлагаю переходить от component.php, к class.php

Чтобы прям совсем круто было.

Создаем с нуля наш компонент и пишем его на class.php, и делаем +- что то посложнее чем реактивные таймеры.

TEMPLATE.PHP

<ul id="users-list" class="users-list">
    <li v-for="(user, key) in arResult.users" class="users-list__item" :key="key">
        {{ user }}
        <button type="button" @click="deleteUser(user.id)">Удалить пользователя</button>
    </li>
    <form @submit="addUser" class="form">
        <div class="form__field">
            <label>Имя пользователя</label>
            <input type="text" name="name">
        </div>
        <button type="submit">Добавить нового пользователя</button>
    </form>
</ul>

Создаем наш темплейт, с логикой Vue, в данном примере будет добавление пользователя и удаление пользователя.

Немного добавим стилей в style.css, чтобы все убедились что стили работают.

.users-list {
    padding: 12px;
}
.users-list__item {
    padding: 12px;
    background-color: #e2e2e2;
    border-radius: 4px;
    margin-bottom: 8px;
}

Далее переходим к самому интересному. Написание логики в class.php и scirpt.js

CLASS.PHP

<?php
if (!defined('B_PROLOG_INCLUDED') || B_PROLOG_INCLUDED !== true)
    die();

use Bitrix\Main\UI\Extension,
    Bitrix\Main\Engine\Contract\Controllerable;

Extension::load("ui.vue3");
class UsersList extends CBitrixComponent implements Controllerable
{
    protected static array $users = [
        [
            'id' => 1,
            'name' => 'Alexander',
        ],
        [
            'id' => 2,
            'name' => 'Ivan',
        ],
        [
            'id' => 3,
            'name' => 'Sergey',
        ],
        [
            'id' => 4,
            'name' => 'Smith',
        ],
        [
            'id' => 5,
            'name' => 'John'
        ],
    ];
    public function configureActions(): array
    {
        return [];
    }

    public function deleteUserAction($id): array
    {
        foreach (self::$users as $key => $user) {
            if ($user['id'] == $id) {
                unset(self::$users[$key]);
                break;
            }
        }
        return self::$users;
    }

    public function getUsersAction(): array
    {
        return self::$users;
    }

    public function addUserAction(): array
    {
        $lastUser = end(self::$users);
        $newUser = [
            'id' => $lastUser['id'] + 1,
            'name' => $_POST['name'],
        ];
        self::$users[] = $newUser;
        return self::$users;
    }

    public function executeComponent(): void
    {
        $this->includeComponentTemplate();
    }
}

Тут все просто подключаем наш Vue, далее создаем класс и наследуемся от стандартного CBitrixComponent и реализуем интерфейс чтобы наш класс был контролируемым и мы могли работать с ним через BX.ajax.runComponentAction, чтобы не выходить за пределы компонента.

SCRIPT.JS

const { runComponentAction } = BX.ajax;
const { ref, createApp, reactive } = BX.Vue3;
BX.ready(() => {
  createApp({
    setup() {
      function deleteUser(id) {
        const request = runComponentAction("test:test", "deleteUser", {
          mode: "class",
          data: { id: id },
        });
        request.then((reponse) => arResult.users = reponse.data);
      }

      function addUser(e) {
        e.preventDefault();
        const request = runComponentAction("test:test", "addUser", {
          mode: "class",
          data: new FormData(e.target),
        });
        request.then((reponse) => arResult.users = reponse.data);
      };

      const arResult = reactive({});
      const request = runComponentAction("test:test", "getUsers", {
        mode: "class",
      });
      request.then((reponse) => (arResult.users = reponse.data));

      return {
        arResult,
        addUser,
        deleteUser
      };
    },
  }).mount("#users-list");
});

Далее тут все просто, берем создаем наше Vue приложение, создаем нужные нам объекты, переменные, функции и работаем с ними.

В самом начале создаем реактивный объект arResult в который будем помещать наши другие массивы, или объекты, данный объект будет отслеживать изменения и менять страницу в соответствии с нашей логикой в template.php.

Вот что должно было у нас получится на странице.
Вот что должно было у нас получится на странице.

Соответственно нажимая на кнопку удалить пользователя, он удалится у нас на странице, если добавить, то пользователь добавиться. Только из за того что мы удаляем из статичного массива, при добавлении после удалении пользователя, мы все равно получим пользователя которого удалили (ну как бы из кода понятно).

Примеров можно придумать уйму как с этим работать, в любом случае стоит отметить что это довольно удобно и быстро.

Так же для удобства можно подключить VueDev Tools они так же довольно удобно подключаются из коробки.

Выводы

SFC компоненты очень удобно разрабатывать, до SPA вряд ли такими темпами дойдем, нужно ждать более лучшую и удобную интеграцию, либо использовать Bitrix как API и не парится, но если такого варианта нет. То данный способ очень не так и плох.

Преимущества:

  1. Не нужно настраивать package.json, устанавливать node и node_modules в Bitrix, все реализовано "нативными" средствами.

  2. Не нужно знать Bitrix CLI для того чтобы создавать свой extension в котором будет Vue приложение.

  3. Верстка template размещается не в JS, её рендерит сервер, а значит если у вас есть какой то текст то страница в теории не будет помечена ботом как пустая. Хотя в любом случае поисковые системы гугла и яндекса умеют работать со скриптами уже.

  4. Есть возможность использовать Composition API, которая является более производительной чем предыдущая API + кода стало меньше, и он стал более читаемый

Недостатки:

  1. Вряд ли получится создать отдельно другой маленький компонент и использовать его.

  2. Не получится сделать адекватное SPA приложение. (Ну вернее можно, но не захочешь)

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

Если статья была вам полезна, оставляйте свой фидбэк. Если есть какие то вопросы пишите я постараюсь ответить.

Комментарии (2)