Интересные задачи верстки и клиентского программирования

Новый ajax в 1С-Битрикс

добавил Шубин Александр 22 Сентябрь, 2018


С версии битрикса 17.5.10 добавился новый функционал по работе с аяксом. Теперь можно вызвать аяксом произвольный метод модуля или произвольный метод компонента без необходимости выполнения шаблона сайта. То есть по запросу выполнится только немного служебного кода и собственно сам метод.

На клиенте все довольно просто. Мы должны отправить обычный аякс запрос по адресу /bitrix/services/main/ajax.php

В js библиотеке битрикса есть методы которые самостоятельно формируют запрос, запись получается слегка короче, чем при написании запроса другими способами. Я покажу примеры запросов на битриксовой библиотеке и на jquery

На конференции битрикса в марте объяснялись нововведения в общих чертах, кому для понимания удобнее смотреть видео, вот:

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

Запрос к компоненту

Для начала рассмотрим как происходит вызов методов компонента.

Клиентская сторона

Вот пример запроса на битриксовой библиотеке:

var request = BX.ajax.runComponentAction('custom:ajax', 'test', {
    mode:'class',
    data: {
        param1: 'asd'
    }
});

request.then(function(response){
    console.log(response);
});

Приведу код и jsdoc битриксовой функции. Из jsdoc можно узнать параметры, которые эта функция ожидает

/**
 *
 * @param {string} component
 * @param {string} action
 * @param {Object} config
 * @param {?string} [config.analyticsLabel]
 * @param {?string} [config.signedParameters]
 * @param {string} [config.method='POST']
 * @param {string} [config.mode='ajax'] Ajax or class.
 * @param {Object} [config.data]
 * @param {?array} [config.headers]
 * @param {?number} [config.timeout]
 * @param {Object} [config.navigation]
 */
BX.ajax.runComponentAction = function (component, action, config)
{
	config = prepareAjaxConfig(config);
	config.mode = config.mode || 'ajax';

	var getParameters = prepareAjaxGetParameters(config);
	getParameters.c = component;
	getParameters.action = action;

	var url = BX.util.add_url_param('/bitrix/services/main/ajax.php', getParameters);

	return buildAjaxPromiseToRestoreCsrf({
		method: config.method,
		dataType: 'json',
		url: url,
		data: config.data,
		timeout: config.timeout,
		preparePost: config.preparePost,
		headers: config.headers
	});
};

Вот примерно то же самое на jquery:

var query = {
    c: 'custom:ajax',
    action: 'test',
    mode: 'class'
};

var data = {
    param1: 'eee',
    SITE_ID: 's1',
    //sessid: BX.message('bitrix_sessid')
};

var request = $.ajax({
    url: '/bitrix/services/main/ajax.php?' + $.param(query, true),
    method: 'POST',
    data: data
});

request.done(function (response) {
    console.log(response);
});

Немного сложностей вносит использование csrf. Если кратко, то это защита от вызова метода на сторонних сайтах. Если вы хотите защитить запрос, то вам придется воспользоваться методом BX.message(‘bitrix_sessid’) в js (он закомментирован в объекте data). Либо каким то другим образом передать с бэкенда на фронт данные функции <?echo bitrix_sessid();?> и подставить эту строку в sessid.

Если же вы никакие сколько нибудь значимые данные в запросах не передаете, то проверку csrf можно будет отключить на бэкенде (для каждого аякс метода независимо, для каких-то можно и оставить) и отправлять запросы без sessid.

Серверная сторона:

На серверной стороне генерировать ответ будет обычный компонент. Так как в примере выше мы отправляли запросы к компоненту custom:ajax, то фактически будет выполняться метод testAction в классе из файла /local/components/custom/ajax/class.php, название класса в файле как обычно не важно. Вот примерно такое там может быть:

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

use Bitrix\Main\Engine\Contract\Controllerable;
use Bitrix\Main\Engine\ActionFilter;

class CCustomAjax extends CBitrixComponent implements Controllerable
{
	/**
	 * @return array
	 */
	public function configureActions()
	{
		return [
			'test' => [
				'prefilters' => [
					new ActionFilter\Authentication(),
					new ActionFilter\HttpMethod(
						array(ActionFilter\HttpMethod::METHOD_GET, ActionFilter\HttpMethod::METHOD_POST)
					),
					new ActionFilter\Csrf(),
				],
				'postfilters' => []
			]
		];
	}

	function executeComponent()
	{
	}


