本文用作对 Angular 中依赖注入的个人理解分享。

关于依赖注入,Angular 从 1 升级到 4 依然锲而不舍,一定是有它的原因吧。

依赖注入

简要说明

  • 控制反转-wiki 控制反转(Inversion of Control,缩写为 IoC),是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度。

其中最常见的方式叫做依赖注入(Dependency Injection,简称 DI)。

本骚年没写过 java,也不清楚 java 的依赖注入是否和 Angular 中的一样,因此以下的分享仅为个人理解,欢迎大家来一起探讨。

理解依赖注入

为什么需要依赖注入呢?

工程化的设计其实对项目的快速拓展和高效开发是很有帮助的。像多人合作或者是长期维护的代码,如果人员间甚至是自身的时期阶段没有一些较好的约定,那么到后面开发和维护成本只会随着项目的成长而增加。

而这样一些磨合和生成共同约定的过程,也在不断地考验人的性能。像一些经历过无数遍被总结归纳的设定,有人开发了一些工具去帮助我们遵守这样的规则。 而所有的设计和架构,都是为了使得我们的工作更加高效。

依赖注入也是这样一个适配 Angular 应用场景的功能设计,只是把我们从原始代码不断整理、抽象和重构这样一个过程做了归纳,给予我们需要的东西。

不仅是依赖注入,像现在被大家槽得多前端太杂太乱、变化更新太快、工具框架层出不同,其实也都是因为有人发现了这些需求,并设计和整理出来方便使用的。 像这样一些分享的爆发、思维碰撞的高峰,其实都是推进生产力、或是演化历史的一个很棒的过程。

而本骚年是这么理解依赖注入的。

像我们组装台式机,自行选购主板、显卡、内存等等。而这些配件是由其他厂家生产制造包装好,配合说明书就可以直接使用的。

这样我们每组装一台台式机的过程,就会拿到这些成品配件然后拼装。对于具体的主板怎么造出来,里面又使用了什么方法和零件,我们并不需要关心。 当我们组装台式机的初始化过程,就已经提供了主板可以使用。

在项目中,体现为项目提供了这样一个注入机制,有人负责提供服务,有人负责消耗服务,而这样的机制提供了中间的接口,并替使用者进行了创建并初始化这样的处理。 我们只需要知道,拿到的是完整可用的服务就好了,至于这个服务内部的实现,甚至是它又依赖了怎样的其他服务,都不需要关注。

Angular 与依赖注入

Angular 依赖注入

Angular 官网上有句话:Angular 的依赖注入系统能够即时地创建和交付所依赖的服务。

本骚年想了很久,其他框架为什么不使用依赖注入呢?

其实像我们设计一个项目,自行封装的一些组件和服务,然后再对它们的新建和初始化等等,也经常需要用到依赖注入这种设计方式的。

我们的服务也可以分为有记忆的和无记忆的,关键在于抽象完的组件是否内部记录自身状态,以及怎样维护这个状态等等,甚至设计不合理的话,这样的状态管理会经常使我们感到困扰,所以 Redux、Flux 和 Mobx 这样的状态管理框架也就出现了。

而 Angular 在某种程度上替我们做了这样的工作,并提供我们使用。

像 Angular 的 release 版开始,有了NgModule模块类这样的概念,这跟依赖注入有什么关系呢?不着急,我们可以先简单介绍一下。

NgModule 模块类

Angular 模块把组件、指令和管道打包成内聚的功能块,每个模块聚焦于一个特性区域、业务领域、工作流或通用工具。 这里对于组件、指令和管道大家也不需要详细了解,暂时可以理解为一些封装好的功能视图组件吧。

我们要组装一个模块类,需要:

  1. 声明哪些组件属于该模块。
  2. 公开某些类,以便其它的组件模板可以使用它们。即一处注册,多处使用。
  3. 导入其它模块,从其它模块中获得本模块所需的组件。
  4. 在应用程序级提供服务,以便应用中的任何组件都能使用它。

