Архитектура MVC и поддержка реактивности для jQuery
Здравствуйте, уважаемый читатель!
В этой статье мы рассмотрим методы создания веб-ресурсов со стороны Frontend разработки, сосредоточившись на подходах которые могут помочь нам с помощью реактивности достичь результатов по разделению логики и отображения.
Сразу хотелось бы отметить, что всегда приветствуется критика, а также предложения по дополнению данной статьи, и что это стартовый опыт автора, в создании материала такого рода, если вы обратили внимание на явный недостаток и у вас есть желание помочь, пожалуйста укажите это — постараюсь оперативно устранить найденную проблему. Заранее, спасибо за понимание.
Зачем нам в проектирование?
В современном мире разработки интерфейсов, уже появилось приличное количество ресурсов которые имеют в себе достаточно сложный пользовательский интерфейс:
Карты
CRM
Приложения для учета бизнес задач
Редакторы всех жанров
Веб-приложения для просмотра медиа контента
…много других пунктов, добавить по желанию
В них может быть заложено:
Калькуляторы считающие самые разные показатели
Сложные и простые фильтрации данных и их структурирование
Организация проверок разного формата, сюда же можно отправить и обработку ошибок
Преобразование данных из одного формата в другой. (допустим конвертация валют)
Работа с кэшированием данных
…еще больше других пунктов, добавить по желанию
Тут и далее, я постараюсь максимально отодвинуть от этого вопроса взаимодействие с сервером, хотя стоит отметить — правильная организация получения и отправки данных — это тоже достаточно серьезная работа со стороны клиента. Вдобавок это коснется примеров которые можно назвать «специфическими», как пример — онлайн игры в браузере. В контексте этой статьи будет уместнее обратить внимание на более частые варианты встречаемых в сети веб-страниц, а случаи по типу игр — достойны отдельного глубокого погружения в специфику их разработки.
Примеры выше нужны дабы объяснить простую мысль, чтобы контролируемо управлять всем этим, необходимо выстраивать свою работу исходя из определенных практик проектирования, и если мы не хотим смешивать расчеты и отображение на нашем сайте (, а мы вероятно не хотим), мы можем взять и использовать для себя преимущества паттернов которые уже существуют, в нашем случае это будет — MVC (Model View Controller), точнее с точки зрения реализации этого паттерна, мы будем смотреть на реактивность, вот такая вот статья.
Не могу удержатся от вставки материала по теме, прошу не ругайте:)
Не сложнее 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, но если вы хотите использовать это в своём проекте (неважно, будет ли это код дополняющий существующий или создание нового решения) то крайне желательно — скачать код библиотек локально. (это тема для споров, автор с ней ознакомлен)
Список дел
Теперь можно создать модели, представления и коллекции. Так же добавить реактивность. Всю информацию, что из себя всё это может представлять — можно получить в официальной документации применяемых инструментов.
Создадим модели и коллекции
// Модель
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(
' /> <%= 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, для проекта, без дополнительных библиотек. Ответ будет сложным, но давайте пойдем по порядку:
То есть весь основной HTML, будет заменён на один canvas элемент. Это достаточно частный пример разработки, но он применяется для редких ресурсов или игр. С вашего позволения, я не буду тратить время читателей и приводить пример кода, так как профессионалам понятно о чем я пишу и они не нуждаются в моих примерах, а новоприбывшим возможно будет тяжело даже с ними.
Тем не менее, я не мог не упомянуть о такой возможности, тем более я обожаю Babylon.js и всё что связывает 3D в браузере, который работает и строится как раз таким образом.
Оговорюсь, что в таких случаях всё окружение canvas, как правило, это обвязка вокруг него. Далеко не значит что она не важна — это значит основная логика по работе с данными и отображением убрана под манипуляции с canvas.
По сути нам нужно сделать это связующее звено. Его задача понятна, это умение определять изменение конкретных данных, и на основании этих изменений мутировать элементы в отображении. Этого можно добиться с помощью сохранения DOM узла в определенную область памяти, и с помощью Proxy отдать объект который будет при изменении мутировать наш сохранённый ранее элемент DOM.
Давайте попробуем написать пример:
Обратите внимание, тут я более подробно остановлюсь на рассматриваемой теме, так как в документацию которая что-то расскажет, увы, вы уже посмотреть не сможете.
Для начала, сразу стоит показать HTML, кроме jQuery, ни одной библиотеки мы не подключили.
Список дел
Теперь нас будет интересовать только файл 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 = $('').css({
display: 'flex',
'flex-direction': 'column'
});
const addTodoField = $('').prop('placeholder', 'Название задачи');
const addButton = $('