Архитектура 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 = $('
Благодаря jQuery — это достаточно простая операция, и не требует каких-то отдельных объяснений. Нам осталось только — получить нашу библиотеку и зарегистрировать через нее наши контроллеры.
const controller = new MyController();
const collectionState = controller.registration({ state: [] }, collection, (node, val) => {
node.empty();
val.forEach(item => {
node.append(
$('').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 = $('
Также с полем ввода получается интересная вещь, которую в React часто называют «контролируемое состояние», так как мы постоянно синхронизируем поле ввода с контролирующим его объектом, нам теперь вообще не нужно получать это поле ввода для того чтобы его изменить или получить данные в него записанные.
Вы уже могли заметить, что в этом примере мы тоже придерживаемся MVC.
Разница, будет на порядок заметнее, когда мы будем передавать сущности типа collectionState.state как поставщики данных — в наши функции логики, и при присвоении им нового значения они сами смогут поменять наше отображение, нам уже не нужно думать о получении и передачи чего-либо, в какой-либо селектор. Еще мы можем сократить тот большой набор данных в HTML, и не думать о проблемах типа:
«Стоит ли удалять этот css класс из вёрстки ? Вдруг в коде что-то по нему искалось ?»
или
«Какой же id назначить этому элементу, чтобы он не дай бог не повторился ?»
Достаточно иметь привязку на каком либо этапе, а далее манипулировать отображением через наш контроллер.
Проблемой тут, конечно, является написание класса MyController. В примере, самый минимальный вариант, который максимум позволяет организовать простые манипуляции с данными и DOM, но для реальных проектов, нам нужно будет как правило куда больше возможностей и абстракций. Решение есть в следующем блоке.
Micro Component
Внимание
Ранее, автор уже написал решение для этого вопроса и осознанно поместил его в конец статьи. Это не реклама «самой лучшей библиотеки», выше было указано как можно добиться результата и без использования дополнительных инструментов (в том числе и этого) или с использованием более проверенных решений.
Для тех кому малоинтересен просмотр варианта предлагаемого автором, следующий блок можно пропустить.
MC создавался с целью легкой интеграции в существующие проекты. Настолько легкой, что можно просто взять и написать код, это небольшое отступление нужно чтобы объяснить логику его применения в рамках jQuery. Давайте посмотрим как это реализовано, и чтобы уже не слезать с рельс TODO, смотрим на этом же примере:
Для начала нам нужно получить библиотеку, для этого можно воспользоваться Github или написать в терминале:
npm i jquery-micro_component
После этого мы можем добавить его в наш index.html.
Обратите внимание, в MC пока что нет поддержки сборщиков. Это будет добавлено позже, а информацию о появлении вы всегда можете найти на Github или в документации.
Список дел
Мы использовали npm и добавили нашу библиотеку, остальной код остался не тронут.
Сразу стоит отметить, что помимо реализации архитектуры, MC дробит код на компоненты, добавляя модульность в ваш проект. Забегая вперед, многие увидят сквозное влияние React, оно действительно есть, но достаточно минимально.
Еще раз хочу напомнить, что рассматриваемая тема MVC и реактивность, поэтому мы не будем детально рассматривать абстракции МС, но я понимаю, что какие-то вещи могут быть сейчас непонятны. Если кому-то будет интересно то можно будет рассказать о MC подробнее в соответствующей статье.
Сразу посмотрим весь код, и где тут MVC с реактивностью.
document.addEventListener("DOMContentLoaded", () => {
// Инициализируем библиотеку ( необходимо один раз на страницу )
MC.init();
// Кнопка
class Button extends MC {
constructor() {
super();
}
/**
* Этот метод отдаст верстку
**/
render(state, props) {
return $('
Начнём с того, что мы привязали к html элементу #root. Наш созданный компонент TodoApp, вернёт вёрстку из своего внутреннего метода render. В самом компоненте, вы можете наблюдать создание сущности которая нам в формате рассматриваемой темы, наиболее интересна — this.todos = super.state ([]);
Это одна из абстракций, которая работает простым способом, когда происходит вызов
this.todos.set (value) — мы обновляем все компоненты в render, которые имеют от неё зависимость. В данном случае, это класс TodoApp.
Получается наш контроллер в этом варианте, будет MC — который предоставляет услуги по обработке нашего jQuery кода и мутацией нужных элементов DOM при изменении конкретных данных — то есть реализуя реактивность, а отображением — будет являться всё, что есть в render (мы не должны записывать в этот метод логику) и jQuery который находится за пределами компонента. Логика же, это любая функция которая может передаваться в методы компонента.
Обратите внимание на важный момент. Логику расчетов мы должны выносить за пределы компонентов, так как внутренние методы желательно отдать под настройку данных в контроллере или операции конкретного компонента.
Вывод
И вот ура, мы финишировали в этом исследовании!
Сразу хочу сказать спасибо всем, кто погрузился в этот вопрос и дочитал тему до этих строк — это показывает, что людям важно знать как сделать их продукт лучше, а значит по итогу и улучшить пользовательский опыт от их продукта! Тем не менее важно сказать, что если у вас небольшая страница, стоит задуматься — поможет ли вам внедрение таких подходов или только замедлит вашу разработку не привнеся определённых результатов. Каждый инструмент или даже просто совет — нужно использовать с умом, там где это нужно.
Я уверен, что люди предложат большее количество вариантов, как можно применять такие практики используя другие инструменты, но пожалуйста, обратите внимание на следующее:
Настоятельно рекомендую, при проектировании нового веб-приложения, используйте решения которые больше подойдут для вашего конкретного случая и специфики ресурса.
В заключении стоит сказать, что конечно тема куда более обширна, и достойна еще ряда обсуждений. Такие подходы конечно, нужны как правило тем у кого уже есть написанный проект хотя бы среднего масштаба, для разделения и улучшения проекта.
Но, если вам очень нравится jQuery — нет ничего зазорного в том чтобы писать фронт на нём, ведь главное это ваш код, и если писать плохо — на любой технологии будет соответствующий результат.
Надеюсь вам было также интересно как и мне.
Успехов в кодировании!