在前端中理解MVC服务之 VanillaJS篇
介绍
这是这个系列的第一篇,该系列文章将了解MVC架构如何建立前端应用。本系列文章的目的是了解如何经过使用JavaScript为面向对象的主要语言来构建一个MVC的应用。
在第一篇文章中,将使用VanillaJS构建该应用程序。所以,本文将是开发与DOM相关的代码最多的地方。可是,了解应用程序的全部部分之间的关系以及其结构很是重要。
在第二部分中,咱们将经过将JavaScript代码转换为TypeScript版原本加强其代码。
最后,在最后一部分中,咱们将转换代码以将其与Angular框架集成。javascript
项目架构
在咱们开始作项目以前,首先得要了解咱们的应用,能够经过下图,来了解咱们所需构建的程序。css
咱们可使用单个JS文件来构建该项目,该文件能够修改文档中的DOM并执行全部操做。
html
什么是MVC架构?
MVC 架构是一个具备三个层/部分的体系前端
Model -管理应用的数据,这些模型将是不可见的,由于它们将被引用于服务。java
View 模型的直观表示,即用户所看到的部分node
Controller - Model与View中的连接git
下面,咱们来展现一下文件结构:
该index.html
文件将充当画布,使用root元素将在其上动态构建整个应用程序。此外,此文件将充当全部文件的加载器,由于它们将连接到html文件自己中。
最后,咱们的文件架构由如下JavaScript文件组成:github
user.model.js —用户的属性(模型)数据库
user.controller.js —负责加入Service和View的部分后端
user.service.js —管理用户的全部操做
user.views.js —负责刷新和更改显示屏幕
HTML文件以下所示:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>User App</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div id="root"></div>
<script src="models/user.model.js"></script>
<script src="services/user.service.js"></script>
<script src="controllers/user.controller.js"></script>
<script src="views/user.view.js"></script>
<script src="app.js"></script>
</body>
</html>
Models
在该项目中将构建的第一个Class类是应用Models user.model.js
,
它由类属性和一个随机ID(这些ID可能来自服务器上的数据库)的私有方法组成。
这些模型应该有如下字段:
id 惟一值
name 用户名
age 用户年龄
complete bool值,能够知道此条数据是否有用
在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)
);
}
}
因为全部的逻辑都负载在Model中,所以,服务端可使模型缺少。在这种特定的状况下,咱们将使用数组的方式来存储全部的用户数据,并构建一个关于用户信息的CRUD四种方法。
应该注意的是,该服务使用的Model,而且实例化的是从提取对象LocalStorage
中的User Class
。这是由于LocalStorage
仅仅只存储数据,而不存储数据的原型,从后端传到前端的数据也会所以发生没有实例化其Class的状况。
咱们的Class构造函数以下:
constructor() {
const users = JSON.parse(localStorage.getItem('users')) || [];
this.users = users.map(user => new User(user));
}
而这里定义了一个名为users
的类变量,用于在将全部数据中的用户从平面对象转换成 Users
这个Class的原型对象后再存储。
而下一个咱们必须在服务端中定义的下一件事是咱们必需要操做的开发。下面咱们来使用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);
}
咱们仍然须要定义 commit
方法来负责将执行的操做存储在咱们的数据存储区中 (使用Localstorage
)
bindUserListChanged(callback) {
this.onUserListChanged = callback;
}
_commit(users) {
this.onUserListChanged(users);
localStorage.setItem('users', JSON.stringify(users));
}
从callback
的定义中能够看出,这个方法在建立服务时调用已绑定的函数bindUserListChanged
。该函数是来自View的函数,它负责刷新屏幕上的用户数据列表:
在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);
}
}
View是Model的视觉表现,咱们将动态建立整个视图,而不是建立一个HTML内容并注入它。咱们首先要作的是经过DOM的方法来缓存视图的全部变量,如View构造函数所示:
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();
}
这一个视图的下一个最相关点是View与Service方法的结合,例如 bingAddUser
方法将驱动程序功能做为参数接收,该参数将执行addUser
的操做。在这些bindXXX
的方法中,绑定了XXX方法,定义了EventListener
的View控件,请注意,从View中,咱们能够访问用户经过屏幕所提供的全部数据,这些数据将经过handler
函数来链接。
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);
}
});
}
而View中的其余代码经过处理文档中的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);
}
});
}
}
在这里构建的最后一个文件就是Controller了.Controller
经过关系依赖注入(DI)接收它所具备的两个依赖关系(servier 和 model)。这些依赖项存储在Controller的私有变量中。另外,因为控制器是访问双方的惟一元素,所以,构造函数在View和Service之间创建了显式连接。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的启动器。咱们一般将它命名为app.js
。该程序是建立不一样的组件:UserService
、UserView
、UserController
。
const app = new UserController(new UserService(), new UserView());
总结
在这一篇文章中,咱们开发了一个Web应用,其中的结构是按照MVC的架构来构造的,其中使用了anemic models而且逻辑使用了Service。
强调这一点的学习是很是重要的,这是为了了解具备不一样任务的不一样文件在项目中的结构,以及视图如何彻底独立于模型/服务端和控制器。
在下一部分中,咱们将使用TypeScript来加强JavaScript,这将为咱们提供更强大的语言来开发Web应用程序。咱们使用JavaScript使咱们编写了许多冗长而重复的代码来管理DOM(若是使用Angular框架将使这种状况最小化)。
这篇文章的GitHub分支是
https://github.com/Caballerog/VanillaJS-MVC-Users。
本文原文来源于 Medium
做者:Carlos Caballero
本文采用了意译并稍加修改
本文分享自微信公众号 - 壹前端(yiqianduan)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。