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

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

В этой статье мы рассмотрим методы создания веб-ресурсов со стороны 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, но если вы хотите использовать это в своём проекте (неважно, будет ли это код дополняющий существующий или создание нового решения) то крайне желательно — скачать код библиотек локально. (это тема для споров, автор с ней ознакомлен)




    
    
    Список дел
    
    
    
    
    


    
    
    

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

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

    // Модель
    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 — нет ничего зазорного в том чтобы писать фронт на нём, ведь главное это ваш код, и если писать плохо — на любой технологии будет соответствующий результат.

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

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

    © Habrahabr.ru