带上下文的文章

现实生活中实用的 Angular 架构

面向真实实践的完整 Angular 架构索引 — 真实问题、真实模式、真实决策。没有空洞的理论。

现实生活中实用的 Angular 架构
14 Apr

现实生活中实用的 Angular 架构

面向真实实践的完整 Angular 架构索引 — 真实问题、真实模式、真实决策。没有空洞的理论。

大多数 Angular 架构课程都会教授理论。不是这个。

在这里,您将学习发现真正的问题,做出适当的决策,并构建能够随着团队成长、产品变化和时间流逝而持续存在的系统。

二十个模块。以实践为导向。无填充。


INDEX — 现实生活中实用的 Angular 架构

01 — 基础架构问题
  • 组件中的业务逻辑
  • 上帝服务
  • 重复或分散的状态
  • 特征之间的耦合
  • 混合职责
  • 文件夹看起来很整齐但不能缩放
  • 过早的抽象
  • 不必要的过度设计
  • 架构是为今天而设计的,但不是为了增长而设计的
  • 难以删除、移动或重构的代码
02 — 实际项目结构
  • 基于特征与基于图层
  • 何时使用每种方法
  • 如何检测不缩放的结构
  • 如何按域或有界上下文划分
  • 如何为真正的团队组织文件夹
  • 有用的命名约定
  • shared 中该放什么、不该放什么
  • 如何检查当前结构是否支持×10
03 — 组件架构
  • 组件太大
  • 职责过多的组件
  • 智能组件与哑组件
  • 现代 Angular 中的容器/演示
  • 成分组成
  • 何时重复使用,何时不重复使用
  • 如何设计干净的组件 API
  • 输入/输出 vs 信号 vs 服务
  • 模板中的典型问题
  • 如何检测难以维护的部件
04 — Angular 中的真实状态
  • 存在哪些类型的状态
  • 如何检测状态滥用
  • 本地状态、共享状态、全局状态、服务器状态
  • 信号与 RxJS
  • 国家安置
  • 何时使用服务状态
  • 何时使用信号存储
  • 何时使用 NgRx
  • 如何检测过度集权
  • 如何检测分布式混沌
  • 副作用中的典型错误
  • 国家正常化
  • 状态清单
05 — 通信和数据流
  • 数据下降/事件上升
  • 组件之间的正确通信
  • 组件之间的通信不正确
  • 功能之间的通信
  • 隐藏耦合的迹象
  • 使用服务进行通信
  • 使用信号进行通信
  • 当事件总线不是一个好主意时
  • 如何检查依赖地址
  • 如何检测难以跟踪的数据流
06 — 数据层和API访问
  • 服务与存储库
  • DTO 与模型
  • 转换和映射
  • 适配器放置位置
  • 使用 API 时的典型错误
  • 如何解耦前端和后端
  • 重试、缓存、分页
  • 无限滚动
  • 从架构来看 REST 与 GraphQL
  • 如何检查数据层是干净的还是混合的
07 — 路由架构
  • 有目的地设计路线
  • 路线中的 UX + SEO
  • 懒惰路线
  • 卫兵
  • 解决
  • URL 作为状态源
  • 深层链接
  • 耦合路线
  • 路线考虑不周
  • 检查导航和可扩展性的清单
08 — 渲染和全球策略
  • SPA、SSR、SSG、ISR
  • 如何根据上下文进行选择
  • SEO 与复杂性
  • 性能与成本
  • Angular SSR 架构
  • 水合作用和建筑影响
  • 何时不使用 SSR
  • 如何检查应用程序是否需要不同的渲染策略
09 — 性能架构
  • 变化检测、OnPush、信号和性能
  • 延迟加载实数
  • 代码分割和捆绑策略
  • 缓存
  • 虚拟滚动和记忆
  • 绩效预算
  • 如何检测架构瓶颈
  • “优化”之前要检查什么
  • 如何区分代码问题与架构问题
10 — 测试架构
  • 测试什么和不测试什么
  • 单元、集成、e2e
  • 设计的可测试性
  • 如何检测难以测试的代码
  • 嘲讽有内涵
  • 测试和过度测试的脆弱性
  • 合约测试前后端
  • 检查架构是否有利于或破坏测试的清单
11 — Nx 和真正的 monorepo
  • 什么时候值得,什么时候不值得
  • 应用程序与库、边界、依赖图
  • 共享库的制作有好有坏
  • 受影响、缓存、代码所有权
  • 如何扩展到多个团队
  • Monorepo 反模式
  • 审查清单
