2.1 模块与注入体系


文档摘要

2.1 模块与注入体系 NgModule 是 Angular 组织组件、指令、管道与服务提供者的传统容器;现代版本中 standalone 组件让模块逐步退居幕后。本节讲清两者关系与注入器树的查找规则——服务在哪里提供,决定了状态能被谁共享,也间接决定了变更检测的波及范围。 第 1 章末尾我们说到"类型系统保证绑定表达式合法"。从本节起进入第 2 章:先解决"代码与依赖如何组织",再谈组件本身。 上一节留下的钩子 第 1 章末尾留下的钩子是"组件树取代了作用域树",本节回答:这棵树的第一层——模块与注入体系——如何搭起来: 写出 @NgModule 五个核心元数据的职责,并说出每个的典型错误用法。 说明 standalone 组件为什么能取代大部分 NgModule,以及两者如何共存过渡。

2.1 模块与注入体系

NgModule 是 Angular 组织组件、指令、管道与服务提供者的传统容器;现代版本中 standalone 组件让模块逐步退居幕后。本节讲清两者关系与注入器树的查找规则——服务在哪里提供,决定了状态能被谁共享,也间接决定了变更检测的波及范围。

第 1 章末尾我们说到"类型系统保证绑定表达式合法"。从本节起进入第 2 章:先解决"代码与依赖如何组织",再谈组件本身。

上一节留下的钩子

第 1 章末尾留下的钩子是"组件树取代了作用域树",本节回答:这棵树的第一层——模块与注入体系——如何搭起来:

  1. 写出 @NgModule 五个核心元数据的职责,并说出每个的典型错误用法。
  2. 说明 standalone 组件为什么能取代大部分 NgModule,以及两者如何共存过渡。
  3. 描述注入器树的层级查找算法,解释"同一个服务三个不同范围"的现象。
  4. 用 Tree-shakable 的 providedIn 方式提供全局服务,说明其相对模块 providers 的优势。

一、NgModule:编译单元与组织单元

传统方式下,组件必须声明在某个模块里才能被模板使用:

import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { OrderListComponent } from './order-list.component'; import { OrderStatusPipe } from './order-status.pipe'; import { OrderService } from './order.service'; @NgModule({ declarations: [ // 声明:本模块私有的组件/指令/管道 OrderListComponent, OrderStatusPipe ], imports: [CommonModule], // 导入:获得其他模块导出的构件与指令 exports: [OrderListComponent], // 导出:允许其他模块的模板使用 providers: [OrderService] // 提供:在本模块注入器上注册服务 }) export class OrderModule {}

五个元数据中最容易被误解的是 declarations 与 imports 的边界:自己写的且未被 standalone 化的构件放 declarations;别人的能力放 imports。把组件同时放进 declarations 又 imports 是版本升级期最常见的报错来源。

为什么需要模块?站在变更检测的视角,NgModule 首先是编译单元——模板里能用哪些指令和管道,由模块的导入闭包决定,编译器据此生成变更检测代码。所以"模块组织"直接影响"绑定表达式怎么被编译"。

二、standalone:把组织权交还组件

新版本里,组件可以直接标记 standalone 并自己声明依赖:

import { Component } from '@angular/core'; import { CommonModule } from '@angular/common'; @Component({ standalone: true, // 不再需要任何 NgModule 声明 imports: [CommonModule], // 组件级导入:模板需要的指令管道在这里声明 selector: 'app-order-list', template: ` <ul> <!-- NgFor 来自 CommonModule,导入闭包以组件为单位计算 --> <li *ngFor="let o of orders">{{ o.orderNo }} - {{ o.status }}</li> </ul> ` }) export class OrderListComponent { orders = [ { orderNo: 'A-1', status: '待支付' }, { orderNo: 'A-2', status: '已发货' } ]; }

从 NgModule 到 standalone 不是简单换语法,而是组织逻辑的迁移:依赖声明从"模块级"下沉到"组件级",谁用谁导入,未使用的构件可以被构建工具摇掉。渐进迁移时可以混用——standalone 组件可以放进 NgModule 的 imports,反向也可以。

💡 判断:新项目我建议全面 standalone,只保留必要的路由配置聚合。旧项目按"叶子组件先行"策略迁移,风险最小。

三、注入器树与服务的查找规则

Angular 运行时维护一棵与组件树并行的注入器树。请求依赖时的查找算法:从当前组件的注入器开始,找不到就问父级,一直到根注入器,沿途第一个命中即返回,并把实例缓存在提供它的那一级。这条规则带来三个典型范围:

import { Injectable, inject } from '@angular/core'; @Injectable({ providedIn: 'root' }) // 方式一:根注入器,全应用单例,可被摇树优化 export class CartService { items: string[] = []; } // 方式二:组件级提供——每个该组件的实例各得一份 @Component({ selector: 'app-cart-slot', template: `<p>本槽位商品数:{{ svc.items.length }}</p>`, providers: [CartService] // 覆盖查找:子树内拿到的是局部实例 }) export class CartSlotComponent { svc = inject(CartService); // inject 函数:构造器注入的现代等价物 } // 方式三:路由级提供——懒加载边界,仅该路由子树可见(第 2.6 节衔接) // loadChildren 页面里 providers: [ReportService]

背景:购物车里三个插槽组件,产品希望它们各自维护独立的暂存清单,但结算服务全店唯一。
操作:暂存清单服务用组件级 providers 提供,结算服务用 providedIn root 提供。
结果:每个插槽操作自己的清单互不干扰;结算时三个插槽都把数据汇总进同一个结算服务。
解读:范围选择本质是"状态共享半径"的选择——半径越大,一次变更影响的面越广,这也为第 3 章 OnPush 优化埋下思考题。
变式:把清单服务误提到 root,三个插槽立刻共享同一份数据——这是真实项目里最常见的"数据串了"事故,排查时先看 providers 写在哪一级。

图:注入器树查找路径示意

图:注入器树查找路径示意

四、常见坑与排查

⚠️ 两个高频坑:其一,模块 providers 与组件 providers 同时写同一服务,注入结果"有时是 A 有时是 B"——取决于注入点在哪个注入器之下,而非书写顺序。其二,懒加载模块的 providers 在旧版会创建独立注入器,服务出现"两份实例",改用 providedIn root 或路由级 provideFor 亚配置统一收口。

本节要点回顾

  • NgModule 五元数据:declarations 管私产、imports 借能力、exports 放行、providers 注册服务、bootstrap 只在根模块。
  • standalone 迁移:组织权从模块下沉到组件,导入闭包按组件计算,新项目建议全面 standalone。
  • 查找算法:就近命中、逐级上溯、实例缓存在提供级——三个范围对应三种单例半径。
  • providedIn root:全局单例且可摇树优化,是默认首选。
  • 主线上的一环:服务提供范围决定状态共享半径,也就决定了数据变化后可能牵动的组件集合。

下一节聚焦树上的节点本身:组件为什么是变更检测的执行单元。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U