Введение
Этот пост является первым в серии из трех постов, которые поймут, как архитектура 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”