Как стать автором
Обновить

Архитектура MVC и поддержка реактивности для jQuery

Уровень сложностиСредний
Время на прочтение15 мин
Количество просмотров7.2K

Здравствуйте, уважаемый читатель!

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

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

Зачем нам в проектирование ?

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

  • Карты

  • CRM

  • Приложения для учета бизнес задач

  • Редакторы всех жанров

  • Веб-приложения для просмотра медиа контента

  • ...много других пунктов, добавить по желанию

    В них может быть заложено: 

  • Калькуляторы считающие самые разные показатели

  • Сложные и простые фильтрации данных и их структурирование

  • Организация проверок разного формата, сюда же можно отправить и обработку ошибок

  • Преобразование данных из одного формата в другой. ( допустим конвертация валют )

  • Работа с кэшированием данных

  • ...еще больше других пунктов, добавить по желанию

Тут и далее, я постараюсь максимально отодвинуть от этого вопроса взаимодействие с сервером, хотя стоит отметить - правильная организация получения и отправки данных - это тоже достаточно серьезная работа со стороны клиента. Вдобавок это коснется примеров которые можно назвать “специфическими”, как пример - онлайн игры в браузере. В контексте этой статьи будет уместнее обратить внимание на более частые варианты встречаемых в сети веб-страниц, а случаи по типу игр - достойны отдельного глубокого погружения в специфику их разработки.

Примеры выше нужны дабы объяснить простую мысль, чтобы контролируемо управлять всем этим, необходимо выстраивать свою работу исходя из определенных практик проектирования, и если мы не хотим смешивать расчеты и отображение на нашем сайте ( а мы вероятно не хотим), мы можем взять и использовать для себя преимущества паттернов которые уже существуют, в нашем случае это будет - MVC ( Model View Controller ), точнее с точки зрения реализации этого паттерна, мы будем смотреть на реактивность, вот такая вот статья.

Не могу удержатся от вставки материала по теме, прошу не ругайте :)

Не сложнее Frontend, чем Backend. Это области, где нужны и общие, и специальные знания, но картинка смешная :)
Не сложнее Frontend, чем Backend. Это области, где нужны и общие, и специальные знания, но картинка смешная :)

Архитектура MVC на client

И сразу давайте определимся с тем, что в нашем случае будет подразумевать под собой реализация MVC архитектуры:

Model - любая логика которая может: принять определенный набор данных и вернуть обратно результат своей работы. Сюда не должно попадать ничего, что может касаться отображения. Помимо чистоты использования, удобной организации - это даст нам возможность, при больших и сложных расчетах легко перевести этот код на WASM, и не потерять в производительности ( так как WASM работает хуже при взаимодействии с DOM, чем JS ).

View - это отображение и только - сюда не должно попасть каких либо расчетов.

Controller - это звено, которое будет принимать данные от View, после чего отправлять в Model, и обратно. По сути, это промежуточная сущность, которая отвечает за взаимодействие нашего интерфейса с логикой нашего приложения.

"Фронтендеру - не нужно!"

Встречались разработчики, которые были не согласны с такой трактовкой. Их позиция касательно этого паттерна была такова, - “Model и Controller - это сервер, а View это клиент!”.  Действительно, если мы пойдем изучать ресурсы связанные с этим вопросом, то как пример его применения мы увидим, что часто сущности Model и Controller отданы на контроль под серверную часть, а View - это client. Не спорю, так тоже можно, иногда даже нужно, но не всегда возможно. От расчетов на клиенте простыми способами в современном мире разработки убежать не получится, а на каждое действие отправлять запрос на сервер, это противоположность оптимистичного интерфейса и производительности ресурса в целом. Конечно, можно организовать построение по MVC на сервер-клиент и только на клиенте.

Последнее, что тут хочется отметить, что на условном WPF который может вообще не производить работы по сети, этот вопрос не возникает. На desktop MVVM и MVC - это наше всё, но как только вопрос коснулся веб пространства мы сталкиваемся с такими разногласиями. Ваше мнение в комментариях по этому вопросу приветствуется, автор тоже из рода людского, и ошибаться вполне может.

Причем тут реактивность и jQuery ?

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

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

