理解前端的 MVC 服务:Angular
介绍
本文是三篇系列文章中的第三篇,旨在帮助读者理解MVC架构如何用于创建前端应用程序。本文的目标是通过逐步演进,将使用JavaScript作为脚本语言的网页开发,最终构建出一个使用JavaScript/TypeScript作为面向对象语言的应用程序,从而理解前端应用程序的架构方式。
在第三篇文章中,我们将使用 Angular 构建应用程序,而第二版则使用 TypeScript。因此,本文将介绍如何将应用程序从 TypeScript 迁移到 Angular。然而,理解应用程序各个部分之间的关系以及其结构至关重要。Angular 允许我们摆脱 DOM 的束缚,因此 user.view.ts 文件将从我们的应用程序中消失。
最后,在最后一篇文章中,我们将修改我们的代码,使其与 Angular 框架集成。
项目架构
没有什么比图片更能帮助我们理解我们将要构建的东西了,下面是一个 GIF 动画,展示了我们正在构建的应用程序。
这个应用程序可以使用单个 JavaScript 或 TypeScript 文件构建,该文件会修改文档的 DOM 并执行所有操作,但这是一种强耦合代码,并非我们在本文中打算应用的代码。
什么是MVC架构?MVC是一种包含3个层次/部分的架构:
-
模型——用于管理应用程序的数据。由于模型需要调用服务,因此它们将是不完善的(缺乏功能)。
-
视图/模板— 用户与应用程序交互的页面/GUI。
-
控制器——服务和视图之间的连接。
下面展示的是我们问题域中的文件结构:
index.html 文件将作为画布,整个应用程序将使用根元素在其上动态构建。
最后,我们的文件架构由以下 TypeScript 文件组成:
-
user.model.ts — 用户的属性(模型)。
-
user.service.ts — 管理用户的所有操作。
-
users.component.ts — 负责将服务和视图连接起来。
-
users.component.html — 负责刷新和更改显示屏幕。
应用程序模块如下所示:
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { UserService } from './shared/services/user.service';
import { UsersComponent } from './views/users/users.component';
@NgModule({
declarations: [AppComponent, UsersComponent],
imports: [BrowserModule, FormsModule, ReactiveFormsModule],
providers: [UserService],
bootstrap: [AppComponent]
})
export class AppModule {}
您可以看到我们的应用使用了三个模块:BrowserModule、FormsModule 和 reactiveFormsModule。第一个模块用于从 Angular 获取基本的结构和属性指令,而第二个和第三个模块则用于创建表单。在这个示例中,由于示例非常简洁,因此没有定义 Users 模块。您可以看到 UserService 和 UsersComponent 都包含在这个模块中。实际上,您可以将 UsersComponent 拆分为两个组件(List 和 Form),但在这个示例中,我们的目标是向您展示从 JavaScript 到 Angular 的演变过程。
模型(贫血)
本示例中构建的第一个类是应用程序模型 user.model.ts,它由类属性和一个生成随机 ID 的私有方法组成(这些 ID 可以来自服务器中的数据库)。
模型将包含以下字段:
-
id。唯一值。
-
名称。用户的名称。
-
年龄。用户的年龄。
-
完成。布尔值,用于告知是否可以将用户从列表中划掉。
User类已使用 TypeScript 进行类型定义。但是,User 构造函数接收一个普通对象,该对象将从 LocalStorage 或通过表单获取的用户数据中获取。此普通对象必须符合UserDto接口,也就是说,只有满足该接口定义的普通对象才能被实例化。
user.model.ts 文件如下所示:
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)
);
}
}
服务
对用户执行的操作都在服务中完成。服务使得模型可以做到“贫血”,因为所有逻辑负载都包含在模型中。在这个例子中,我们将使用一个数组来存储所有用户,并构建与读取、修改、创建和删除(CRUD)用户相关的四个方法。需要注意的是,服务会使用模型,将从 LocalStorage 中提取的对象实例化为 User 类。这是因为 LocalStorage 只存储数据,而不存储数据的原型。从后端传输到前端的数据也是如此,它们没有实例化自己的类。
我们类的构造函数如下:
constructor() {
const users: UserDto[] = JSON.parse(localStorage.getItem('users')) || [];
this.users = users.map(user => new User(user));
}
请注意,我们定义了一个名为 users 的类变量,用于存储所有用户,这些用户已从普通对象 ( UserDto ) 转换为 User 类的原型对象。
接下来,我们需要在服务中定义我们想要开发的每个操作。以下使用 TypeScript 展示了这些操作:
add(user: User) {
this.users.push(new User(user));
this._commit(this.users);
}
edit(userID: string, userToEdit: User) {
this.users = this.users.map(user =>
user.id === userID
? new User({
...user,
...userToEdit
})
: user
);
this._commit(this.users);
}
delete(userID: string) {
this.users = this.users.filter(({ id }) => id !== userID);
this._commit(this.users);
}
toggle(userID: string) {
this.users = this.users.map(user =>
user.id === userID
? new User({ ...user, complete: !user.complete })
: user
);
this._commit(this.users);
}
还需要定义负责将执行的操作存储到我们的数据存储(在本例中为 LocalStorage)中的提交方法。
_commit(users: User[]) {
localStorage.setItem('users', JSON.stringify(users));
}
此方法不会像使用 JavaScript 或 TypeScript 开发时那样,在创建服务时调用绑定回调函数,因为 Angular 会自动执行此操作。也就是说,Angular 会负责控制器和模板之间的绑定。
用户服务文件 user.service.ts 的内容如下:
import { User, UserDto } from "../models/user.model";
export class UserService {
public users: User[];
constructor() {
const users: UserDto[] = JSON.parse(localStorage.getItem("users")) || [];
this.users = users.map(user => new User(user));
}
_commit(users: User[]) {
localStorage.setItem("users", JSON.stringify(users));
}
add(user: User) {
this.users.push(new User(user));
this._commit(this.users);
}
edit(userID: string, userToEdit: User) {
this.users = this.users.map(user =>
user.id === userID
? new User({
...user,
...userToEdit
})
: user
);
this._commit(this.users);
}
delete(userID: string) {
this.users = this.users.filter(({ id }) => id !== userID);
this._commit(this.users);
}
toggle(userID: string) {
this.users = this.users.map(user =>
user.id === userID
? new User({ ...user, complete: !user.complete })
: user
);
this._commit(this.users);
}
}
浏览量
这是与本系列之前的文章相比变化最大的部分。在这种情况下,我们不需要直接操作 DOM,因为 Angular 会负责动态操作 DOM 这项“艰巨”的任务。但是,我们必须正确定义模板。
为本示例创建的模板(一个经过 Angular 增强的 HTML 版本)如下所示。
<h1>Users</h1>
<form [formGroup]="userForm" (ngSubmit)="add(userForm.value)">
<input
type="text"
placeholder="Name"
name="name"
formControlName="name"
/><input
type="text"
placeholder="Age"
name="age"
formControlName="age"
/><button>Submit</button>
</form>
<ul class="user-list">
<li *ngFor="let user of users">
<input type="checkbox" (change)="toggle(user)" [checked]="user.complete" />
<span>
<s *ngIf="user.complete; else uncompleteName">{{ user.name }}</s>
<ng-template #uncompleteName>{{ user.name }}</ng-template>
</span>
<span
#age
contenteditable="true"
class="editable"
(focusout)="edit(user, age)"
>
<s *ngIf="user.complete; else uncompleteAge">{{ user.age }}</s>
<ng-template #uncompleteAge>{{ user.age }}</ng-template></span
>
<button class="delete" (click)="delete(user)">Delete</button>
</li>
</ul>
本示例并非 Angular 教程,而是一系列文章,展示了 Web 应用程序从 JavaScript 到 TypeScript 再到 Angular 的演变过程。不过,我们注意到,之前文章中大量的 DOM 操作代码已被 Angular 通过提供 `*ngFor` 和 `*ngIf` 这两个结构指令来解决,从而允许直接从模板中轻松操作 DOM。
在这个例子中,Angular 帮助我们实现的另一个有趣的方面是响应式表单的使用,因为我们无需通过发送处理程序来执行任务,就将模板与控制器连接起来,而无需在双方之间建立连接。
控制器
该架构的最后一个文件是控制器(users.component.ts)。控制器通过依赖注入(DI)接收两个依赖项(服务和表单构建器)。这些依赖项存储在控制器的私有变量中。
控制器只负责管理与视图(模板)关联的属性并调用服务。这与我们之前文章中的第一个 JavaScript 代码或第二个 TypeScript 版本完全相同。在这种情况下,我们将所有与 DOM 相关的任务都交给了框架。
users.component.ts 文件如下所示:
import { Component, OnInit } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { UserService } from 'src/app/shared/services/user.service';
@Component({
selector: 'app-users',
templateUrl: './users.component.html',
styleUrls: ['./users.component.css']
})
export class UsersComponent implements OnInit {
public users;
public userForm;
constructor(
private userService: UserService,
private formBuilder: FormBuilder
) {
this.userForm = this.formBuilder.group({
name: '',
age: ''
});
}
ngOnInit() {
this.refreshUsers();
}
refreshUsers() {
this.users = this.userService.users;
}
add(userForm) {
this.userService.add(userForm);
this.refreshUsers();
this.userForm.reset();
}
delete({ id }) {
this.userService.delete(id);
this.refreshUsers();
}
edit(user, { innerText: age }) {
const { id } = user;
this.userService.edit(id, { ...user, age });
this.refreshUsers();
}
toggle({ id }) {
this.userService.toggle(id);
this.refreshUsers();
}
}
结论
在第三篇文章中,我们开发了一个 Web 应用程序,该项目按照 MVC 架构构建,其中使用了贫血模型,逻辑责任由服务承担。
非常重要的是要强调,这篇文章的教学意义在于理解项目在不同文件中的结构,以及不同文件的职责,并说明视图是如何完全独立于模型/服务和控制器的。
值得注意的是,在本文中,我们将应用程序从 TypeScript 迁移到了 Angular,从而避免了与 DOM 相关的重复性任务,这些任务在我们开发的所有 Web 应用程序中都是完全相同的。
我的建议是,先从第一篇关于 JavaScript 的文章入手,理解其中的架构。下一步是使用 TypeScript 来强化代码(见第二篇文章),最后阅读这篇将代码适配到框架中的文章。
当然,这并不是一个 Angular 教程,而是关于我们的代码从一种技术演变为一个框架的故事。
本文的GitHub分支地址为: https://github.com/Caballerog/Angular-MVC-Users
原文发表于https://www.carloscaballero.io,日期为 2019 年 11 月 12 日。
文章来源:https://dev.to/angular/understanding-mvc-services-for-frontend-angular-3e8a

