本文从下面几个方面全面、深入的阐述 Angular service 中的相关概念:
- 创建一个 Angular Service
- 配置服务的生效范围
- 确认注入服务的结构上的层级
- 创建异步服务
- 使用内置服务
1. Module 1
1.1 什么是 service?
下面是使用 Angular service 或者将部分逻辑剥离 component 的原因:
而 Angular 官网上给出的解释为:
Do limit logic in a component to only that required for the review. All other logic should be delegated to services.
看起来 component 中处理的是样式逻辑,而 services 中处理的则是除了样式之外的逻辑!因此你也可以根据下面的依据决定这部分逻辑应该写在哪里。
1.2 加强的 npm start
concurrently
是一个运行在Node.js上的命令行工具,它允许你同时运行多个命令。这在开发过程中非常有用,特别是当你需要同时执行多个任务,例如同时进行前端构建和后端服务器运行。
完整命令:
bash
concurrently --kill-others \"ng build --watch --no-delete-output-path\" \"node server.js\"
命令执行流程:
-
构建Angular应用 :
ng build --watch --no-delete-output-path
命令启动Angular应用的构建过程,并在文件变化时自动重新构建,但不删除输出路径中的已生成文件。 -
运行Node.js服务器 :
node server.js
命令启动你的Node.js服务器,它将提供你的应用或API。 -
并发执行 :
concurrently
确保这两个命令同时运行。如果Angular构建过程中发生错误并终止,--kill-others
选项会触发,Node.js服务器也会停止运行。 -
监视文件变化:在开发过程中,当你修改了Angular应用的源代码或服务器端的代码时,相关的命令(Angular构建或Node.js服务器)会捕获这些变化并重新执行,以确保你的应用总是最新的。
使用场景:
这种命令模式在开发中非常有用,特别是当你需要快速迭代前端和后端代码时。例如,前端开发者可以更改Angular代码并立即看到更新,而后端开发者可以更改服务器逻辑并立即测试其效果,而不必分别手动重启构建过程或服务器。
注意事项:
- 确保
server.js
是可执行的,并且位于执行该命令的相同目录中,或者提供正确的路径。 - 使用
concurrently
时,所有子命令都会共享同一个控制台/终端会话,这有助于你监控所有进程的输出。 - 如果你使用的是集成开发环境(IDE)或代码编辑器,可能存在内建的任务运行器,可以实现类似的并发执行功能。
2. Module 2 -- 初见服务
本节内容有:
- 了解 service 的组成部分
- 创建一个 service
- 手动方式
- 使用脚手架的方式
- 将服务注入到组件中
- 使用服务分享数据
2.1 service 的组成部分
一个完整的 service 由以下几个部分组成:
2.2 service 的原理
Angular服务利用依赖注入(DI)系统实现。服务定义时,可通过providedIn
指定提供位置,如root
、platform
或特定模块。DI系统负责创建服务实例,并根据服务的提供范围(如根服务或模块服务)缓存实例。组件或其他服务通过构造函数或方法中声明的依赖项,请求服务实例。若服务配置为单例,DI系统将提供同一缓存实例,确保整个应用或模块内服务实例的唯一性。这使得服务可在整个应用范围内共享状态和功能,同时保持代码的整洁和可维护性。
2.3 嵌套的 services
一个服务中可以嵌套其他服务,这一点至关重要。
2.4 使用服务共享数据
由于服务的实例是单例,因此可以用来在不同的组件之间共享数据。
举一个例子,如果我想实现国际化,那么新建一个服务并在其中维护一个私有的字段保存当前使用的语言是一个很不错的选择。因为服务被注入到 root 中,所以在整个 root 范围内都可以获取此服务的实例,并且是一个单例。于是单例中维护的私有变量经过接口函数就可以被全局范围的组件获取和设置了。
如下代码所示:
ts
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class LangStorageService {
private current_lang_key = 'CURRENT_LANG';
private default_lang = 'EN';
private current_lang = '';
constructor() {
this.initLanguage();
}
initLanguage(): void {
const storedLang = localStorage.getItem(this.current_lang_key);
if (storedLang) {
this.current_lang = storedLang;
} else {
this.current_lang = this.default_lang;
localStorage.setItem(this.current_lang_key, this.default_lang);
}
}
getLanguage(): string {
return this.current_lang;
}
setLanguage(current_lang: string): string {
this.current_lang = current_lang;
localStorage.setItem(this.current_lang_key, current_lang);
return current_lang;
}
clearLanguage(): void {
localStorage.removeItem(this.current_lang_key);
}
}
2.5 小结一下
在Angular中,服务是类,由注入器按需提供实例。保持服务命名的一致性至关重要,推荐使用骆驼式命名法(CamelCase)。服务是数据共享的理想选择,确保了应用各部分之间的数据一致性。
3. Module 3
在 Angular 中,注入的是服务单例;而不是注入服务类然后再在组件内实例化这个服务类。
Angular服务注入的设计原理
Angular服务之所以采用依赖注入形式,基于以下设计理念:
- 控制反转(IoC):Angular通过依赖注入实现控制反转,将获取依赖的控制权从组件转移到框架,简化组件间的依赖管理。
- 解耦合:服务与组件之间的依赖通过接口定义,降低了它们之间的耦合度,使得代码更加模块化。
- 生命周期管理:依赖注入系统自动处理服务的生命周期,确保服务在需要时可用,且在不再需要时可以被适当地销毁。
- 测试性:服务的依赖通过构造函数注入,便于在单元测试中模拟依赖,提高测试的覆盖率和可靠性。
- 可配置性:依赖注入允许开发者通过提供不同的实现来配置服务,适应不同的运行环境或条件。
- 作用域控制:服务的作用域可以精确控制,可以是全局的,也可以限定在特定的模块或组件。
Angular服务的注入形式是为了提供一种标准化、自动化的依赖管理方式,从而提高开发效率,降低错误率,并增强代码的可维护性和可测试性。
3.1 provider 之于 service 的意义
A provider tells an injector how to create the service.
token & recipe
如上图所示,你可以将 token 和 recipe 设置成相同的字段,然后简写。但是这两个绝对不是一回事。
3.2 使用不同的 recipe
recipe 不一定要和 token 相同,只需要保证 recipe 的值实现了 token 的接口即可,如下所示:
你甚至可以写成这样:
也可以这样:
3.3 Injectors 的作用
Injectors 有以下四个作用:
3.4
在Angular中,注入器(Injector)层次结构是一种设计,用于管理依赖项的提供和检索。这个层次结构确保了依赖项的解析是按照它们声明的上下文进行的。
Angular注入器层次结构的关键点:
-
根注入器(Root Injector):
- 每个Angular应用都有一个根注入器,它是应用中所有其他注入器的起点。
-
平台注入器(Platform Injector):
- 平台注入器通常用于提供平台级别的服务,比如
HttpClient
。
- 平台注入器通常用于提供平台级别的服务,比如
-
模块注入器(Module Injector):
- 每个NgModule有自己的注入器,它用于提供该模块范围内的服务。
-
组件注入器(Component Injector):
- 每个Angular组件都有自己的注入器,用于提供或检索与该组件相关的服务。
-
视图注入器(View Injector):
- 与组件注入器相关联,通常用于提供与特定视图相关的服务。
-
指令注入器(Directive Injector):
- 对于带有
providers
属性的指令,Angular会创建一个局部注入器,用于提供指令特有的服务。
- 对于带有
解析依赖项:
- 当请求服务时,Angular首先在当前注入器中查找。
- 如果当前注入器没有提供该服务,Angular会向上查找其父注入器。
- 这个过程会一直持续,直到根注入器被检查过。
层次结构的好处:
- 封装性:服务可以在它们被使用的地方局部提供,避免全局污染。
- 重用性:服务可以在多个层次上提供,增加代码的重用性。
- 灵活性:允许在不同的层次上覆盖服务,例如在特定组件或模块中使用不同的服务实现。
示例:
typescript
// 根模块提供根服务
@NgModule({
providers: [RootService],
// ...
})
export class AppModule { }
// 特定模块覆盖根服务
@NgModule({
providers: [{ provide: RootService, useClass: ModuleSpecificService }],
// ...
})
export class SpecificModule { }
// 组件提供自己的服务
@Component({
providers: [ComponentService],
// ...
})
export class SomeComponent { }
在上述示例中,RootService
在应用的根模块中提供,ModuleSpecificService
在特定模块中覆盖了RootService
,而ComponentService
是特定于组件的服务。
通过这种层次化的注入器系统,Angular提供了一种强大且灵活的方式来管理依赖项,使得应用的架构更加清晰和可维护。
3.5 service 注入位置
基于下面的准则去注入服务。
关于上面的第三条,这里详细说明一下:
在定义服务的时候不去指定元信息中的注入位置,也就是只装饰,而不提供装饰信息。然后在 component 中直接引入,而不是在某个模块中。
而对于第四条,我们创建 core module 之后,在其中注入各个服务;最后再在跟模块中引入此 core module.
注意,使用这种方法注入的服务 LoggerService
和 DataService
都没有在元信息中指定注入的位置。
3.6 模块加载守卫
给我们的 core module 增加守卫,防止懒加载模块中出现重复加载的问题。
4. Module 4 -- 异步服务
异步服务的根本特点是:服务提供的方法具有类型为 Promise 或者 Observable 类型的返回值。
4.1 异步服务的重要性
异步服务的重要性可以通过下图展示:
4.2 处理异步方法的返回值
异步服务的典型应用场景就是发起各种客户端的网络请求,下面展示了如何处理异步服务方法返回值。
而对应的服务异步方法则可以写成:
4.3 处理异步方法的错误或者异常
处理异步方法的错误,首先需要明确的是,我们不应该将浏览器的错误对象直接暴露出来,而是应该先定义一个错误类,这个错误类的作用是整理过滤最初的错误信息。
然后使用这个自定义错误类:
使用方就可以捕获自定义的错误对象了:
4.4 关于 Promise
并不是所有的异步方法的返回值都是 observable 对象。有的返回值是 promise. 对于这种情况只有一个点需要说明。那就是 promise 链的最后一个总是 catch(). 这样才是比较保险的做法。
总结一下:异步服务指的就是那些有着异步方法,即返回值为 promise 或者 observable 类型的服务。这类服务相当重要,因为永远都不要将 long task 放在 component class 中,而是应该使用异步的 service 来执行它们。
5. Module 5 -- 使用内置服务
你可以在这个网站找到一些非常好用的 Angular 内置服务:angular.io/api
在这个网站上,搜索 Title Version ErrorHandle 这三种服务最为常见。
关于 ErrorHandle 这个服务,主要将其作为接口来实现,如下所示: