Рубрики
Без рубрики

Понимание MVC-сервисов для Frontend: vanillajs

Этот пост является первым в серии из трех постов, которые поймут, как архитектура MVC работает для создания фронтальных приложений. Цель этой серии сообщений-понять, как структурировать приложение, развивая веб-страницу, на которой JavaScript используется в качестве языка сценариев в отношении приложения, в котором JavaScript используется в качестве объектно-ориентированного языка. Tagged с JavaScript, MVC.

Введение

Этот пост является первым в серии из трех постов, которые поймут, как архитектура MVC работает для создания фронтальных приложений. Цель этой серии сообщений-понять, как структурировать приложение, развивая веб-страницу, на которой JavaScript используется в качестве языка сценариев в отношении приложения, в котором JavaScript используется в качестве объектно-ориентированного языка.

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

Во второй статье мы укрепим код JavaScript, преобразуя его в версию TypeScript.

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

Архитектура проекта

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

Это приложение может быть построено с использованием одного файла JavaScript, который изменяет DOM документа и выполняет все операции, но это тесно связанный код и не то, что мы намерены применить в этом посте.

Что такое архитектура MVC? MVC – это архитектура с 3 слоями/деталями:

  • Модели – Управление данными приложения. Модели будут анемичными (им не хватает функциональных возможностей), поскольку они будут направлены на услуги.

  • Просмотры – Визуальное представление моделей.

  • Контроллеры – Ссылки между услугами и представлениями.

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

Файл index.html будет действовать как холст, на котором все приложение будет динамически построено с помощью корневого элемента. Кроме того, этот файл будет действовать как загрузчик всех файлов, поскольку они будут связаны в самом файле HTML.

Наконец, наша архитектура файлов состоит из следующих файлов JavaScript:

  • user.model.js – Атрибуты (модель) пользователя.

  • user.controller.js – тот, кто отвечает за присоединение к сервису и виду.

  • user.service.js – Управлять всеми операциями на пользователях.

  • user.views.js – Отвечает за освежение и изменение экрана дисплея.

HTML -файл – это то, что показано ниже:




  
    
    
    

    User App

    
  

  
    

Модели (анемия)

Первым классом, встроенным в этот пример, является модель приложения, user.model.js, которая состоит из атрибутов класса, и частный метод, который генерирует случайные идентификаторы (эти идентификаторы могут поступать из базы данных на сервере).

Модели будут иметь следующие поля:

  • id Анкет Уникальное значение.

  • имя Анкет Имя пользователей.

  • возраст Анкет Возраст пользователей.

  • Завершите Анкет Boolean, которая позволяет вам узнать, сможем ли мы вычеркнуть пользователя из списка.

User.model.js показан ниже:

/**
 * @class Model
 *
 * Manages the data of the application.
 */

class User {
  constructor({ name, age, complete } = { complete: false }) {
    this.id = this.uuidv4();
    this.name = name;
    this.age = age;
    this.complete = complete;
  }

  uuidv4() {
    return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c =>
      (
        c ^
        (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))
      ).toString(16)
    );
  }
}

Операции, выполняемые для пользователей, выполняются в службе. Сервис – это то, что позволяет моделям быть анемичными, так как вся логическая нагрузка находится в них. В этом конкретном случае мы будем использовать массив для хранения всех пользователей и создавать четыре метода, связанные с чтением, изменением, созданием и удалением (CRUD) пользователей. Вы должны отметить, что служба использует модель, создавая объекты, извлеченные из LocalStorage в класс пользователя. Это связано с тем, что LocalStorage только хранит данные, а не прототипы сохраненных данных. То же самое происходит с данными, которые перемещаются от бэкэнд на фронт, у них нет создания классов.

Конструктор нашего класса заключается в следующем:

  constructor() {
    const users = JSON.parse(localStorage.getItem('users')) || [];
    this.users = users.map(user => new User(user));
  }

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

Следующим, что мы должны определить в службе, будет каждая из операций, которые мы хотим разработать. Эти операции показаны ниже с использованием ECMascript, без использования одной строки в TypeScript:

add(user) {
    this.users.push(new User(user));

    this._commit(this.users);
  }

  edit(id, userToEdit) {
    this.users = this.users.map(user =>
      user.id === id
        ? new User({
            ...user,
            ...userToEdit
          })
        : user
    );

    this._commit(this.users);
  }

  delete(_id) {
    this.users = this.users.filter(({ id }) => id !== _id);

    this._commit(this.users);
  }

  toggle(_id) {
    this.users = this.users.map(user =>
      user.id === _id ? new User({ ...user, complete: !user.complete }) : user
    );

    this._commit(this.users);
  }

Еще предстоит определить метод совершения, который отвечает за хранение операции, выполняемой в нашем хранилище данных (в нашем случае LocalStorage).

  bindUserListChanged(callback) {
    this.onUserListChanged = callback;
  }

  _commit(users) {
    this.onUserListChanged(users);
    localStorage.setItem('users', JSON.stringify(users));
  }