Я уже начал писать пример, но решил остановится. Это выходит за рамки темы этой статьи и я не хотел бы красть так много времени у читателей. Если вы хотите получить пример с точки зрения Frontend, хорошим вариантом будет посмотреть отличия любого современного веб-фреймворка от нативной разработки.

Также хотелось бы процитировать Википедию:

К примеру, в MVC архитектуре с помощью реактивного программирования можно реализовать автоматическое отражение изменений из Model в View и наоборот из View в Model.

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

Важно уточнить, что мы будем строго придерживаться и названия этой статьи, и рассматривать вопрос со стороны написания кода с использованием jQuery, и для этого есть как минимум 2 обоснования:

Первое, на jQuery пишут. В сети много разной информации, о том сколько ресурсов сейчас уже поддерживает jQuery, сколько планируется ожидать таковых в будущем, также стоит ли использовать её при разработке своего веб ресурса или всё же избегать её появление в коде. Тем не менее, можно посетить страницу на Github и убедится, что есть определенное количество людей которые используют её в своих проектах, и она по сей день также продолжает получать обновления. 

Это обсуждение стоит отдельной, проработанной статьи, но тут важно отметить, что именно ресурсы с  использованием jQuery ( не используя сторонних решений ) сильно нарушают обсуждаемую архитектуру, в угоду удобства использования и быстроты написания кода ( тут с этим сложно спорить, библиотека jQuery действительно очень удобная в использовании), но jQuery и создавалась с иными целями с которыми прекрасно справляется и сегодня.

Второе, с остальными инструментами в этом вопросе проще. Если мы говорим о современном веб-фреймворке, то они построены с учетом опыта сообщества Frontend разработчиков. Это означает, что в них изначально заложена определённая гибкость и обсудить их архитектуру построения - это отдельная статья.

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


Как это сделать ? Какие варианты ?

Сразу стоит упомянуть, то что сейчас уже есть и можно использовать. Связка RxJS и Backbone.js при правильном взгляде на построение проекта могут решить озвученную выше проблему, но требуют тяжелого процесса интеграции в уже существующие веб приложения. Также был замечен недостаток выраженный в разрастании кодовой базы, из-за абстракций который поставляет вместе с собою Backbone.js. Тем не менее этот вариант тоже предлагается рассмотреть.

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

Начнём с того, что подключим это всё в проект. Так как это тестовая среда, воспользуемся CDN, но если вы хотите использовать это в своём проекте ( неважно, будет ли это код дополняющий существующий или создание нового решения ) то крайне желательно - скачать код библиотек локально. ( это тема для споров, автор с ней ознакомлен )

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Список дел</title>
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.13.1/underscore-min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/backbone.js/1.4.1/backbone-min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/7.5.0/rxjs.umd.min.js"></script>
    <script src="./script.js"></script>
</head>
<body>
    <input id="todoInput" type="text" placeholder="Новая задача">
    <button id="addTodoBtn">Добавить</button>
    <ul id="todoList"></ul>
</body>
</html>

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

Создадим модели и коллекции

// Модель
const TodoModel = Backbone.Model.extend({
  defaults: {
    title: "",
    completed: false,
  },
});

// Коллекция
const TodoCollection = Backbone.Collection.extend({
  model: TodoModel,
});

Обработку событий отдадим под крыло RxJs

const addTodoClick = rxjs.fromEvent($("#addTodoBtn"), "click");

addTodoClick.subscribe(() => {
  const todoTitle = $("#todoInput").val().trim();
  if (todoTitle !== "") {
    todoCollection.add({ title: todoTitle });
/* Тут удобно будет очищать это поле ввода, 
* но учтите что это не желательный  подход. 
* Вообще чем меньше мы получаем *элементы с помощью $(elem) - то лучше.  
* Отлично, если это значение вовсе будет нулевым.
*/
      $("#todoInput").val("");
  }
});

Отображение и инициализация приложения

const TodoView = Backbone.View.extend({
  tagName: "li",
  template: _.template(
    '<input type="checkbox" <% if(completed) print("checked") %> /> <%= title %>'
  ),
  events: {
    'change input[type="checkbox"]': "toggle",
  },
  initialize: function () {
    this.listenTo(this.model, "change", this.render);
  },
  render: function () {
    this.$el.html(this.template(this.model.toJSON()));
    return this;
  },
  toggle: function () {
    this.model.set("completed", !this.model.get("completed"));
  },
});

