简介
这篇文章是系列文中的第二篇,旨在了解 MVC 体系结构如何创建前端应用程序。
通过开发一个网页应用来理解构建前端应用的方法,其中,使用JavaScript作为脚本语言,并转向使用JavaScript/TypeScript作为面向对象程序开发的语言
在这一篇文章中,将使用第一个版本的TypeScript来构建应用程序,因此,本文将上次的程序由VanillaJS迁移到TypeScript中,但是,了解应用程序的所有部分以及如何构建它才是本文的重中之重。最后,在最后一篇文章中,我们将转换我们的代码,将其与Angular框架集成。
项目架构:
可以参照上次的文章,可以理解我们所需要构建的内容。
MVC 架构是一个具有三个层/部分的体系
下面,我们列出了项目中的文件结构 该文件将作为一个画布,整个应用将使用 “元素动态构建”。此外,此文件将充当所有文件的加载程序,因为它们将在 HTML 文件本身中链接。最后,我们的文件体系结构由以下 TypeScript 文件组成:
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="css/style.min.css" />
</head>
<body>
<div id="root"></div>
</body>
<script src="bundle.js"></script>
</html>
我们可以看到的是,只有一个调用的文件已链接,而这个文件bundle.js
将在TypeScript转换到JS并最小化应用之后生成。
我们不会专注于构建应用的工具,因为我们将负责用gulpfile
来执行项目所有的转换任务
在这种情况下我们决定使用gulp
工具,当然,如果用webpack也是可以的。
如果你知道JS,你能够读懂它的代码的意思,并且你能够几乎完全的理解我们所执行的任务,在我们的这个案例中,我们使用browserity
插件来打包、创建模块系统并执行TS到JS的转换。
const gulp = require('gulp');
const browserify = require('browserify');
const source = require('vinyl-source-stream');
const buffer = require('vinyl-buffer');
const sourcemaps = require('gulp-sourcemaps');
const concat = require('gulp-concat');
const minifyCSS = require('gulp-minify-css');
const autoprefixer = require('gulp-autoprefixer');
const useref = require('gulp-useref');
const rename = require('gulp-rename');
const { server, reload } = require('gulp-connect');
gulp.task('watch', function() {
gulp.watch('src/**/*.ts', gulp.series('browserify'));
gulp.watch('src/**/*.html', gulp.series('html'));
gulp.watch('src/**/*.css', gulp.series('css'));
});
gulp.task('html', function() {
return gulp
.src('src/*.html')
.pipe(useref())
.pipe(gulp.dest('dist'))
.pipe(reload());
});
gulp.task('css', function() {
return gulp
.src('src/**/*.css')
.pipe(minifyCSS())
.pipe(autoprefixer('last 2 version', 'safari 5', 'ie 8', 'ie 9'))
.pipe(concat('style.min.css'))
.pipe(gulp.dest('dist/css'))
.pipe(reload());
});
gulp.task('images', function() {
gulp.src('src/**/*.jpg').pipe(gulp.dest('dist'));
return gulp.src('src/**/*.png').pipe(gulp.dest('dist'));
});
gulp.task('serve', () => {
server({
name: 'Dev Game',
root: './dist',
port: 5000,
livereload: true,
});
});
gulp.task('browserify', function() {
return browserify({
entries: './src/app.ts',
})
.plugin('tsify')
.bundle()
.on('error', function(err) {
console.log(err.message);
})
.pipe(source('bundle.js'))
.pipe(buffer())
.pipe(sourcemaps.init({ loadMaps: true }))
.pipe(sourcemaps.write('./'))
.pipe(gulp.dest('dist'))
.pipe(reload());
});
gulp.task(
'default',
gulp.series(['browserify', 'html', 'css', 'images', gulp.parallel('serve', 'watch')]),
);
这个示例中的第一个生成Class是Model,它由Class属性和生成随机ID组成 user.model.ts
模型将具有以下字段:
Interface
接口,以便任何纯对象都不能实例化,而是满足定义的接口的对象。User UserLocalStorage UserDto
在 user.model.ts
文件中写下以下代码:/**
* @class Model
*
* Manages the data of the application.
*/
export interface UserDto {
name: string;
age: string;
complete: boolean;
}
export class User {
public id: string;
public name: string;
public age: string;
public complete: boolean;
constructor(
{ name, age, complete }: UserDto = {
name: null,
age: null,
complete: false
}
) {
this.id = this.uuidv4();
this.name = name;
this.age = age;
this.complete = complete;
}
uuidv4(): string {
return (([1e7] as any) + -1e3 + -4e3 + -8e3 + -1e11).replace(
/[018]/g,
(c: number) =>
(
c ^
(crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))
).toString(16)
);
}
}
对用户执行的操作在服务中执行。该服务允许Model为贫血模式,因为所有的逻辑负载都在其中。
在此特定情况下,我们将使用数组来存储所有用户,并生成与读取、修改、创建和删除 (CRUD) 用户关联的四种方法。
应该注意的是,服务使用模型,实例化从类提取的对象。这是因为只存储数据,而不是存储数据的原型。从后端到前端的数据也是如此 , 它们没有实例化其类。
我们类的构造函数如下所示:
constructor() {
const users: UserDto[] = JSON.parse(localStorage.getItem('users')) || [];
this.users = users.map(user => new User(user));
}
我们定义了一个名为“类变量”的Class变量,该变量在所有用户从纯对象转换为类的原型对象后存储它们。
在服务中我们必须定义的是我们想要创建的每个操作。使用 TypeScript 如下所示:
add(user: User) {
this.users.push(new User(user));
this._commit(this.users);
}
edit(id: string, userToEdit: User) {
this.users = this.users.map(user =>
user.id === id
? new User({
...user,
...userToEdit
})
: user
);
this._commit(this.users);
}
delete(_id: string) {
this.users = this.users.filter(({ id }) => id !== _id);
this._commit(this.users);
}
toggle(_id: string) {
this.users = this.users.map(user =>
user.id === _id ? new User({ ...user, complete: !user.complete }) : user
);
this._commit(this.users);
}
它仍有待定义负责存储在我们的数据存储中执行的操作的方法
bindUserListChanged(callback: Function) {
this.onUserListChanged = callback;
}
_commit(users: User[]) {
this.onUserListChanged(users);
localStorage.setItem('users', JSON.stringify(users));
}
此方法调用在创建 zervice 时绑定的函数,如方法的定义中所示。我已经可以告诉你,callbackbindUserListChanged
是从视图产生的功能,并负责刷新屏幕上的用户列表。
user.service.ts
文件如下所示:
import { User, UserDto } from '../models/user.model';
/**
* @class Service
*
* Manages the data of the application.
*/
export class UserService {
public users: User[];
private onUserListChanged: Function;
constructor() {
const users: UserDto[] = JSON.parse(localStorage.getItem('users')) || [];
this.users = users.map(user => new User(user));
}
bindUserListChanged(callback: Function) {
this.onUserListChanged = callback;
}
_commit(users: User[]) {
this.onUserListChanged(users);
localStorage.setItem('users', JSON.stringify(users));
}
add(user: User) {
this.users.push(new User(user));
this._commit(this.users);
}
edit(id: string, userToEdit: User) {
this.users = this.users.map(user =>
user.id === id
? new User({
...user,
...userToEdit
})
: user
);
this._commit(this.users);
}
delete(_id: string) {
this.users = this.users.filter(({ id }) => id !== _id);
this._commit(this.users);
}
toggle(_id: string) {
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();
}
视图的下一个最相关的点是View与Service (将通过Controller发送)的结合。例如,bindAddUseraddUser
接收一个驱动程序函数作为参数,该参数将执行服务中描述的操作。在方法中,将定义每个视图控件的 。请注意,从视图中,我们可以访问用户提供的所有数据,这些数据通过函数连接。 bindXXXEventListenerhandler
bindAddUser(handler: Function) {
this.form.addEventListener('submit', event => {
event.preventDefault();
if (this._nameText) {
handler({
name: this._nameText,
age: this._ageText
});
this._resetInput();
}
});
}
bindDeleteUser(handler: Function) {
this.userList.addEventListener('click', event => {
if ((event.target as any).className === 'delete') {
const id = (event.target as any).parentElement.id;
handler(id);
}
});
}
bindEditUser(handler: Function) {
this.userList.addEventListener('focusout', event => {
if (this._temporaryAgeText) {
const id = (event.target as any).parentElement.id;
const key = 'age';
handler(id, { [key]: this._temporaryAgeText });
this._temporaryAgeText = '';
}
});
}
bindToggleUser(handler: Function) {
this.userList.addEventListener('change', event => {
if ((event.target as any).type === 'checkbox') {
const id = (event.target as any).parentElement.id;
handler(id);
}
});
}
视图的其他代码将处理文档的 DOM。user.view.ts
文件如下所示:
import { User } from '../models/user.model';
/**
* @class View
*
* Visual representation of the model.
*/
interface Input {
key: string;
type: string;
placeholder: string;
name: string;
}
export class UserView {
private app: HTMLElement;
private form: HTMLElement;
private submitButton: HTMLElement;
private inputName: HTMLInputElement;
private inputAge: HTMLInputElement;
private title: HTMLElement;
private userList: HTMLElement;
private _temporaryAgeText: string;
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 }: Input = {
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: string, className?: string) {
const element = document.createElement(tag);
if (className) element.classList.add(className);
return element;
}
getElement(selector: string): HTMLElement {
return document.querySelector(selector);
}
displayUsers(users: User[]) {
// 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') as HTMLInputElement;
checkbox.type = 'checkbox';
checkbox.checked = user.complete;
const spanUser = this.createElement('span');
const spanAge = this.createElement('span') as HTMLInputElement;
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 as any).className === 'editable') {
this._temporaryAgeText = (event.target as any).innerText;
}
});
}
bindAddUser(handler: Function) {
this.form.addEventListener('submit', event => {
event.preventDefault();
if (this._nameText) {
handler({
name: this._nameText,
age: this._ageText
});
this._resetInput();
}
});
}
bindDeleteUser(handler: Function) {
this.userList.addEventListener('click', event => {
if ((event.target as any).className === 'delete') {
const id = (event.target as any).parentElement.id;
handler(id);
}
});
}
bindEditUser(handler: Function) {
this.userList.addEventListener('focusout', event => {
if (this._temporaryAgeText) {
const id = (event.target as any).parentElement.id;
const key = 'age';
handler(id, { [key]: this._temporaryAgeText });
this._temporaryAgeText = '';
}
});
}
bindToggleUser(handler: Function) {
this.userList.addEventListener('change', event => {
if ((event.target as any).type === 'checkbox') {
const id = (event.target as any).parentElement.id;
handler(id);
}
});
}
}
在我们的项目中,最后一个文件就是Controller,它将通过依赖注入(DI)来接受其具有的Service
和View
服务项 这些依赖项存储在控制器中的私有变量中。此外,构造函数使视图和服务之间的显式连接,因为控制器是唯一可以访问双方的元素。 user.controller.ts
文件如下所示:
import { User } from '../models/user.model';
import { UserService } from '../services/user.service';
import { UserView } from '../views/user.view';
/**
* @class Controller
*
* Links the user input and the view output.
*
* @param model
* @param view
*/
export class UserController {
constructor(private userService: UserService, private 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: User[]) => {
this.userView.displayUsers(users);
};
handleAddUser = (user: User) => {
this.userService.add(user);
};
handleEditUser = (id: string, user: User) => {
this.userService.edit(id, user);
};
handleDeleteUser = (id: string) => {
this.userService.delete(id);
};
handleToggleUser = (id: string) => {
this.userService.toggle(id);
};
}
我们应用程序的最后一点是应用程序启动器。在我们的案例中,我们称它为app.ts
,应用程序通过创建不同的元素来执行:UserService
、UserView
和UserController
如app.ts
文件中所示:
import { UserController } from './controllers/user.controller';
import { UserService } from './services/user.service';
import { UserView } from './views/user.view';
const app = new UserController(new UserService(), new UserView());
在第二篇文章中,我们开发了一个 Web 应用程序,其中项目的结构遵循 MVC 体系结构,其中使用了贫血模型,逻辑的责任在于Service。
了解不同文件中具有不同职责的项目结构以及视图如何完全独立于Model/Service和Controller非常重要。还必须注意,在本文中,我们将应用程序从 JavaScript 迁移到 TypeScript,从而允许我们获取类型化代码,帮助开发人员最大限度地减少错误并了解其每个部分的作用。
在本系列的下一篇文章中,我们将将 TypeScript 代码迁移到 Angular。这种迁移到框架将意味着我们不必处理使用 DOM 的复杂性和重复性。
这篇文章的GitHub分支位于 https://github.com/Caballerog/TypeScript-MVC-Users
本文来源于 Medium 作者:Carlos Caballero 转载请注明出处!