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

角斗士的七宗罪

角斗士的七宗罪

封面原图由Nick Gavrilov拍摄,来自Unsplash。

Angular 以其鲜明的风格和规范性而闻名。尽管如此,它和其他任何技术一样,也存在一些致命的缺陷。本文将回顾 Angular 应用中最常见且最致命的错误。您将学习如何改正这些错误,从而避免您的 Angular 灵魂遭受永恒的诅咒。

本文中,我们创建了一个评级系统,根据每种 Angular 错误的影响程度及其对 Angular 代码库的具体影响,对其进行分类。我们根据这些错误的影响程度来评估其价值:

  • 可能存在漏洞
  • 可维护性
  • 建筑学
  • 表现
  • 可扩展性
  • 捆绑包大小
  • 无障碍
  • 代码重用

#7:急切加载所有功能

在我们的应用程序中不使用延迟加载是一个巨大的罪过,尤其因为延迟加载

  • 很简单
  • 内置
  • 显著提升了性能和网络使用率。

简而言之,在适用的情况下使用延迟加载,方法是将应用程序仔细地划分为逻辑上合理的模块,每个模块包含相关的逻辑,然后延迟加载这些模块。

修改:要么使用 Angular Router 的延迟加载功能,要么使用类似函数的动态导入语句。

#6:按类型对类进行分组

我们经常在 Angular 应用的代码库中看到名为 services、pipes、directives 和 components 的文件夹。表面上看,这似乎很合理:毕竟,如果我要查找某个服务,在名为services 的文件夹下查找它似乎合情合理。但实际上,这会带来几个问题:

  • 类型分组文件夹最终变成了杂乱无章的抽屉,里面堆满了不相关的类,难以浏览。
  • 使用该服务的组件在开发过程中需要访问非常远的文件夹。这违反了邻近原则,该原则指出,经常同时更改的文件应该放在彼此靠近的位置。
  • 这会降低应用程序的可扩展性:如果所有服务、指令、管道和组件都放在同一个目录中,那就意味着需要进行更多的重构。

那么我们该如何解决这个问题呢?以下是一些建议:

  • 首先按特征分组,然后按图层分组,最后可能按类型分组。
  • 如果某个服务与 Angular 模块相关,请先将其放置在该模块中。
  • 如果模块足够大,可以考虑创建子模块。
  • 那么,最基本的模块可以有一个services文件夹,其中只包含与该模块相关的服务。

一个相关的例子是包含子模块的管理模块,这些子模块允许用户管理公司及其关联的用户。很自然地,我们会创建一个“用户”模块和一个“公司”模块,并在各自的模块中提供“UserService”和“CompanyService”服务。但现在假设我们需要在用户详情页面显示一个包含公司名称的下拉列表,以便将该用户添加为某个公司的员工。显然,我们需要使用“CompanyService”服务,但它位于“CompanyModule”模块中。因此,我们需要将其移至“AdminModule”模块,以便两个模块都能访问它。之后,我们将在所有类似的场景中进行类似的重构。

这里有一个不错的文件夹结构,类似于这个示例中前端架构的良好方法:

├───app
│ │ app-routing.module.ts
│ │ app.component.ts
│ │ app.module.ts
│ │
│ ├───admin
│ │ │ admin.component.ts
│ │ │ admin.module.ts
│ │ │ admin.routing.ts
│ │ │
│ │ ├───companies
│ │ │ companies.component.ts
│ │ │ companies.module.ts
│ │ │ companies.routing.ts
│ │ │
│ │ │───services
│ │ │ companies.service.ts
│ │ │
│ │ └───users
│ │ │ users.component.ts
│ │ │ users.module.ts
│ │ │ users.routing.ts
│ │
│ │───services
│ │ users.service.ts
│ │
│ └───common
│ │ common.module.ts
│ │
│ ├───directives
│ │ error-highlight.directive.ts
│ │
│ ├───pipes
│ │ includes.pipe.ts
│ │
│ └───services
│ local-storage.service.ts
Enter fullscreen mode Exit fullscreen mode

您可以在这里找到示例应用程序

#5:手动订阅可观察对象

本质上,手动订阅 Observable 意味着执行命令式逻辑。那么,为什么有人会手动订阅 Observable 呢?如果不是为了执行命令式操作,那么手动订阅就毫无意义。如果我们能用 RxJS 操作符以更声明式的方式表达相同的意思,那么就没有必要订阅 Observable;我们可以直接使用 `Observable` AsyncPipe。但是,请注意,AsyncPipe`Observable` 并不处理错误和代码补全。经验法则:只有当需要执行无法以其他方式完成的命令式操作时,才应该手动订阅 Observable。一个非常常见的例子是FormControl根据 RxJS 流的最新输出来启用/禁用 Observable。这只能使用FormControl`Observable` 的 ` enable`/`disable`方法来实现,而这些方法本身就是命令式的,因此需要手动订阅。

第四部分:庞大而毛茸茸的部件

想象一下,整个 Angular 应用都只用一个组件实现。你是不是在笑?我们见过这种情况。这种做法之所以被视为致命错误,同样适用于规模较小的组件。你是不是每个功能或每个页面都用一个组件?你做错了!