const TodoListView = Backbone.View.extend({
  el: $("#todoList"),
  initialize: function () {
    this.listenTo(todoCollection, "add", this.addOne);
  },
  addOne: function (todo) {
    const todoView = new TodoView({ model: todo });
    this.$el.append(todoView.render().el);
  },
});

// Инициализация
const todoCollection = new TodoCollection();
new TodoListView({ collection: todoCollection });

Сразу стоит напомнить о том, что цель этой статьи показать как можно вести разработку веб страницы по MVC, объяснение принципов работы BackBone и RxJs - целью статьи не является.

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

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

Как итог этого блока, стоит сказать, что проверку временем этот подход не прошёл. Знатоки BackBone и RxJs скорее всего мне вероятно возразят, но я не в коем случае не хочу как-то принижать вклад каких-либо инструментов - просто сейчас, на мою скромную оценку, они достаточно редки в применении.
( RxJS используют как зависимость в Vue.js, но сейчас речь о чистой разработке без фреймворка ).

Важно, я к числу профи по ним - не отношусь, но обойти их в контексте MVC я тоже не мог. Пожалуйста, если вы обнаружили неточность или ошибку - укажите это. Заранее спасибо.

А если не использовать инструменты ?

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

  • Возможно, если мы будем писать под canvas на всю страницу.

То есть весь основной HTML, будет заменён на один canvas элемент. Это достаточно частный пример разработки, но он применяется для редких ресурсов или игр. С вашего позволения, я не буду тратить время читателей и приводить пример кода, так как профессионалам понятно о чем я пишу и они не нуждаются в моих примерах, а новоприбывшим возможно будет тяжело даже с ними.

Тем не менее, я не мог не упомянуть о такой возможности, тем более я обожаю Babylon.js и всё что связывает 3D в браузере, который работает и строится как раз таким образом.

Оговорюсь, что в таких случаях всё окружение canvas, как правило, это обвязка вокруг него. Далеко не значит что она не важна - это значит основная логика по работе с данными и отображением убрана под манипуляции с canvas.

  • написать контроллер самостоятельно

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

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

Для начала, сразу стоит показать HTML, кроме jQuery, ни одной библиотеки мы не подключили.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Список дел</title>
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
    <script src="./script.js"></script>
</head>
<body>
    <div id="root"></div>
</body>
</html>

Теперь нас будет интересовать только файл script.js, поскольку там мы и создадим всё нам необходимое.

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

class MyController {
  registration(target, Node, fn) {
    return new Proxy(target, {
      get: (target, prop) => target[prop],
      set: (_, prop, val) => {
        fn(Node, val)
        target[prop] = val;
        return target[prop];
      }
    })
  };
}

Наш MyController состоит всего из 1 метода для регистрации состояний. Класс, тут не обязателен и его можно вынести в функцию, но скорее всего у вас появятся свои абстракции, так что лучше хранить их вместе - в классе или группе классов. В данном случае, всё что мы делаем, это привязываем конкретный элемент DOM к изменению определенного объекта. Собственно объект который мы будем изменять и отдаёт метод registration.

Теперь можно создать базовую функциональность нашего приложения, давайте сделаем и это:

const collection = $('<div>').css({
    display: 'flex',
    'flex-direction': 'column'
  });
  const addTodoField = $('<input>').prop('placeholder', 'Название задачи');
  const addButton = $('<button>').text('Добавить');

  $('#root').append(
    $('<div>').css({ display: 'flex' }).append(
      addTodoField,
      addButton
    ),
    collection
  );

Благодаря jQuery - это достаточно простая операция, и не требует каких-то отдельных объяснений. Нам осталось только - получить нашу библиотеку и зарегистрировать через нее наши контроллеры.

 const controller = new MyController();

  const collectionState = controller.registration({ state: [] }, collection, (node, val) => {
    node.empty();
    val.forEach(item => {
      node.append(
        $('<span>').text(item),
      )
    })
  });

  const inputState = controller.registration({ state: '' }, addTodoField, (node, val) => {
    node.val(val);
  });

