发布于 2026-01-05 6 阅读
0

理解前端的 MVC 服务:Angular

理解前端的 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 {}
Enter fullscreen mode Exit fullscreen mode

您可以看到我们的应用使用了三个模块: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)
    );
  }
}

Enter fullscreen mode Exit fullscreen mode

服务

对用户执行的操作都在服务中完成。服务使得模型可以做到“贫血”,因为所有逻辑负载都包含在模型中。在这个例子中,我们将使用一个数组来存储所有用户,并构建与读取、修改、创建和删除(CRUD)用户相关的四个方法。需要注意的是,服务会使用模型,将从 LocalStorage 中提取的对象实例化为 User 类。这是因为 LocalStorage 只存储数据,而不存储数据的原型。从后端传输到前端的数据也是如此,它们没有实例化自己的类。

我们类的构造函数如下:

constructor() {
  const users: UserDto[] = JSON.parse(localStorage.getItem('users')) || [];
  this.users = users.map(user => new User(user));
}
Enter fullscreen mode Exit fullscreen mode

请注意,我们定义了一个名为 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);
  }
Enter fullscreen mode Exit fullscreen mode

还需要定义负责将执行的操作存储到我们的数据存储(在本例中为 LocalStorage)中的提交方法。

_commit(users: User[]) {
  localStorage.setItem('users', JSON.stringify(users));
}
Enter fullscreen mode Exit fullscreen mode

此方法不会像使用 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);
  }
}
Enter fullscreen mode Exit fullscreen mode

浏览量

这是与本系列之前的文章相比变化最大的部分。在这种情况下,我们不需要直接操作 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>
Enter fullscreen mode Exit fullscreen mode

本示例并非 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();
  }
}
Enter fullscreen mode Exit fullscreen mode

结论

在第三篇文章中,我们开发了一个 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