WinddSnow

Angular-Project-Structure-and-NgModule

字数统计: 4.5k阅读时长: 18 min
2026/08/01

第115课:Angular 项目结构与 NgModule——模块化、组件声明、导出、bootstrap

Angular 是一个完整的框架——它不仅提供了组件化 UI 的能力,还内置了模块系统、依赖注入、路由、HTTP 客户端和测试工具。与 React 和 Vue 的渐进式哲学不同,Angular 在项目初始化时就提供了标准化的目录结构清晰的职责划分。理解 Angular 的核心架构概念——特别是 NgModule(模块)是如何组织和装配代码的——是进入 Angular 世界的第一道门槛。NgModule 是 Angular 特有的概念,它将相关的组件、指令、管道和服务聚合到一个编译上下文中,定义它们的编译范围依赖注入作用域。本节课将带你从 Angular CLI 生成的项目骨架出发,逐一拆解根模块、特性模块、共享模块的职责与配置方式,以及组件如何在模块中声明、导出和启动。


1. Angular CLI 与项目初始化

1.1 安装 Angular CLI 与创建项目

1
2
npm install -g @angular/cli
ng new my-angular-app --style=scss --routing=true --standalone=false

参数说明

  • --style=scss:使用 SCSS 作为样式预处理器。
  • --routing=true:自动生成路由模块(app-routing.module.ts)。
  • --standalone=false:使用传统的 NgModule 模式(而非 Angular 17+ 的默认 Standalone 模式)。本课程以 NgModule 模式为主,因为大量现有企业项目仍在使用它,且它更清晰地展示了 Angular 模块系统的运作方式。

1.2 项目骨架结构

生成后的项目核心目录如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
my-angular-app/
├── src/
│ ├── app/
│ │ ├── app.component.ts # 根组件(TS 逻辑)
│ │ ├── app.component.html # 根组件模板
│ │ ├── app.component.scss # 根组件样式
│ │ ├── app.module.ts # 根模块
│ │ └── app-routing.module.ts # 根路由模块
│ ├── assets/ # 静态资源(图片、字体)
│ ├── environments/ # 环境配置
│ ├── index.html # 入口 HTML
│ ├── main.ts # 入口 TypeScript 文件
│ └── styles.scss # 全局样式
├── angular.json # Angular CLI 项目配置
├── package.json
└── tsconfig.json

入口启动流程

  1. main.ts 调用 platformBrowserDynamic().bootstrapModule(AppModule) 启动应用。
  2. AppModule(根模块)是 Angular 应用的编译和装配入口。它告诉 Angular:“这个应用由哪些组件、哪些模块、哪些服务组成”。
  3. AppModule 中通过 bootstrap 数组声明启动组件(通常是 AppComponent)。
  4. AppComponent 的模板中渲染了应用的主视图,并通过 <router-outlet> 承载路由切换的内容。

2. NgModule 的核心概念

NgModule 是 Angular 特有的逻辑编译单元。它的设计目的是将相关的代码组织在一起,同时定义它们的可见性边界——哪些组件/指令/管道在本模块内可用,哪些可以被其他模块导入使用。

2.1 @NgModule 装饰器的配置项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
import { HeaderComponent } from './header/header.component';
import { SharedModule } from './shared/shared.module';

@NgModule({
// 1. 声明:本模块拥有的组件、指令、管道
declarations: [
AppComponent,
HeaderComponent,
],

// 2. 导入:本模块依赖的其他模块
imports: [
BrowserModule, // 浏览器平台基础模块(仅根模块导入)
FormsModule, // 模板驱动表单模块
SharedModule, // 自定义的共享模块
],

// 3. 导出:允许其他模块使用的组件/指令/管道
exports: [
// 通常为共享模块中的组件、或重新导出的其他模块
],

// 4. 提供者:注册服务(依赖注入)
providers: [
// 服务类
],

// 5. 启动组件:仅在根模块中配置
bootstrap: [
AppComponent, // Angular 启动时创建的初始组件
],
})
export class AppModule {}

五个核心元数据项解析

元数据项 作用 仅根模块特有?
declarations 声明本模块拥有的组件、指令、管道。它们在本模块内部可直接使用。
imports 导入其他模块,使本模块可以访问它们 exports 中暴露的组件/指令/管道。
exports 导出本模块的组件/指令/管道(或重新导出其他模块),供其他模块导入使用。
providers 注册服务提供者(依赖注入容器会在该模块层级创建服务的实例)。
bootstrap 声明应用的根启动组件。Angular 会在 DOM 中动态创建该组件。 (仅根模块)