Вы можете обратить внимание, что тут тоже нет ничего сложного. Единственный вопрос в том, какой первый аргумент мы отдали в registration, это тот объект который будет проксирован, но в нашем случае - правильно назвать его initial, то есть состоянием при инициализации. Обязательно нужно передать объект, подробнее можете почитать тут Proxy.

И теперь, чтобы поменять что-то на странице, допустим добавить TODO нам ничего не нужно кроме изменения данных в контроллере:

addTodoField.on('input',(e) => {
    inputState.state = e.target.value;
  })

addButton.click(() => {
    const todos = collectionState.state;
    todos.push(inputState.state);
    collectionState.state = todos;
    inputState.state = '';
  })

И тут мы не работаем с элементами напрямую, мы работаем с состояниями и при изменении данных мутируем только их.

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

const delButton = $('<button>').text('Очистить').on('click', () => {
    collectionState.state = [];
}).appendTo('#root');

Также с полем ввода получается интересная вещь, которую в React часто называют “контролируемое состояние”, так как мы постоянно синхронизируем поле ввода с контролирующим его объектом, нам теперь вообще не нужно получать это поле ввода для того чтобы его изменить или получить данные в него записанные.

Вы уже могли заметить, что в этом примере мы тоже придерживаемся MVC.

Разница, будет на порядок заметнее, когда мы будем передавать сущности типа collectionState.state как поставщики данных - в наши функции логики, и при присвоении им нового значения они сами смогут поменять наше отображение, нам уже не нужно думать о получении и передачи чего-либо, в какой-либо селектор. Еще мы можем сократить тот большой набор данных в HTML, и не думать о проблемах типа:

"Стоит ли удалять этот css класс из вёрстки ? Вдруг в коде что-то по нему искалось ?"

или

"Какой же id назначить этому элементу, чтобы он не дай бог не повторился ?"

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

Проблемой тут, конечно, является написание класса MyController. В примере, самый минимальный вариант, который максимум позволяет организовать простые манипуляции с данными и DOM, но для реальных проектов, нам нужно будет как правило куда больше возможностей и абстракций. Решение есть в следующем блоке.

Micro Component

Внимание

Ранее, автор уже написал решение для этого вопроса и осознанно поместил его в конец статьи. Это не реклама «самой лучшей библиотеки», выше было указано как можно добиться результата и без использования дополнительных инструментов ( в том числе и этого ) или с использованием более проверенных решений.

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

MC создавался с целью легкой интеграции в существующие проекты. Настолько легкой, что можно просто взять и написать код, это небольшое отступление нужно чтобы объяснить логику его применения в рамках jQuery. Давайте посмотрим как это реализовано, и чтобы уже не слезать с рельс TODO, смотрим на этом же примере:

Для начала нам нужно получить библиотеку, для этого можно воспользоваться Github или написать в терминале:

npm i jquery-micro_component

После этого мы можем добавить его в наш index.html. 

Обратите внимание, в MC пока что нет поддержки сборщиков. Это будет добавлено позже, а информацию о появлении вы всегда можете найти на Github или в документации.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Список дел</title>
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
    <script src="./script.js"></script>
    <script src="./node_modules/jquery-micro_component/MC.min.js"></script>
</head>
<body>
    <div id="root"></div>
</body>
</html>

Мы использовали npm и добавили нашу библиотеку, остальной код остался не тронут.

Сразу стоит отметить, что помимо реализации архитектуры, MC дробит код на компоненты, добавляя модульность в ваш проект. Забегая вперед, многие увидят сквозное влияние React, оно действительно есть, но достаточно минимально. 

Еще раз хочу напомнить, что рассматриваемая тема MVC и реактивность, поэтому мы не будем детально рассматривать абстракции МС, но я понимаю, что какие-то вещи могут быть сейчас непонятны. Если кому-то будет интересно то можно будет рассказать о MC подробнее в соответствующей статье.

Сразу посмотрим весь код, и где тут MVC с реактивностью.

document.addEventListener("DOMContentLoaded", () => {
// Инициализируем библиотеку ( необходимо один раз на страницу )
MC.init();

// Кнопка
class Button extends MC {
  constructor() {
    super();
  }

  /**
  * Этот метод отдаст верстку
  **/
  render(state, props) {
    return $('<button>').text(props.text).on('click', props.event);
  }
}

// Список дел
class Todos extends MC {
  constructor() {
    super();
  }

  /**
  * Этот метод отдаст верстку
  **/
  render(state, props) {
    return $('<div>').css({ display: 'flex', 'flex-direction': 'column' }).append(
      props.todos.map((todo, index) => {
        return $('<div>').append(
            $('<span>').text(todo),
            
            $.MC(Button, MC.Props({
              props: {
                text: 'Удалить',
                event: () => props.deleteEvent(index)
              }
            }), 'del_btn'),
          );
      })
    )
  }
}

// Наше приложение
    class TodoApp extends MC {
      todos;

      constructor() {
        super();
        /**
         * Это тот самый, но уже прокаченный объект прокси.
        **/
        this.todos = super.state([]);
        this.inputValue = '';
      }

      addTodo() {
        if(!this.inputValue){
          return;
        }

        const todos = this.todos.get();
        todos.push(this.inputValue);
        this.inputValue = '';
        this.todos.set(todos);
      }

      deleteTodo(index) {
        const newTodos = this.todos.get();
        newTodos.splice(index, 1);
        this.todos.set(newTodos);
      }
      
      render(state) {
        const [ todos ] = state.local;

        return $('<div>').append(
          $('<input>').prop('placeholder', 'Название задачи').val(this.inputValue).on('input', (e) => {
            this.inputValue = e.target.value;
          }),

          $.MC(Button, MC.Props({
            props: {
            text: 'Добавить',
            event: () => this.addTodo()
            }
          }), 'add_btn'),

          $.MC(Todos, MC.Props({
            props: { 
              todos: todos,
              deleteEvent: (index) => this.deleteTodo(index)
            }
          }), 'todos')
      )
    }
    }
  
    //точка входа
    $('#root').append(
       /**
         * Такая структура нужна, чтобы можно было внедрять такие абстрации
         * в уже ранее написанные проекты.
        **/
      $.MC(TodoApp)
    )
});

Начнём с того, что мы привязали к html элементу #root. Наш созданный компонент TodoApp, вернёт вёрстку из своего внутреннего метода render. В самом компоненте, вы можете наблюдать создание сущности которая нам в формате рассматриваемой темы, наиболее интересна - this.todos = super.state([]);

Это одна из абстракций, которая работает простым способом, когда происходит вызов

this.todos.set(value) - мы обновляем все компоненты в render, которые имеют от неё зависимость. В данном случае, это класс TodoApp.

Получается наш контроллер в этом варианте, будет MC - который предоставляет услуги по обработке нашего jQuery кода и мутацией нужных элементов DOM при изменении конкретных данных - то есть реализуя реактивность, а отображением - будет являться всё, что есть в render ( мы не должны записывать в этот метод логику ) и jQuery который находится за пределами компонента. Логика же, это любая функция которая может передаваться в методы компонента.

Обратите внимание на важный момент. Логику расчетов мы должны выносить за пределы компонентов, так как внутренние методы желательно отдать под настройку данных в контроллере или операции конкретного компонента.


Вывод

И вот ура, мы финишировали в этом исследовании!

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

Я уверен, что люди предложат большее количество вариантов, как можно применять такие практики используя другие инструменты, но пожалуйста, обратите внимание на следующее:

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

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

Но, если вам очень нравится jQuery - нет ничего зазорного в том чтобы писать фронт на нём, ведь главное это ваш код, и если писать плохо - на любой технологии будет соответствующий результат.

Надеюсь вам было также интересно как и мне.

Успехов в кодировании!

Теги:
Хабы:
Всего голосов 3: ↑3 и ↓0+3
Комментарии10

Публикации

Истории

Работа

Ближайшие события

Антиконференция X5 Future Night
Дата30 мая
Время11:00 – 23:00
Место
Онлайн
OTUS CONF: GameDev
Дата30 мая
Время19:00 – 20:30
Место
Онлайн
Конференция «IT IS CONF 2024»
Дата20 июня
Время09:00 – 19:00
Место
Екатеринбург
Summer Merge
Дата28 – 30 июня
Время11:00
Место
Ульяновская область