Этот метод вызывает функцию обратного вызова, которая была приготовлена при создании услуги, как это можно увидеть в определении BindUserListChanged метод Я уже могу сказать вам, что этот обратный вызов – функция, которая поступает из представления и отвечает за обновление списка пользователей на экране.

Файл user.service.js выглядит следующим образом:

/**
 * @class Service
 *
 * Manages the data of the application.
 */
class UserService {
  constructor() {
    const users = JSON.parse(localStorage.getItem('users')) || [];
    this.users = users.map(user => new User(user));
  }

  bindUserListChanged(callback) {
    this.onUserListChanged = callback;
  }

  _commit(users) {
    this.onUserListChanged(users);
    localStorage.setItem('users', JSON.stringify(users));
  }

  add(user) {
    this.users.push(new User(user));

    this._commit(this.users);
  }

  edit(id, userToEdit) {
    this.users = this.users.map(user =>
      user.id === id
        ? new User({
            ...user,
            ...userToEdit
          })
        : user
    );

    this._commit(this.users);
  }

  delete(_id) {
    this.users = this.users.filter(({ id }) => id !== _id);

    this._commit(this.users);
  }

  toggle(_id) {
    this.users = this.users.map(user =>
      user.id === _id ? new User({ ...user, complete: !user.complete }) : user
    );

    this._commit(this.users);
  }
}

Представление является визуальным представлением модели. Вместо того, чтобы создавать контент HTML и вводить его (как это делается во многих структурах), мы решили динамически создать весь представление. Первое, что должно быть сделано, – это кэшировать все переменные представления через методы DOM, как показано в конструкторе представления:

constructor() {
    this.app = this.getElement('#root');

    this.form = this.createElement('form');
    this.createInput({
      key: 'inputName',
      type: 'text',
      placeholder: 'Name',
      name: 'name'
    });
    this.createInput({
      key: 'inputAge',
      type: 'text',
      placeholder: 'Age',
      name: 'age'
    });

    this.submitButton = this.createElement('button');
    this.submitButton.textContent = 'Submit';

    this.form.append(this.inputName, this.inputAge, this.submitButton);

    this.title = this.createElement('h1');
    this.title.textContent = 'Users';
    this.userList = this.createElement('ul', 'user-list');
    this.app.append(this.title, this.form, this.userList);

    this._temporaryAgeText = '';
    this._initLocalListeners();
  }

Следующей наиболее важной точкой зрения является союз представления с методами обслуживания (который будет отправлен через контроллер). Например, метод BindAdduser получает функцию драйвера в качестве параметра, который будет выполнять операцию AddUser, описанную в службе. В методах Bindxxx определяется eventlistener каждого из элементов управления представления. Обратите внимание, что из представления у нас есть доступ ко всем данным, предоставленным пользователем с экрана; которые связаны через функции обработчика.

 bindAddUser(handler) {
    this.form.addEventListener('submit', event => {
      event.preventDefault();

      if (this._nameText) {
        handler({
          name: this._nameText,
          age: this._ageText
        });
        this._resetInput();
      }
    });
  }

  bindDeleteUser(handler) {
    this.userList.addEventListener('click', event => {
      if (event.target.className === 'delete') {
        const id = event.target.parentElement.id;

        handler(id);
      }
    });
  }

  bindEditUser(handler) {
    this.userList.addEventListener('focusout', event => {
      if (this._temporaryAgeText) {
        const id = event.target.parentElement.id;
        const key = 'age';

        handler(id, { [key]: this._temporaryAgeText });
        this._temporaryAgeText = '';
      }
    });
  }

  bindToggleUser(handler) {
    this.userList.addEventListener('change', event => {
      if (event.target.type === 'checkbox') {
        const id = event.target.parentElement.id;

        handler(id);
      }
    });
  }

Остальная часть кода представления проходит обработку DOM документа. Файл user.view.js выглядит следующим образом:

/**
 * @class View
 *
 * Visual representation of the model.
 */
class UserView {
  constructor() {
    this.app = this.getElement('#root');

    this.form = this.createElement('form');
    this.createInput({
      key: 'inputName',
      type: 'text',
      placeholder: 'Name',
      name: 'name'
    });
    this.createInput({
      key: 'inputAge',
      type: 'text',
      placeholder: 'Age',
      name: 'age'
    });

    this.submitButton = this.createElement('button');
    this.submitButton.textContent = 'Submit';

    this.form.append(this.inputName, this.inputAge, this.submitButton);

    this.title = this.createElement('h1');
    this.title.textContent = 'Users';
    this.userList = this.createElement('ul', 'user-list');
    this.app.append(this.title, this.form, this.userList);

    this._temporaryAgeText = '';
    this._initLocalListeners();
  }

  get _nameText() {
    return this.inputName.value;
  }
  get _ageText() {
    return this.inputAge.value;
  }