如果把所有功能都放在一个组件里,Angular 的性能就会受到很大影响,因为每次修改都会导致所有数据绑定重新评估和脏检查。更糟糕的是,你会把这种难以维护的烂摊子留给你的同事或者未来的自己。

组件体积过大的原因有很多。例如,它可能承担了过多的职责。理想情况下,组件应该是轻量级的封装层,将用户交互、应用程序事件与用户界面粘合在一起。

所以本质上,我们的组件应该做某些事情,也不应该做某些事情。以下是组件应该做的一些事情:

  • 使用 DOM
  • 显示来自商店/服务的数据
  • 处理其生命周期事件
  • 管理表单(模板驱动/响应式)
  • 用户交互
  • 向子组件传递数据

组件应该做的事情:

  • 直接加载数据
  • 修改全局状态
  • 直接操作存储(cookie、localStorage 等)
  • 直接使用实时连接(WebSocket 等)
  • 处理自定义的 DOM 相关场景(例如,高亮显示无效输入)。这些场景可以提取到服务中,以提高可重用性。

变体:大型、毛茸茸的服务

  • 有时我们无法正确地组织我们的服务。
  • 通常情况下,处理外部数据(例如通过 HTTP 加载的数据)的服务应该按功能进行排序。
  • 但有时逻辑会混淆。例如,一个名为ArticleService 的服务可能会发出 HTTP 请求来创建/更新书签或标签。这显然违反了单一职责原则。ArticleService 应该执行的操作包括:数据库添加文章、删除文章、获取/排序/筛选文章列表,本质上就是 CRUD 操作(创建、读取、更新、删除)。
  • 为避免出现这种情况,请始终根据服务处理的数据特征对服务进行分类,不要将它们与提供抽象层的服务(例如第三方库的适配器)混为一谈。

#3:将复杂逻辑放入组件模板中

虽然声明式组件模板很好用,但它们不应该用于复杂的逻辑,无论是展示型逻辑还是其他类型的逻辑。严格的模板类型检查可以避免诸如类型错误或拼写错误之类的低级错误。

将逻辑放在组件模板中会强制你通过 DOM 进行测试。组件测试比单元测试慢,因为组件模板需要编译,而且需要进行大量设置。此外,放在组件模板中的逻辑无法重用。

至少,要将组件模板中的逻辑提取到组件模型中。

不过,最好将所有逻辑都提取到服务中。展示型逻辑应该放在展示器(Presenter)中,非展示型逻辑则应该放在其他服务类型中。更多相关内容,请参阅第4 节:庞大而复杂的组件。

#2:将所有声明放在 AppModule 中

坦白说,模块可能是 Angular 中最受诟病的特性。它们难以向新手解释,有时难以维护,而且总体上容易造成混乱。因此,一个非常糟糕的做法就是将所有导入/导出/声明都直接放在根AppModule中。这不仅违反了关注点分离原则,而且随着应用程序的复杂程度增加, AppModule也会变得异常臃肿。但幸运的是,这个问题有一个相对简单的解决方案。

  1. 创建要素模块并将不同的要素组件声明分离到其中。
  2. 对于不同模块使用的组件/管道/指令/服务,请创建共享模块。

但如果我们从第二点开始,它也可能变得有点罪恶。

变体:在 SharedModule 中放置过多声明

为了避免这种情况,我们也可以开始对功能模块内部的依赖项进行分组。例如,如果我们有一个包含UserModuleAccountModule 的AdminModule ,而这两个模块都使用名为 ManagementService 的服务我们可以将该服务移到AdminModule内部,而不是整个应用程序模块中;这样,功能模块就可以拥有自己的共享模块。

#1:使用命令式编程和默认变更检测

有些错误是可以理解的。尽管 Angular 基于 RxJS 构建,但它本身仍然鼓励命令式编程:状态是一个我们可以随意修改的对象,Angular 的变更检测机制会相应地更新 DOM。但这种方法存在诸多问题:

  • 命令式编程过于冗长且难以理解;通常需要阅读整段代码才能明白数据状态是如何改变的。
  • 命令式编程围绕着状态的改变而构建:同一引用下的对象会不断发生改变,这可能会成为奇怪的 bug 的持续来源:你的状态发生了改变,但你却不知道是如何改变的,也不知道是从哪里改变的!
  • 默认的 Angular 变更检测机制虽然效率尚可,但仍然存在许多不必要的步骤,我们可以轻松跳过这些步骤。

有几种方法可以弥补这种罪过:

  • 最重要的是,摒弃命令式编程,转而采用声明式编程;遵循函数式编程的最佳实践;编写纯函数;力求清晰明确;使用组合;避免不良编程实践。
  • 更多地使用 RxJS Observables 和运算符,并开始将你的状态及其变化描述为流。
  • 停止手动修改数据,切换到ChangeDetectionStrategy.OnPush,并将 Observables 与异步管道结合使用。
  • 此外,还可以考虑使用像 NGRX 这样的状态管理系统。

结论

开发前端应用程序时,有很多环节都可能出错;本指南旨在展示开发者在使用 Angular 时最常犯的错误。希望通过本指南,您在检查应用程序并纠正这些错误后,最终能够获得更具可扩展性、更易于理解和维护的代码库。

文章来源:https://dev.to/this-is-angular/7-deadly-sins-of-angular-1n2j