第四点我们需要使用provider这样的参数字段,而作为最浅显的服务依赖注入便是通过provider进行的。

provider 服务提供

不管是在组件内,还是在模块内,我们使用providers的时候,就是进行了一次依赖注入的注册和初始化。

举例来说,我们有这样一个结构的应用:

根模块
├── 登录模块
│
├── 内部模块
│      ├── 头部面包屑 (需监听路由)
│      ├── 左侧菜单列 (需触发路由)
│      └── 展示内容
│
└── 其他服务、组件等 (路由服务在这里)

如果说我们分别在面包屑和菜单列使用providers,就相当于我们分别注册了两次路由服务。 这个时候,问题就会来了。因为我们注册了两次服务,所以我会获得两个实例,而这样的话,将导致我的面包屑和菜单列将无法获取到相同的路由事件。

所以,我们在根模块里面使用providers传入路由服务,就可以获得一个服务实例。 然后我们在子模块里面就可以直接使用该服务,获取同样的路由状态和事件。

多级依赖注入

再谈NgModule和依赖注入的关系,其实模块类(NgModule)也就和组件一样,在依赖注入中的身份是一个注入器,作为容器提供依赖注入的接口。 在 Angular 更新 release 版本之后,NgModule的出现使得我们不需要再在一个组件中注入另外一个组件了,通过模块类(NgModule)的方式可以进行获取和共享。

我们会发现,一个 Angular 应用是一个组件树,同时每个组件实例都有自己的注入器,组件的树与注入器的树平行。

现在树结构已经在前端领域越来越流行了,浏览器的 DOM 树/CSS 规则树、React 的虚拟 DOM、以及 Angular(其实不只是 Angular)的组件树和注入器树。

上面也说道,并不是所有的组件都会注入服务的,所以有了"注入器冒泡":

当一个组件申请获得一个依赖时,Angular 先尝试用该组件自己的注入器来满足它。如果该组件的注入器没有找到对应的提供商,它就把这个申请转给它父组件的注入器来处理。

对比其他框架谈谈依赖注入

其实上面也简单提到过,像其他框架(React/Vue)中状态管理是通过组件传递、bus 总线、事件传递、或者是状态管理工具 Redux/Flux/Vuex 等进行。 通过前面几种方法,项目规模大了之后,如果少了一些约束或者是约定,则应用状态将会变得愈发不可控。

故我们有了状态管理系列工具,无非就是将需要共享或者传递的状态,通过公共的通道进行传输和触发更新,然后我们只需要在公共的通道里,找到想要定位的状态即可。 而不同的状态管理工具要结合不同框架使用,也需要对不用框架的组件状态更新方式(setState()之类的)进行适配和调整。

再回头看看 Angular,你会发现整个框架的设计都很不一样。 即使同样有生命周期、组件、数据绑定等等的东西,也有事件传递和监听。但在 Angular 里面我们常常通过服务来共享一些状态的,而这些管理状态和数据的服务,便是通过依赖注入的方式进行处理的。

所以归根到底,很多时候我们创建服务,是为了维护公用的状态和数据,通过依赖注入的方式来规定哪些组件可共享。 而在其他框架,我们要设计这样一个可根据不同场景单独使用或是共享的服务,则是需要自行添加初始化、新创建实例、共享实例等等方面的考虑和设计了。或者是,使用状态管理工具。

结束语

关于组件和应用的状态管理一直是个比较棘手的问题,尤其是在项目规模变大之后问题就会更加显著。对于 Angular 的依赖注入,以及其他状态管理框架,不同的设计方式其实只是导致不同的约束和使用方法而已。
当然,如果有具体的场景需求,还是可以结合实际需要来进行选型和调整的。

部分文章中使用了一些网站的截图,如果涉及侵权,请告诉我删一下谢谢~
温馨提示喵