  _resetInput() {
    this.inputName.value = '';
    this.inputAge.value = '';
  }

  createInput(
    { key, type, placeholder, name } = {
      key: 'default',
      type: 'text',
      placeholder: 'default',
      name: 'default'
    }
  ) {
    this[key] = this.createElement('input');
    this[key].type = type;
    this[key].placeholder = placeholder;
    this[key].name = name;
  }

  createElement(tag, className) {
    const element = document.createElement(tag);

    if (className) element.classList.add(className);

    return element;
  }

  getElement(selector) {
    return document.querySelector(selector);
  }

  displayUsers(users) {
    // Delete all nodes
    while (this.userList.firstChild) {
      this.userList.removeChild(this.userList.firstChild);
    }

    // Show default message
    if (users.length === 0) {
      const p = this.createElement('p');
      p.textContent = 'Nothing to do! Add a user?';
      this.userList.append(p);
    } else {
      // Create nodes
      users.forEach(user => {
        const li = this.createElement('li');
        li.id = user.id;

        const checkbox = this.createElement('input');
        checkbox.type = 'checkbox';
        checkbox.checked = user.complete;

        const spanUser = this.createElement('span');

        const spanAge = this.createElement('span');
        spanAge.contentEditable = true;
        spanAge.classList.add('editable');

        if (user.complete) {
          const strikeName = this.createElement('s');
          strikeName.textContent = user.name;
          spanUser.append(strikeName);

          const strikeAge = this.createElement('s');
          strikeAge.textContent = user.age;
          spanAge.append(strikeAge);
        } else {
          spanUser.textContent = user.name;
          spanAge.textContent = user.age;
        }

        const deleteButton = this.createElement('button', 'delete');
        deleteButton.textContent = 'Delete';
        li.append(checkbox, spanUser, spanAge, deleteButton);

        // Append nodes
        this.userList.append(li);
      });
    }
  }

  _initLocalListeners() {
    this.userList.addEventListener('input', event => {
      if (event.target.className === 'editable') {
        this._temporaryAgeText = event.target.innerText;
      }
    });
  }

  bindAddUser(handler) {
    this.form.addEventListener('submit', event => {
      event.preventDefault();

      if (this._nameText) {
        handler({
          name: this._nameText,
          age: this._ageText
        });
        this._resetInput();
      }
    });
  }

  bindDeleteUser(handler) {
    this.userList.addEventListener('click', event => {
      if (event.target.className === 'delete') {
        const id = event.target.parentElement.id;

        handler(id);
      }
    });
  }

  bindEditUser(handler) {
    this.userList.addEventListener('focusout', event => {
      if (this._temporaryAgeText) {
        const id = event.target.parentElement.id;
        const key = 'age';

        handler(id, { [key]: this._temporaryAgeText });
        this._temporaryAgeText = '';
      }
    });
  }

  bindToggleUser(handler) {
    this.userList.addEventListener('change', event => {
      if (event.target.type === 'checkbox') {
        const id = event.target.parentElement.id;

        handler(id);
      }
    });
  }
}

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

Файл user.controller.js – это то, что показано ниже:

/**
 * @class Controller
 *
 * Links the user input and the view output.
 *
 * @param model
 * @param view
 */
class UserController {
  constructor(userService, userView) {
    this.userService = userService;
    this.userView = userView;

    // Explicit this binding
    this.userService.bindUserListChanged(this.onUserListChanged);
    this.userView.bindAddUser(this.handleAddUser);
    this.userView.bindEditUser(this.handleEditUser);
    this.userView.bindDeleteUser(this.handleDeleteUser);
    this.userView.bindToggleUser(this.handleToggleUser);

    // Display initial users
    this.onUserListChanged(this.userService.users);
  }

  onUserListChanged = users => {
    this.userView.displayUsers(users);
  };

  handleAddUser = user => {
    this.userService.add(user);
  };

  handleEditUser = (id, user) => {
    this.userService.edit(id, user);
  };

  handleDeleteUser = id => {
    this.userService.delete(id);
  };

  handleToggleUser = id => {
    this.userService.toggle(id);
  };
}

Последним пунктом нашего приложения является пусковая установка приложения. В нашем случае мы назвали это app.js Анкет Приложение выполняется путем создания различных элементов: Userservice , Userview и USERCONTROLLER , как показано в файле app.js .

const app = new UserController(new UserService(), new UserView());

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

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

В следующей статье мы будем подкреплять JavaScript с использованием TypeScript, который даст нам более мощный язык для разработки веб -приложений. Тот факт, что мы использовали JavaScript, заставил нас написать много словесного и повторяющегося кода для управления DOM (это будет минимизировано с помощью угловой структуры).

GitHub Branch этого поста https://github.com/caballerog/vanillajs-mvc-users

Оригинал: “https://dev.to/carlillo/understanding-mvc-services-for-frontend-vanillajs-335h”