	/**
	 * @param string $param2
	 * @param string $param1
	 * @return array
	 */
	public function testAction($param2 = 'qwe', $param1 = '')
	{
		return [
			'asd' => $param1,
			'count' => 200
		];
	}

}

Обращаю ваше внимание, на клиенте мы спрашиваем просто test, а по факту выполняется метод testAction.

Метод configureActions описывает пре- и постфильтры для запроса. В примере выше, чтобы запрос выполнился пользователь должен быть авторизован (префильтр ActionFilter\Authentication()), запрос должен идти методом GET или POST (префильтр ActionFilter\HttpMethod) и если это POST запрос, то в запросе должна быть переменная sessid (префильтр ActionFilter\Csrf). Это набор дефолтных префильтров. Если метод configureActions() ничего не будет возвращать, то именно эти префильтры по дефолту подставит битрикс. Если же метод возвращает массив, в одном из ключей которого есть описание метода test, то выполнятся именно пользовательские префильтры, а стандартные будут проигнорированы.

Выше я говорил, что можно убрать проверку csrf токена на бэкенде, это делается именно тут, в методе configureActions(). Чтобы csrf токен не проверялся нужно убрать ActionFilter\HttpMethod и ActionFilter\Csrf из возвращаемого массива для конкретного ajax метода, недостаточно убрать просто ActionFilter\Csrf, если в ActionFilter\HttpMethod разрешен POST, то префильтр ActionFilter\Csrf автоматически включается.

На что еще обратить внимание. В методе testAction есть два аргумента — $param2 и $param1. Я намеренно поставил их в обратном порядке, чтобы показать как происходит маппинг данных из запроса. Если вы вернетесь в описание клиентской части, то увидите, что там передается param1, а param2 не передается. Битрикс проверяет имена переменных через php reflection и передает в аргументы метода ajax данные с тем же типом и названием. То есть другими словами не нужно задумываться какой у вас там аргумент первый, какой второй, как это все придет из браузера, в каком порядке, достаточно просто называть переменные одинаково на сервере и на клиенте.

В клиентском запросе мы указывали mode: ‘class’, если посмотреть jsdoc битриксовой функции, то оказывается, что она принимает еще и mode: ‘ajax’. На самом деле это практически то же самое, только подключается не class.php из папка компонента, а файл ajax.php. Ну и сам класс нужно наследовать немного иначе. Вот пример класса в файле ajax.php:

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

use Bitrix\Main\Engine\Controller;

class CustomAjaxController extends Controller
{
	/**
	 * @return array
	 */
	public function configureActions()
	{
		return [
			'test' => [
				'prefilters' => []
			]
		];
	}

	/**
	 * @param string $param2
	 * @param string $param1
	 * @return array
	 */
	public static function testAction($param2 = 'qwe', $param1 = '')
	{
		return [
			'asd' => $param1,
			'count' => 300
		];
	}

}

То есть примерно то же самое, только убрали implements Controllerable, и отнаследовались от Controller.В остальном никаких отличий нет. Название класса так же как и с компонентом не важно.

Запрос к модулю:

Теперь попробуем отправлять запрос не к компоненту а к модулю. Я буду отправлять запросы к модулю local.lib.

Клиентская сторона

var request = BX.ajax.runAction('local:lib.api.test.example', {
    data: {
        param1: 'hhh'
    }
});

request.then(function(response){
    console.log(response);
});

Вот опять же код и jsdoc вызываемой функции

/**
 *
 * @param {string} action
 * @param {Object} config
 * @param {?string} [config.analyticsLabel]
 * @param {string} [config.method='POST']
 * @param {Object} [config.data]
 * @param {?Object} [config.headers]
 * @param {?Object} [config.timeout]
 * @param {Object} [config.navigation]
 * @param {number} [config.navigation.page]
 */
BX.ajax.runAction = function(action, config)
{
	config = prepareAjaxConfig(config);
	var getParameters = prepareAjaxGetParameters(config);
	getParameters.action = action;

	var url = BX.util.add_url_param('/bitrix/services/main/ajax.php', getParameters);

	return buildAjaxPromiseToRestoreCsrf({
		method: config.method,
		dataType: 'json',
		url: url,
		data: config.data,
		timeout: config.timeout,
		preparePost: config.preparePost,
		headers: config.headers
	});
};

Аналогичный запрос на jquery:

var query = {
    action: 'local:lib.api.test.example',
};

var data = {
    param1: 'eee',
    SITE_ID: 's1',
    //sessid: BX.message('bitrix_sessid')
};

var request = $.ajax({
    url: '/bitrix/services/main/ajax.php?' + $.param(query),
    method: 'POST',
    data: data
});

request.done(function (response) {
    console.log(response);
});

Принципы формирования sessid те же, что и для компонента.

Серверная сторона

На серверной стороне есть отличия. Для начала в папке с модулем нам понадобится файл .settings.php, именно в папке с модулем. Вот пример файла:

<?php
return [
	'controllers' => [
		'value' => [
			'namespaces' => [
				'\\Local\\Lib\\Controller' => 'api'
			]
		],
		'readonly' => true
	]
];

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

Далее создаем папку lib/controller и файл test.php в этой папке:

<?php
namespace Local\Lib\Controller;

use Bitrix\Main\Engine\Controller;

class Test extends Controller
{
	/**
	 * @return array
	 */
	public function configureActions()
	{
		return [
			'example' => [
				'prefilters' => []
			]
		];
	}

	/**
	 * @param string $param2
	 * @param string $param1
	 * @return array
	 */
	public static function exampleAction($param2 = 'qwe', $param1 = '')
	{
		return [
			'asd' => $param1,
			'count' => 300
		];
	}
}

Содержимое тут примерно такое же как и в файле ajax.php компонента, так что останавливаться на этом не буду.

Ключевое для понимания работы аякс в модуле — как именно формируется action и как в итоге будет обрабатываться запрос. Разберем тестовый экшен local:lib.api.test.example

  • local:lib — модуль local.lib, где точку заменили на двоеточие
  • api — неймспейс который мы зарегистрировали в .settings.php модуля
  • test — название файла и класса в папке lib/controller
  • example — метод exampleAction в классе Test, без суффикса action

Каждый отдельный файл регистрировать в .settings.php модуля не нужно.

Выводы

В целом пока что впечатления от нового механизма положительные. Нужно еще оценить удобство на практике, но чувствуется, что над функционалом довольно долго думали, проектировали. Много нового функционала. Я не затронул еще одну интересную штуку про которую рассказывали на видео — механизм auto wiring (автоматическое связывание), простых примеров так сразу не смог придумать.

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

Если информация оказалась полезной, скажите спасибо, это мотивирует писать чаще 🙂


добавил Шубин Александр 22 Сентябрь, 2018
Рубрика: AJAX