2.2 组件必须被声明在一个且仅一个模块中

Angular 的每个组件(以及指令、管道)必须被声明在恰好一个 NgModule 中。如果组件 A 在 AppModule 中被声明,它就不能同时在 SharedModule 中被声明。如果需要跨模块共享组件 A,应将其声明在 SharedModule 中,并在 exports 中导出,然后其他模块通过 imports: [SharedModule] 来间接使用。

1
2
3
4
5
6
7
8
9
10
// ❌ 错误:同一个组件在两个模块中声明
// AppModule.declarations = [HeaderComponent];
// SharedModule.declarations = [HeaderComponent];

// ✅ 正确:在 SharedModule 中声明并导出,其他模块导入 SharedModule
@NgModule({
declarations: [HeaderComponent],
exports: [HeaderComponent],
})
export class SharedModule {}

2.3 根模块与特性模块的职责分工

模块类型 职责 示例
根模块 启动应用、导入 BrowserModule、声明根组件、配置核心服务。 AppModule
特性模块 封装某个业务领域的功能(如用户管理、订单处理)。 UserModuleOrderModule
共享模块 聚合可复用的组件、指令、管道,供多个特性模块导入。 SharedModule
路由模块 仅负责路由配置,通常与对应的特性模块配对。 AppRoutingModule

3. declarations:组件、指令与管道的注册

当一个组件(或指令、管道)被创建后,它必须被添加到某个 NgModule 的 declarations 数组中,否则 Angular 无法识别它,会在模板中报错 'xxx' is not a known element

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// user.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { UserListComponent } from './user-list/user-list.component';
import { UserDetailComponent } from './user-detail/user-detail.component';

@NgModule({
declarations: [
UserListComponent,
UserDetailComponent,
],
imports: [
CommonModule, // 提供 *ngIf、*ngFor 等常用指令
],
})
export class UserModule {}

关键规则

  • 每个组件必须属于恰好一个 declarations 数组。
  • 本模块 declarations 中的组件,在本模块的所有模板中都可以直接使用,无需额外导入。
  • 如果组件需要在其他模块中被使用,必须通过 exports 数组导出,且其他模块必须导入该模块。

4. exports:控制对外可见的 API

exports 数组定义了本模块对外暴露的组件、指令和管道。只有被导出的内容,才能在其他导入本模块的模块中使用。一个常见的误解是将服务也放在 exports 中——服务不需要通过 exports 导出,它们通过依赖注入系统跨模块共享。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// shared.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ButtonComponent } from './button/button.component';
import { CardComponent } from './card/card.component';

@NgModule({
declarations: [
ButtonComponent,
CardComponent,
],
imports: [CommonModule],
exports: [
ButtonComponent, // 显式导出:其他模块导入 SharedModule 后可使用 <app-button>
CardComponent, // 显式导出
CommonModule, // 重新导出模块:其他模块导入 SharedModule 后自动获得 *ngIf/*ngFor 等指令
],
})
export class SharedModule {}

重新导出模块:在 exports 中包含其他模块(如 CommonModule),可以让导入本模块的其他模块自动获得这些被重新导出的模块中的指令和管道。这是一种简化导入的常用模式——特性模块只需导入 SharedModule,即可同时获得 ButtonComponentCommonModule 的能力。


5. bootstrap:应用启动的根组件

bootstrap 数组仅在根模块中有效。它指定了 Angular 应用启动时应创建的初始组件。Angular CLI 生成的项目中,bootstrap 通常只包含 AppComponent

1
2
3
4
5
6
7
// app.module.ts
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule, AppRoutingModule],
bootstrap: [AppComponent], // 启动组件
})
export class AppModule {}

启动过程

  1. main.ts 调用 platformBrowserDynamic().bootstrapModule(AppModule)
  2. Angular 加载 AppModule,解析其依赖。
  3. Angular 读取 bootstrap 数组,创建 AppComponent 实例。
  4. AppComponent 的模板被渲染到 index.html 中的 <app-root></app-root> 标签内。
  5. 如果模板中包含 <router-outlet>,Angular Router 接管后续的页面切换。

bootstrap 可以包含多个组件(例如同时启动桌面和移动端根组件),但绝大多数应用只有一个根组件。


6. BrowserModuleCommonModule 的区别

  • BrowserModule:提供在浏览器平台运行所需的指令和依赖。它导入并重新导出CommonModule,并额外注册了浏览器特有的服务(如 TitleMetaDOCUMENT)。
  • **CommonModule**:提供 Angular 的基础模板指令(*ngIf*ngFor*ngSwitch[ngClass][ngStyle] 等)和管道(datecurrencyuppercase 等)。

关键规则BrowserModule 只能被导入一次——在根模块中。所有其他模块(特性模块、共享模块)应导入 CommonModule。如果在多个模块中导入 BrowserModule,会导致编译错误或运行时不正常行为。


7. 综合实战:构建一个简单的 Angular 应用骨架

以下代码展示了如何从零搭建一个包含根模块、一个特性模块(UserModule)和一个共享模块(SharedModule)的 Angular 应用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
// ---- shared/shared.module.ts ----
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { CardComponent } from './card/card.component';

@NgModule({
declarations: [CardComponent],
imports: [CommonModule],
exports: [CardComponent, CommonModule], // 重新导出 CommonModule
})
export class SharedModule {}

// ---- shared/card/card.component.ts ----
import { Component, Input } from '@angular/core';

@Component({
selector: 'app-card',
template: `<div class="card"><h3>{{ title }}</h3><ng-content></ng-content></div>`,
styles: [`.card { border: 1px solid #ddd; border-radius: 8px; padding: 16px; }`],
})
export class CardComponent {
@Input() title: string = '';
}

// ---- user/user.module.ts ----
import { NgModule } from '@angular/core';
import { SharedModule } from '../shared/shared.module';
import { UserListComponent } from './user-list/user-list.component';

@NgModule({
declarations: [UserListComponent],
imports: [SharedModule], // 导入 SharedModule 获得 CardComponent 和 CommonModule
})
export class UserModule {}

// ---- user/user-list/user-list.component.ts ----
import { Component } from '@angular/core';

@Component({
selector: 'app-user-list',
template: `
<app-card title="用户列表">
<ul>
<li *ngFor="let user of users">{{ user }}</li>
</ul>
</app-card>
`,
})
export class UserListComponent {
users = ['Alice', 'Bob', 'Charlie'];
}

// ---- app.module.ts ----
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { UserModule } from './user/user.module';

@NgModule({
declarations: [AppComponent],
imports: [BrowserModule, UserModule],
bootstrap: [AppComponent],
})
export class AppModule {}

// ---- app.component.ts ----
import { Component } from '@angular/core';

@Component({
selector: 'app-root',
template: `
<h1>Angular 应用骨架</h1>
<app-user-list></app-user-list>
`,
})
export class AppComponent {}

关键解析

  • SharedModule 声明并导出了 CardComponent,同时重新导出了 CommonModule。任何导入 SharedModule 的模块都自动获得 *ngFor 等指令。
  • UserModule 导入 SharedModule 后,在其模板中可直接使用 <app-card>
  • 根模块导入 BrowserModuleUserModule,并启动 AppComponent
  • CardComponent 使用 <ng-content>(内容投影,类似 Vue 的插槽),父组件可以将内容投影到卡片内部。

8. Standalone Components:Angular 的未来方向

Angular 17+ 引入了 Standalone Components 模式,允许组件脱离 NgModule 独立使用。在 Standalone 模式中,组件通过 standalone: true 标记自身,并直接在 imports 中声明其依赖的指令、管道和其他模块。这简化了 Angular 的架构,减小了 NgModule 带来的样板代码。

1
2
3
4
5
6
7
8
9
10
11
// Standalone Component(Angular 17+)
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';

@Component({
selector: 'app-standalone-card',
standalone: true,
imports: [CommonModule], // 直接声明依赖
template: `<div class="card"><ng-content></ng-content></div>`,
})
export class StandaloneCardComponent {}

本课程仍以 NgModule 模式为主,因为:

  • 大量遗留项目和现有企业 Angular 应用使用 NgModule 架构。
  • 理解 NgModule 对于阅读和改造旧项目至关重要。
  • Standalone 模式与 NgModule 可以共存,掌握了 NgModule 后迁移到 Standalone 非常容易。

课后练习

一、概念自测(选择题 / 填空题)

  1. (单选) 在 Angular 中,@NgModule 装饰器的哪个属性用于声明本模块拥有的组件和指令?
    A. imports
    B. providers
    C. declarations
    D. exports

  2. (单选) 以下关于 BrowserModuleCommonModule 的描述,正确的是?
    A. BrowserModule 应该被导入到每个特性模块中。
    B. CommonModule 提供 *ngIf*ngFor 等基础指令。
    C. BrowserModuleCommonModule 功能完全相同。
    D. CommonModule 只能被导入一次,与 BrowserModule 互斥。

  3. (填空) 要在其他模块中使用本模块中的组件,必须在 @NgModule______ 数组中显式导出该组件。

  4. (多选) 关于 Angular 的根模块(AppModule),哪些描述是正确的?
    A. bootstrap 数组仅在根模块中有效。
    B. 根模块必须导入 BrowserModule
    C. 根模块中可以声明多个 bootstrap 组件。
    D. 根模块的 declarations 可以包含来自其他模块的组件。

二、AI 编程任务:编写面向 AI 的提示词

场景:你需要为 Angular 项目搭建一个基础架构,包含以下模块:

  • SharedModule:声明并导出 AlertComponent(显示一条消息,通过 @Input() message 接收)和 ButtonComponent(显示一个按钮,通过 @Input() label@Output() clicked 分别接收标签和发出点击事件)。同时重新导出 CommonModule
  • DashboardModule(特性模块):导入 SharedModule,声明 DashboardComponent,在其中使用 AlertComponentButtonComponent
  • 根模块 AppModule:导入 BrowserModuleDashboardModule,启动 AppComponent

任务要求:请写出一段完整的中文提示词,发送给 AI,使其生成上述所有模块和组件的完整 TypeScript 代码。提示词中需明确指定每个模块的 declarationsimportsexports 配置,以及组件的 @Input/@Output 用法。

三、Agent 模式下的提示词示例

你是一个资深前端开发 Agent。请为 Angular 项目创建基础模块架构。需要创建以下文件:

  1. src/app/shared/shared.module.tsdeclarations 包含 AlertComponentButtonComponentimports 包含 CommonModuleexports 包含 AlertComponentButtonComponentCommonModule
  2. src/app/shared/alert/alert.component.tsselector: 'app-alert'@Input() message: string = ''@Input() type: 'info' | 'success' | 'error' = 'info'。模板使用 *ngIf 控制显示,根据 type 设置不同背景色。
  3. src/app/shared/button/button.component.tsselector: 'app-button'@Input() label: string = ''@Input() disabled: boolean = false@Output() clicked = new EventEmitter<void>()。模板中 <button (click)="clicked.emit()">{{ label }}</button>
  4. src/app/dashboard/dashboard.module.tsimports: [SharedModule]declarations: [DashboardComponent]
  5. src/app/dashboard/dashboard.component.ts:模板中使用 <app-alert [message]="'欢迎回来!'" [type]="'success'"></app-alert><app-button [label]="'查看详情'" (clicked)="handleClick()"></app-button>。类中包含 handleClick() 方法,打印 '按钮被点击'
  6. src/app/app.module.tsimports: [BrowserModule, DashboardModule]declarations: [AppComponent]bootstrap: [AppComponent]
  7. src/app/app.component.ts:模板中仅包含 <h1>Angular 应用</h1><app-dashboard></app-dashboard>
    所有组件使用内联模板和样式。完成后列出所有文件内容。

四、面试真题与参考答案

题目(字节跳动前端面试题):

请解释 Angular 中 NgModule 的设计目的。为什么 Angular 需要 NgModule 这个额外的概念?它与 ES Modules 的 import/export 有何不同?Standalone Components 的引入是否意味着 NgModule 将被淘汰?请结合 Angular 的编译模型说明你的观点。

参考答案

NgModule 的设计目的是为 Angular 的编译器和依赖注入系统提供必要的元数据上下文。与 React 和 Vue 依赖打包工具(如 Webpack/Vite)来解析组件间依赖不同,Angular 使用自有的 AOT 编译器(Ahead-of-Time Compiler),它在构建时分析整个应用的模板、组件和指令,生成优化后的 JavaScript 代码。NgModule 为编译器提供了这一分析所需的信息:哪些组件属于同一个编译上下文(declarations)、哪些外部模块提供了可用的组件和指令(imports)、哪些组件可以对外暴露(exports),以及服务在哪个层级创建和共享(providers)。

NgModule 与 ES Modules 的 import/export不同层面的概念:ES Modules 负责文件级别的代码组织——哪些变量、类、函数可以从一个文件导出并在另一个文件中导入。NgModule 负责框架层面的编译和装配——ES Modules 告诉 TypeScript 编译器“这个类存在且可以导入”,但 Angular 还需要知道“这个类是组件、指令还是服务?它在哪个模块的模板中可用?它的依赖如何被注入?”。两者是互补的:ES Modules 处理文件依赖,NgModule 处理 Angular 特有的语义依赖。

Standalone Components 的引入简化但未淘汰 NgModule。在 Standalone 模式下,组件通过 imports 直接声明其依赖的其他组件、指令和管道,Angular 编译器可以从 Standalone 组件的元数据中直接推断出编译上下文,不再需要显式的 NgModule 包裹。然而,Standalone 组件可以被导入到 NgModule 中,两者可以共存。对于大型企业应用中的复杂依赖注入配置(如多级注入器、延迟加载模块、服务作用域控制),NgModule 仍然提供了结构清晰的集中式配置方式。Angular 团队表示 NgModule 不会被移除——Standalone 只是一个可选的替代模式,让小型应用或新组件可以从 NgModule 的样板代码中解放出来。


课后练习答案

一、概念自测答案

  1. C

    • 解析:declarations 用于声明本模块拥有的组件、指令和管道。imports 是导入其他模块,providers 是注册服务,exports 是导出供其他模块使用。
  2. B

    • 解析:CommonModule 提供 *ngIf*ngFor 等基础指令。A 错误,BrowserModule 仅应在根模块导入一次;C 错误,功能不同;D 错误,CommonModule 可多次导入。
  3. exports

    • 解析:只有被添加到 exports 数组的组件/指令/管道才能在其他模块中使用。
  4. A、B、C

    • 解析:D 错误,根模块的 declarations 只能声明本模块自己的组件。A(bootstrap 仅根模块)、B(必须导入 BrowserModule)、C(可声明多个启动组件)均为正确。

二、AI 编程任务参考答案(提示词示例)

示例提示词
“请为 Angular 项目搭建基础架构。要求:

  • SharedModule:声明并导出 AlertComponent(message/type 输入)和 ButtonComponent(label 输入/clicked 输出),重新导出 CommonModule。
  • DashboardModule:导入 SharedModule,声明 DashboardComponent,使用 Alert 和 Button 组件。
  • AppModule:导入 BrowserModule 和 DashboardModule,启动 AppComponent。
  • 所有组件使用内联模板和样式。输出所有文件的完整 TypeScript 代码。”
CATALOG
  1. 1. 第115课:Angular 项目结构与 NgModule——模块化、组件声明、导出、bootstrap
    1. 1.1. 1. Angular CLI 与项目初始化
      1. 1.1.1. 1.1 安装 Angular CLI 与创建项目
      2. 1.1.2. 1.2 项目骨架结构
    2. 1.2. 2. NgModule 的核心概念
      1. 1.2.1. 2.1 @NgModule 装饰器的配置项
      2. 1.2.2. 2.2 组件必须被声明在一个且仅一个模块中
      3. 1.2.3. 2.3 根模块与特性模块的职责分工
    3. 1.3. 3. declarations:组件、指令与管道的注册
    4. 1.4. 4. exports:控制对外可见的 API
    5. 1.5. 5. bootstrap:应用启动的根组件
    6. 1.6. 6. BrowserModule 与 CommonModule 的区别
    7. 1.7. 7. 综合实战:构建一个简单的 Angular 应用骨架
    8. 1.8. 8. Standalone Components:Angular 的未来方向
    9. 1.9. 课后练习
      1. 1.9.1. 一、概念自测(选择题 / 填空题)
      2. 1.9.2. 二、AI 编程任务:编写面向 AI 的提示词
      3. 1.9.3. 三、Agent 模式下的提示词示例
      4. 1.9.4. 四、面试真题与参考答案
    10. 1.10. 课后练习答案
      1. 1.10.1. 一、概念自测答案
      2. 1.10.2. 二、AI 编程任务参考答案(提示词示例)