在 Angular 中“结构与约定”是什么意思
Angular 常被描述为 有意见的(opinionated)。在框架语境下,这意味着它不仅提供构建模块——还推荐(有时会强制)具体的组装方式。你会被引导采用特定的文件布局、模式、工具和约定,这样即便不同团队构建的两个 Angular 项目也会有相似的“感觉”。
有意见并不等于“限制”——而是“决断”
Angular 的这些意见体现在如何创建组件、如何组织功能、默认如何使用依赖注入以及通常如何配置路由。与其让你在多种相互竞争的方案中选择,Angular 缩小了推荐选项的范围。
这种权衡是刻意为之:
更少决策负担: 你可以少花时间争论架构、文件夹结构、状态边界或构建配置。
更少自由度: 如果你非常偏好另一种风格,Angular 可能会让你觉得有阻力。
为什么大型应用更重视一致性而非灵活性
小型应用可以容忍实验:不同的编码风格、为同一任务使用多种库,或随时间演化的临时模式。大型 Angular 应用——尤其是那些多年维护的应用——为这种灵活性付出的代价很高。在大规模代码库中,最难的问题往往是协调问题:如何让新人快速上手、快速审查拉取请求、在重构时保证安全,以及让众多功能协同工作。
Angular 的结构旨在让这些活动变得可预测。当模式一致时,团队可以自信地在不同功能间切换,把更多精力放到产品工作上,而不是重复学习“这一部分是怎么做的”。
本文将覆盖的内容
接下来的文章将拆解 Angular 的结构来自何处——它的架构选择(组件、模块/独立组件、DI、路由)、工具链(Angular CLI),以及这些意见如何支持团队协作和大规模长期维护。
为什么大型应用会推动框架走向约定优先
小型应用能在很多“行得通就好”的决策下幸存。大型 Angular 应用通常不能。一旦多个团队共同维护同一代码库,微小的不一致会累积成真实成本:重复的工具函数、略有差异的文件夹结构、相互竞争的状态管理模式,以及三种处理同一 API 错误的方法。
团队扩展:防止代码漂移
随着团队扩大,人们自然而然会复制身边看到的实现。如果代码库没有清晰地指示首选模式,结果就是代码漂移——新功能遵循的是最后一个开发者的习惯,而不是共享的做法。
约定减少了每个功能需要做出的决策数量。这缩短了入职时间(新员工在你的仓库中就能学会“Angular 的方式”),并降低了审查摩擦(少了诸如“这不符合我们的模式”之类的评论)。
长期存续的应用:为变化优化
企业前端很少是“做完就不管”的。它们经历维护周期、重构、重设计和持续的功能变更。在这种环境中,结构更像是求生之道而非美学:
可预测的文件组织让所有权和导航更简单。
一致的边界让重构更安全(你知道逻辑应该放在哪里)。
标准模式减少了需求变更时的返工。
横切关注点在没有标准时无法扩展
大规模应用不可避免地共享横切关注点:路由、权限、本地化、测试以及与后端的集成。如果每个功能团队都以不同方式解决这些问题,你最终会在调试交互上浪费时间,而不是构建产品。
Angular 在模块/独立组件边界、依赖注入默认行为、路由和工具链方面的意见,旨在默认让这些关注点保持一致。回报很直接:更少的特例、更少的返工,以及多年来更顺畅的协作。
组件模型:可预测的构建块
Angular 的核心单元是组件:它是具有清晰边界的自包含 UI 单元。当产品变大时,这些边界防止页面变成“所有东西都互相影响”的大文件。组件让你明确一个功能所在、它负责什么(模板、样式、行为)、以及它如何被复用。
模板 + 类:一个职责,两部分
组件分为模板(描述用户所见的 HTML)和类(用 TypeScript 保存状态和行为)。这种分离鼓励呈现与逻辑之间的清晰划分:
模板专注于渲染和绑定。
类专注于数据、事件处理和协调。
// user-card.component.ts
@Component({ selector: 'app-user-card', templateUrl: './user-card.component.html' })
export class UserCardComponent {
@Input() user!: { name: string };
@Output() selected = new EventEmitter\u003cvoid\u003e();
onSelect() { this.selected.emit(); }
}
\u003c!-- user-card.component.html --\u003e
\u003ch3\u003e{{ user.name }}\u003c/h3\u003e
\u003cbutton (click)=\"onSelect()\"\u003eSelect\u003c/button\u003e
Inputs/Outputs = 可预测的数据流
Angular 推崇一个直观的组件间契约:
@Input() 把数据从父传到子。
@Output() 把事件从子发到父。
这个约定使数据流很容易推理,尤其在大型 Angular 应用中,多支团队会触及相同界面。打开一个组件时,你可以快速辨别:
它期望什么数据
它可能发出哪些事件
它负责渲染什么
有助于团队加速的约定
由于组件遵循一致的模式(选择器、文件命名、装饰器、绑定),开发者能一眼认出结构。这个共享的“形状”减少了交接摩擦,加速了审查,并让重构更安全——无需每个人记住每个功能的定制规则。
模块与功能组织以支持扩展
当应用增长时,最难的问题往往不是写新功能,而是找出把它们放在哪里并理解谁“拥有”它。Angular 倾向于结构化,以便团队可以在不停地协商约定的情况下继续前进。
NgModules 与 standalone:两种划分边界的方式
历史上,NgModules 把相关组件、指令和服务组合成功能边界(例如 OrdersModule)。现代 Angular 也支持独立组件(standalone),它们减少了对 NgModules 的需求,同时仍通过路由和文件夹结构鼓励明确的“功能切片”。
不论哪种方式,目标都是一致的:让功能易于发现并使依赖有意为之。
按功能分组支持所有权与导航
一种常见且可扩展的模式是按功能组织,而不是按类型组织:
features/orders/(订单相关页面、组件、服务)
features/billing/
features/admin/
当每个功能文件夹包含了大部分所需内容时,开发者可以打开某个目录并迅速理解该区域的工作方式。这也很自然地映射到团队所有权:“Orders 团队负责 features/orders 下的所有内容”。
Core vs Shared vs Feature(以及“万能 shared 模块”陷阱)
Angular 团队常把可复用代码拆分为:
Core: 应用范围的单例与基础设施(认证、拦截器、全局服务)。
Shared: 多个功能复用的 UI 片段和工具函数。
Feature: 领域特定逻辑,不应随意泄露。
常见错误是把 shared/ 变成垃圾场。如果“shared”导入了所有东西且人人都引用“shared”,依赖会纠缠在一起,构建时间会增长。更好的做法是让 shared 保持小、聚焦并且低依赖性。
Angular 如何推动一致性
在模块/独立组件边界、依赖注入默认设置和基于路由的功能入口点之间,Angular 自然地把团队推向可预测的文件夹布局和更清晰的依赖图——这些都是使大型 Angular 应用可维护的关键要素。
依赖注入作为默认架构