我们在前面的小节中成功地引用了`HttpClient`并使用其发起了数据请求,最终将请求的数据成功的显示到了界面上。在此过程中,我们普遇到过如下错误: ![](https://img.kancloud.cn/5b/e7/5be7e7416ffc7f6a908954fabdbe2905_575x68.png) 最终通过在测试文件的imports中加入了HttpClientModule将该错误消除。本节我们将重点讨论一下为什么引入了HttpClientModule后错误就消息了。 在继续学习以前,我们暂时移除测试文件`app.component.spec.ts`中对`HttpClientModule`的引入: ```typescript imports: [ RouterTestingModule, HttpClientModule ✘ ], ``` * ✘ 删除该行 此时`ng t`将报如下错误: ![](https://img.kancloud.cn/57/42/57421d179f3d9392b2078e06c72f4263_606x67.png) # 错在哪 解决问题的前提是弄明白产生问题的原因,找到问题产生的原因的前提是找到出错代码的位置。为了弄清楚到底是哪行代码导致的异常,我们只保留该测试中的第一行代码: ```typescript it('组件初始化', () => { const fixture = TestBed.createComponent(AppComponent); // const app = fixture.componentInstance; // expect(app).toBeTruthy(); // // // 启用angular的自动变更检测机制,自动对V层中的数据进行渲染 // fixture.autoDetectChanges(); }); ``` ![](https://img.kancloud.cn/57/42/57421d179f3d9392b2078e06c72f4263_606x67.png) 异常依旧,此时我们大概可以确认是第一行代码出现了问题。为了避免误杀,我们在将第一行代码也注释掉: ```typescript it('组件初始化', () => { // const fixture = TestBed.createComponent(AppComponent); // const app = fixture.componentInstance; // expect(app).toBeTruthy(); // // 启用angular的自动变更检测机制,自动对V层中的数据进行渲染 // fixture.autoDetectChanges(); }); ``` ![](https://img.kancloud.cn/1d/74/1d74189bc61a752344c5880ab01acd5c_611x77.png) 错误消失,则可以完全确认是第一行代码出了问题。随后移除刚刚第一行代码的注释后,我们继续学习。 >[success] 当发生一些预期以外的问题时,分块注释是个解决问题的好办法。 ## 组件初始化 对组件进行测试的前提是创建一个组件,而创建组件的过程中必然离不开组件的实例化。为了更好的理解这一过程,我们对单元测试中的相关代码并添加相应注释如下: ```typescript it('组件初始化', () => { // 创建一个夹具,该夹具中实例化了一个AppComponent对象 const fixture = TestBed.createComponent(AppComponent); // 获取夹具中的AppComponent对象 // const app = fixture.componentInstance; // 预测上述过程没有发生错误,即成功获取到了AppComponent对象 // expect(app).toBeTruthy(); // 启用angular的自动变更检测机制,自动对V层中的数据进行渲染 // fixture.autoDetectChanges(); ``` `TestBed.createComponent(AppComponent)`的流程图大体如下: ![](https://img.kancloud.cn/9f/87/9f872b7ab8d76cd32159d6bc49cf8722_475x385.png) 通过该图片可以得出结论:错误产生的原因于Angular在能力初始化的过程中没有设置提供`HttpClient`的能力。 而这个能力设置的对象,就是本节中我们将讨论的**模块**。 # 模块 在Angular中**模块**是可独立运行的最小单元,比如我们在src/app文件夹中的app.module.ts的作用就是在声明一个**模块**。 ```bash . ├── app-routing.module.ts ├── app.component.css ├── app.component.html ├── app.component.spec.ts ├── app.component.ts └── app.module.ts 😀 ``` **组件**依赖于**模块**,存在于**模块**,组件若想成动运行,则必然是运行于某个**模块**之中。组件成功运行的前提,是在**模块**中被成功地实例化,**模块**能够成功实例化某个**组件**的前提是**模块**拥有**组件**想要的一切。 ![](https://img.kancloud.cn/04/11/041113da69fe9cf930c935442c56486b_336x104.png) ## 测试模块 在单元测试中,我们通过以下代码创建一个测试模块: ```typescript beforeEach(async () => { await TestBed.configureTestingModule({ imports: [ RouterTestingModule, ], declarations❶: [ AppComponent❷ ], }).compileComponents(); }); ``` 通过在❶declarations中声明❷AppComponent来表示:AppComponent组件属于当前测试模块。当单元测试发生错误时,我们可以查看到当前测试模块的身影。 ![](https://img.kancloud.cn/bb/ca/bbcae41a724713bf0ebe6137734942e4_566x42.png) 它的名称为DynamicTestModule,即动态测试模块,即为当前AppComponent组件所在模块。 一个**模块**拥有什么样的能力,除可以在`declarations`中声明自身拥有的能力以外,还可以在`imports`引用外部的资源。比如当前引入了`RouterTestingModule`,则当前模块拥有`RouterTestingModule`提供的相关能力。 ```typescript imports: [ RouterTestingModule, ], ``` 由于我们要测试的`AppComponent`并不依赖于任何`RouterTestingModule`提供的能力,所以在此我们可以将其引入的代码删除。 ```typescript import {RouterTestingModule} from '@angular/router/testing'; ✘ ... imports: [ RouterTestingModule, ✘ ], ``` ## HttpClientModule `declarations`与`imports`不同的是,前者仅能够声明`组件`等**模块**的成员,而后者则是声明的其它**模块**。 与当前**动态测试模块**相同,**HttpClientModule**也是一个模块,该模块下有一个**可用**的**HttpClient**成员,所以当我们将`HttpClientModule`加入到`imports`中时: ```typescript imports: [ HttpClientModule ], ``` 相当于: ![](https://img.kancloud.cn/0d/e3/0de386bb8e7c287381f26c3dad004b50_391x181.png) 此时动态测试模块便拥有了两个成员:自己声明的`APP组件`以及引入的`HttpClientModule`模块中的`HttpClient`。所以此时在执行`TestBed.createComponent(AppComponent)`当前模块便拥有了创建`AppComponent`的先决条件。从而使得在动态测试模块中成功的创建了`AppComponent` 最后我们忧愁单元测试相关注释内容: ```typescript it('组件初始化', () => { // 创建一个夹具,该夹具中实例化了一个AppComponent对象 const fixture = TestBed.createComponent(AppComponent); // 获取夹具中的AppComponent对象 const app = fixture.componentInstance; // 预测上述过程没有发生错误,即成功获取到了AppComponent对象 expect(app).toBeTruthy(); // 启用angular的自动变更检测机制,自动对V层中的数据进行渲染 fixture.autoDetectChanges(); ``` # 依赖注入 依赖注入(Dependency Injection)简称DI,是大部分框架支持的特性。好像已然成为了面试的必考题了一样。在此,我们简单对此有个介绍。 ![](https://img.kancloud.cn/9f/87/9f872b7ab8d76cd32159d6bc49cf8722_475x385.png) 简单来说:上图中的❸❹❻❼的过程称即被称为**依赖注入**。 因为❸❹❻❼过程大体实现了:根据AppComponent声明的**依赖**类型`HttpClient`来将一个`HttpClient`实例**注入**给AppComponent。 更准确的来讲,**依赖**一词源于UML类图中的类与类的一种关系,表示两个类之间存在的一种弱关系,是两个类耦合最小的一种。比如人类当前依赖于手机、依赖于网络,都是现实生活中的依赖的具体体现。 **注入**一词就是给的意思,但这个给的操作是Angular主动实施的,是自上而上的,所以称为**注入**更恰当一些。 # 总结 让我们简单总结下: * 组件不能够独立存在,其必然属于某个**模块** * 一个模块的**能力**取决于两个方面: * 其`declarations`的声明的成员 * 其`imports`中声明模块的**可用**成员 * 一个模块的能力列表决定了其是否有创建某个组件的能力 # 资源列表 | 名称 | 地址 | |---- | ---- | | Angular模块简介 | [https://www.angular.cn/guide/ngmodules](https://www.angular.cn/guide/ngmodules) | | Angular中的依赖注入 | [https://www.angular.cn/guide/dependency-injection](https://www.angular.cn/guide/dependency-injection) | | 本节源码 | [https://github.com/mengyunzhi/angular11-guild/archive/step2.2.3.zip](https://github.com/mengyunzhi/angular11-guild/archive/step2.2.3.zip) |