12 — 微前端和模块联合
  • 当是和当否时
  • 他们解决什么实际问题?
  • 隐性成本
  • 主机与远程、版本控制、MFE 之间的通信
  • 独立部署和组织复杂性
  • 决定是否支付的清单
13 — 有用的设计系统
  • 它们解决了什么问题?什么时候不需要解决严重的问题?
  • 组件库、标记、主题、变体
  • 一致性与灵活性
  • 故事书,设计开发同步
  • 典型错误
  • 如何检查您的 UI 系统是否正在缩放或崩溃
14 — 前端安全
  • XSS、CSRF、清理
  • 身份验证架构:JWT、刷新令牌
  • 警卫和基于角色的访问
  • SSR中的安全问题
  • 实用修订清单
15 — 可观察性和维护
  • 日志记录、错误跟踪、Sentry、指标
  • 概念追踪、功能标记、A/B 测试
  • 如何发现盲点
  • 如何检查应用程序是否可以在生产环境中运行
16 — DevEx 和平台思维
  • 开发人员经验和工具
  • CI/CD、生成器、原理图、自动化
  • 如何检测不必要的摩擦
  • 如何为团队设计架构而不仅仅是代码
17 — 权衡和决策
  • 如何证明决策的合理性
  • 成本与收益、复杂性与可扩展性、速度与可维护性
  • 建造与购买
  • 如何撰写 ADR
  • 如何在面试或实际工作中捍卫自己的决定
18 — Angular 架构师的反模式
  • 过度设计、无用的层、过早的抽象
  • shared 设计不当,不必要的全局状态
  • 交叉依赖、静默耦合
  • 糟糕的模块化,忽略真正的指标
  • 危险信号清单
19 — Angular 应用程序的实际审核
  • 如何审查现有应用程序
  • 首先要看什么
  • 哪些迹象表明债务严重
  • 如何确定问题的优先级
  • 首先要解决哪些问题,哪些还不能碰
  • 如何进行有用的架构审查
20 — 真实案例及培训
  • 电商审核
  • SaaS 仪表板审核
  • 企业后台审核
  • 带有 SEO 的公共应用程序审核
  • 从头开始的应用程序设计
  • 混乱的应用程序重新设计
  • 面试问题
  • 检测、决策和改进练习

模块 0 — 系统基础

点 0 不是内容模块。这是您将在整个路线图中使用的查看方式。

在谈论Angular中的具体问题之前,需要先回答一个问题:

你如何看待一个你不了解的应用程序并决定它是好还是坏?

为此有四种基本技能。


1. 无需接触代码即可分析应用程序

应用程序的第一次阅读不是在编辑器中。它在结构中。在打开单个文件之前,您可以提取信息:

  • 这些文件夹叫什么?这些名字说明了它们的作用吗?
  • 是否有一个文件夹 shared 比其他所有东西都重?
  • 该结构有多少个嵌套层?
  • 模块或功能是否有域名或技术名称?
  • 服务在哪里?松散还是在功能范围内?

这已经告诉您很多关于开发该应用程序的人是否从业务角度或技术层面进行思考。


2. 以实用的方式回顾架构

审查并不是从上到下阅读代码。它提出的问题有以下标准:

  • 业务逻辑在哪里?
  • 谁知道每一层都有什么?
  • 您需要更改多少个站点才能添加新功能?
  • 是否存在依赖关系走向错误的方向?

高级架构师不会从最复杂的组件开始。从最高风险点开始:共享服务、全局状态和数据访问层。


3. 编程前检测问题

大多数建筑损坏发生在看似无关紧要的早期决策中:

  • “我现在将其放在共享服务上”
  • “父组件处理这个状态,然后我们移动它”
  • “我们所有事情都使用 NgRx,所以它是一致的”

这些决定会在几个月后产生真正的影响。架构标准是了解每个决策正在购买什么以及它产生了哪些债务。


4. 将理论转化为实际清单

我们将在这个路线图中看到的每个概念——智能/哑组件、状态放置、上帝服务等——都必须以一个问题结束,你可以在审查真实代码时问自己:

  • 该组件知道数据来自哪里吗?
  • 这项服务是否有多个理由需要更改?
  • 这种状态是否存在于两个不同的地方?

如果你不能将一个概念转化为复习问题,那么你还没有将它内化。


模块 1 — 如何在不接触代码的情况下分析 Angular 应用程序

第一次架构审查发生在打开编辑器之前。仅从文件夹结构和文件名中,您就可以提取清晰的质量信号。这就是高级架构师在前 10 分钟使用未知应用程序所做的事情。

为什么这很重要?

  • 如果您花费很长时间来检测架构问题,那么纠正这些问题的成本就会呈指数级增长。
  • 从第一天起设计不佳的结构就会成为技术债务,并在几个月后阻碍团队。
  • 培养快速的视觉判断能力可以让您在编写一行代码之前做出更好的决策。

步骤 1 — 以地图形式读取文件夹结构

文件夹结构是第一个可见的架构决策。它告诉你团队如何组织他们的心理世界:他们是从技术角度还是从业务角度思考?

什么是功能?

功能是完整的业务功能。不是文件类型。不是技术层。

想想一个电子商务应用程序。特点是:

  • 产品目录
  • 购物车
  • 结账流程
  • 用户个人资料
  • 订单管理

其中每一项都是一项本身有意义的业务。用户输入、浏览目录、添加到购物车、结账。这些都是特点。

模型 1 — 按技术层组织(规模较大时存在问题)

几乎每个人开始时都会这样做,因为它看起来很简洁:

src/app/
  components/
    product-card.component.ts
    product-list.component.ts
    cart-item.component.ts
    checkout-form.component.ts
    user-profile.component.ts
  services/
    product.service.ts
    cart.service.ts
    checkout.service.ts
    user.service.ts
  models/
    product.model.ts
    cart.model.ts
    user.model.ts

真正的问题:您必须修改结账流程。要了解所涉及的内容,您可以在三个不同的文件夹之间导航 - 在 components/ 中查找组件,在 services/ 中查找服务,在 models/ 中查找模型。如果有特定的结账管道,它会与其他功能的管道混合在 pipes/ 中。

要更改单个功能,您可以浏览整个应用程序。这就是结构耦合。当团队成长时,两个致力于不同功能的人不断接触相同的文件夹 → git 中的冲突,很难知道谁拥有什么。

模型 2 — 按功能组织(规模)

src/app/
  features/
    catalog/
      components/
        product-card.component.ts
        product-list.component.ts
      services/
        product.service.ts
      models/
        product.model.ts
      catalog.routes.ts
    cart/
      components/
        cart-item.component.ts
        cart-summary.component.ts
      services/
        cart.service.ts
      cart.routes.ts
    checkout/
      components/
        checkout-form.component.ts
        order-confirmation.component.ts
      services/
        checkout.service.ts
      checkout.routes.ts
  shared/
  core/

为什么这样做:当您点击结帐时,结帐中的所有内容都会在一起。新开发人员确切地知道该去哪里寻找。一个人可以拥有一整套功能。您可以通过仅删除其文件夹来删除整个要素。 Git 不同功能之间的冲突几乎完全消失。

关键经验法则: 如果删除某个功能,您可以删除其整个文件夹而不触及其他任何内容吗?如果答案是肯定的,则该功能已得到很好的隔离。如果您必须在整个应用程序中寻找片段,事实并非如此。

第 2 步 — 读出名字

名称是文档。坏名字会隐藏意图,并给每个阅读代码的人增加认知负担。

shared/ — 它应该包含什么和不应该包含什么

shared/ 适用于多个功能使用且没有自己的业务逻辑的事物。

shared/ 中正确:

shared/
  components/
    button/
    modal/
    spinner/
    avatar/
  pipes/
    date-format.pipe.ts
    truncate.pipe.ts
  directives/
    click-outside.directive.ts
  validators/
    email-validator.ts

它们是可重用的 UI 部分或纯粹的实用程序。他们对生意一无所知。 ButtonComponent 不知道它是结账按钮还是用户个人资料按钮。他只知道如何成为一个按钮。

shared/ 中不正确:

shared/
  services/
    cart.service.ts           ← lógica de negocio aquí
    user-auth.service.ts      ← pertenece a auth/core
    product-filter.service.ts ← pertenece a catalog
    report-generator.service.ts ← pertenece a reporting

当您在 shared/ 中看到具有业务逻辑的服务时,这意味着有人不知道将它们放在哪里并将它们放在那里。随着时间的推移,shared/ 成为应用程序的包罗万象。

core/ — 它是什么和不是什么

core/ 适用于需要整个应用程序一次的单例基础设施。

core/ 中正确:

core/
  services/
    auth.service.ts
    logger.service.ts
    error-handler.service.ts
  interceptors/
    auth.interceptor.ts
    error.interceptor.ts
  guards/
    auth.guard.ts
  models/
    user-session.model.ts

core/ 中不正确:

core/
  components/
    dashboard.component.ts  ← eso es una feature
    home.component.ts       ← igual
  services/
    product.service.ts      ← pertenece a catalog
    report-generator.service.ts ← pertenece a reporting

这些是实例化一次并影响整个应用程序的东西。身份验证拦截器拦截整个应用程序的所有 HTTP 请求,这就是它位于 core/ 中的原因。

模糊的名称——危险信号

一个模糊的名字是一个推迟的决定。当有人创建 data.service.ts 时,他们尚未决定该服务正在谈论哪些数据。

你看到什么 这意味着什么
shared/ 非常大 一切不适合其他地方的东西。它变成了一个混合包。
common/ shared/ 相同,但名称更糟糕。没有进入的标准。
utils/ 提供服务 伪装成实用程序的业务逻辑。严重的危险信号。
helpers/ utils/ 相同。这个名字没有提到责任。
根中的 components/ 没有按域组织。所有组件都在一起。
core/ 有 40 个文件 核心定义不明确。 shared/ 被用作第二个。
app.service.ts 等待成长的上帝服务。最大报警信号。
data.service.ts 数据是什么?从名字上看,责任无限。
helper.service.ts 它最终成为具有不相关方法的服务。
main.component.ts 什么是“主要”?隐藏真正责任的通用名称。

命名规则: 如果文件名没有准确地告诉您它的作用,则该文件可能作用太多或位置不佳。

模糊名称及其隐藏内容的具体示例:

  • utils/format.ts 有 800 行:日期格式+表单验证+API 转换的组合。
  • helper.service.ts:最终成为具有不相关方法的上帝服务。
  • app.service.ts:称为整个应用程序的服务,负责整个应用程序。

步骤 3 — 无需打开文件即可计数和测量

在阅读代码之前,您可以直接从结构中进行这些观察:

以元件尺寸为标志

符号 它说明什么?
+300-400 行在一个组件中 你几乎总是做得太多。这不是绝对规则,但值得研究。
+10 个输入和输出 API 太复杂。候选者被分为几个组成部分。
组件中直接 HTTP 调用 将演示与数据访问混合在一起。缺少服务层。
每个功能 1-2 个组件 他们可能做得太多了。内部缺乏分工。
+20 家全球供应商 太多的状态和集中逻辑。并非所有事情都需要全球化。

全球服务 — providedIn: 'root' 的危险

全局服务意味着任何功能的任何组件都可以注入和使用它。这会在本应独立的功能之间产生不可见的依赖关系。

// Servicio que DEBERÍA ser local a la feature de catalog:
@Injectable({ providedIn: 'root' })  // ← señal de problema
export class ProductFilterService { ... }

// Ahora cualquier componente de cualquier feature puede usarlo.
// Si catalog cambia su lógica de filtrado, puede romper
// algo en una feature aparentemente no relacionada.

第 4 步 — app.module.tsapp.config.ts:要查找的内容

在带有模块的应用程序中(Angular < 17 或旧版本)

@NgModule({
  imports: [
    BrowserModule,
    HttpClientModule,
    RouterModule,
    AuthModule,       // ← bien, infraestructura singleton

    ProductModule,    // ← señal de problema
    CartModule,       // ← señal de problema
    CheckoutModule,   // ← señal de problema
    UserModule,       // ← señal de problema
    DashboardModule,  // ← señal de problema
    // Si hay 15+ módulos de features aquí,
    // el lazy loading no está funcionando.
  ],
  providers: [
    ProductService,   // ← si hay 20+ providers aquí,
    CartService,      // hay demasiada centralización
    UserService,
    AuthService,
  ]
})

功能模块应该延迟加载(按需),而不是直接导入到根模块中。如果它们都在这里,即使用户从不需要它们,它们也会在启动时加载。

在独立应用程序中(Angular 17+)

// app.config.ts
export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes, withLazyLoading()),   // ← correcto
    provideHttpClient(withInterceptors([authInterceptor])),
    provideAnimations(),

    ProductService,   // ← esto NO debería estar aquí
    CartService,      // ← pertenece a la feature de cart
  ]
}

全局配置规则: 全局配置应该只具有影响整个应用程序的基础设施:具有延迟加载的路由器、具有全局拦截器的 HttpClient、动画、基础设施单例服务(身份验证、记录器、错误处理程序)。如果您在全局配置中看到要素服务,则表明有人在全局注册事物是为了方便,而不是必要。

完整清单 — 初读 Angular 应用程序

结构

  • 结构是按功能(业务领域)还是按技术层组织的?
  • 我可以通过仅删除其文件夹而不触及其他任何内容来删除整个功能吗?
  • 功能是否具有业务域名(checkoutcatalog)或技术名称(listformdetail)?
  • 是否存在名称模糊的文件夹:utilshelperscommonmiscdata

shared/core/

  • shared/ 是否只包含 UI 组件和实用程序而没有业务逻辑?
  • core/ 是否仅包含单例基础设施(拦截器、守卫、身份验证服务)?
  • shared/ 内是否有具有业务逻辑的服务?
  • core/ 是否有功能组件或领域服务?

服务和提供商

  • 业务服务是在您的功能范围内还是在全球范围内松散?
  • 全球供应商是否超过 20 家?
  • 根模块或 app.config.ts 是否直接导入功能模块(不使用惰性)?
  • 是否有带有 providedIn: 'root' 的服务应该是某个功能的本地服务?

姓名

  • 文件名是否准确表达了它们的作用?
  • 是否存在其名称可以应用于应用程序任何部分的文件?
  • 是否有名为 app.service.tsdata.service.tshelper.service.ts 的文件?
  • shared/ 是否比任何单个特征更重要?

### 锻炼

在 GitHub 上搜索任何公共 Angular 项目 - 它可以是您自己的项目,也可以是开源项目。不打开任何文件,只查看文件夹结构和名称:

  1. 您使用什么组织模型:功能层还是技术层?
  2. 哪些名字让你产生疑问或怀疑? 3.您怀疑业务逻辑在哪里?
  3. 您认为哪个文件夹的职责最多?
  4. 你能删除一个特征而不触及其他任何东西吗?

分享您在会议中观察到的内容,我们会像高级架构师一样一起审查它。


提示:用 4 点分析您的项目

复制此提示并将其与任何 AI(ChatGPT、Claude、Copilot)一起使用,将其传递给项目的文件夹树:


应用第一支柱的这 4 点来分析项目架构并生成报告。

第 1 点 — 文件夹结构

分析 apps/libs/ 并回答:

  • 组织是按功能(业务领域)还是按技术层划分?
  • 您可以通过仅删除其文件夹来删除整个功能吗?
  • 这些功能是否具有域名(checkoutcatalog)或技术名称(listformdetail)?
  • 是否有名称模糊的文件夹:utilshelperscommonmiscdata
  • 显示您找到的实际文件夹树。

第 2 点 — 文件和文件夹名称

搜索整个项目并列出:

  • 名称模糊的文件:data.service.tshelper.service.tsapp.service.tsmain.component.tsutils,其中包含服务。
  • 业务逻辑在 shared/ 内的服务。
  • core/ 内的组件或域服务。
  • shared/ 是否比任何单独的功能更重要?文件计数。

第 3 点 — 全球服务

搜索整个项目:

  • 所有带有 providedIn: 'root' 的服务 — 列出它们是什么以及它们是否应该是某个功能的本地服务。
  • 统计全球供应商总数。
  • 识别应在您的功能范围内的全球注册业务服务。
  • libs/services/ 中搜索明显属于特定域的服务。

第 4 点 — 全局配置(app.config.tsapp.module.ts

找到各个app的根配置文件并分析:

  • 什么是不应该在全球范围内注册的?
  • 功能模块是延迟加载还是直接导入?
  • 全局配置中有业务服务吗?
  • 全球共有多少家注册提供商?

报告格式

对于每个点,使用这个精确的结构:

✅ 有什么好的

(具体列表与真实代码示例)

⚠️ 需要检查的标志

(带有文件路径的具体列表以及为什么它是一个标志)

❌ 明确问题

(具体列表包含文件路径、问题和实际影响)

最后它包括:

## 执行摘要 一张包含 4 个点、一个状态表情符号 (✅ ⚠️ ❌) 和每个点一条结论线的表格。

后续步骤的优先顺序

最多 5 个特定操作的列表(按影响排序),以及要触摸的文件或文件夹的确切路径。

直接一点。不要解释什么是特征或理论。只需分析这个具体项目并给出真实的发现即可。

如果你读了一篇文章,它应该给你一些可以直接应用的东西。

Angular 中真实的 SEO:添加 SSR 之前先修复什么