在前端中理解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源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。