25 комментариев

  • Отличная статья, спасибо, Александр.

  • Парфён Рогожин:

    Пропустил анонс этой фичи в своё время. Спасибо за краткий экскурс!)

  • Славка:

    При обращении к модулю (vendor.module) нужно точку заменить не на нижнее подчеркивание , а на :

    • Да, спасибо. Давно собираюсь поправить в статье. На момент написания было подчеркивание, но через несколько недель обновили

  • coder420:

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

    • Почему эта часть самая интересная? В некоторых случаях удобно, но не более, это не ключевая вещь. Не знаю кто ее «утаил», тем более «как всегда».

  • Юрий:

    Не получается.
    В ответе получаю {«status»:»error»,»data»:null,»errors»:[{«message»:»Could not build component instance namer:workplace.lists.element.edit»,»code»:0,»customData»:null}]}

    • У тебя есть файл /bitrix/components/namer/workplace.lists.element.edit/class.php или /local/components/namer/workplace.lists.element.edit/class.php?

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

  • Максим:

    Не работает для гостей.
    Status Code: 401 Unauthorized
    В классе компонента убран параметр new ActionFilter\Authentication(). При авторизации на сайте все ок.

    • Никита:

      Решили проблему? То же самое.

      • Выше я говорил, что можно убрать проверку csrf токена на бэкенде, это делается именно тут, в методе configureActions(). Чтобы csrf токен не проверялся нужно убрать ActionFilter\HttpMethod и ActionFilter\Csrf из возвращаемого массива для конкретного ajax метода, недостаточно убрать просто ActionFilter\Csrf, если в ActionFilter\HttpMethod разрешен POST, то префильтр ActionFilter\Csrf автоматически включается.

        Возможно дело в этом. Попробуй вообще все префильтры убрать

    • Александр:

      такая же проблема(

  • Максим:

    Добрый день. Спасибо за хорошее руководство, написанное внятным языком.
    Попробовал сделать запрос к модулю по описанному и при вызове BX.ajax.runAction получаю сообщение об ошибке Could not find description of api.ec_ajax.test in Bitrix\\Main\\Engine\\DefaultController. Такое впечатление, что моего контроллера в упор не видно. Не подскажете ли, куда копать, что почитать?

    • У меня самая частая ошибка — отсутствие постфикса Action у методов. То есть функция в классе должна называться testAction() чтобы ее механизм аякса корректно подхватил.

      Ну и код действия ты неверно указаываешь. Если там к модулю идет обращение, то нужно писать local:lib.api.test.example

      Я выше в тексте пояснял что значит и как формируется это строка. Почему у тебя api.ec_ajax.test в вызове на фронте я не знаю.

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

      • Максим:

        С постфиксом всё хорошо. Функция в классе называется testAction().

        Вызов на фронте полностью выглядит так
        var request = BX.ajax.runAction(‘evotorcards.api.ecajax.test’, {
        data: {cardNumber: ‘123456’}
        });
        evotorcards — имя моего модуля, в его названии не используется точка.

        Путь к файлу с классом контроллера в твоём примере выглядит так local.lib\lib\controller\test.php ?

        • Да, у меня путь такой.

          В твоем случае путь будет зависеть от того как ты его зарегистрируешь в .settings.php модуля.

          • Максим:

            Ты об этом?
            ‘namespaces’ => [ ‘\\Evotorcards\\Controller’ => ‘api’,],
            И путь у меня evotorcards\lib\controller\ecajax.php
            Всё верно?

    • Была аналогичная ошибка. Я накосячил в том, что файл контроллера назвал с большой буквы. Т.е. класс контроллера называем Test, а файл — lib/controller/test.php

  • Игорь Бадьин:

    В версии 18.0.2 выкидывало Exception при обращении к модудю «There is no configuration in vendor:module with ‘controllers’ value.»

    Проблема: Битрикс не мог найти конфиг модуля потому, что обращался к конфигу по кулючу vendor:module
    вместо vendor.module (проблема в двоеточии)

    Лечение:
    1) Обновление Битрикс
    2) Если нет возможности, то можно взять из новой версии пару файлов и заменить их /bitrix/modules/main/lib/engine/router.php и /bitrix/modules/main/lib/engine/resolver.php

  • Денис:

    А как файл передать? Есть в новом AJAX готовый механизм?

    • Просто передавай объект FormData вместо переменных. Не уверен что битриксовый js это поддерживает. На сервере корректно обрабатывается. Вот пример на jquery, пару лет назад писал в ответ на похожий вопрос:

      <form action="">
          <input type="file" name="test" id="file">
          <input type="text" name="text_test" id="" value="asd">
          <input type="button" onclick="sendAjax()" value="send">
      </form>
       
      <script>
          function sendAjax() {
              var query = {
                  c: 'custom:ajax',
                  action: 'test',
                  mode: 'class'
              };
       
              var data = new FormData();
              $.each($('#file')[0].files, function(i, file) {
                  data.append('file', file);
              });
       
              data.append('param1', 'eee');
              data.append('SITE_ID', 's1');
              data.append('sessid', BX.message('bitrix_sessid'));
       
              var request = $.ajax({
                  url: '/bitrix/services/main/ajax.php?' + $.param(query, true),
                  data: data,
                  cache: false,
                  contentType: false,
                  processData: false,
                  method: 'POST',
                  type: 'POST',
              });
       
              request.done(function (response) {
                  console.log(response);
              });
       
              return false;
          }
      </script>
      
      
      		
      				
  • Александр:

    При попытке ajax запроса выдает вот такую ошибку
    The component opensource:order must be implement interface \\Bitrix\\Main\\Engine\\Contract\\Controllerable

    • В клиентском запросе мы указывали mode: ‘class’, если посмотреть jsdoc битриксовой функции, то оказывается, что она принимает еще и mode: ‘ajax’. На самом деле это практически то же самое, только подключается не class.php из папка компонента, а файл ajax.php.

      Цитата из статьи выше.
      В файле компонента заказа нет ни одного метода который бы отвечал на аякс запросы. Весь аякс вынесен в ajax.php в папке компонента. И запросы соответственно надо сласть с mode = ajax.

  • ajax упорно возвращал ошибку «Could not find description of api.test.example in Bitrix\Main\Engine\DefaultController»

    почему то нигде не упомятнуто, видимо для всех это само «собой разумеется» но я два часа тупил, пока дебагом не прошелся — результат:

    файл контроллера /local/components/custom/ajax/class.php должен быть подтянут автолоадером в модуле!

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *