1注入,一种组件树级通信模式&设计模式1.1组件通信模式在Angular项目开发中,我们通常使用Input属性绑定和Output事件绑定来进行组件通信,但是Input而Output只能在父子组件之间传递信息。组件按照调用关系形成组件树。如果只有属性绑定和事件绑定,那么两个非直接关系的组件需要通过每个连接点本身进行通信,中间人需要不断地处理和传递一些它不需要知道的东西。信息(图1左侧)。Angular提供的InjectableService可以提供在模块、组件或指令中,它可以与构造函数注入一起使用来解决这个问题(图1右)。图1组件通信方式左图仅通过父子组件传递信息。节点a和节点b需要经过很多节点才能通信;如果节点c想通过一些配置来控制节点b,中间的节点还必须设置额外的属性或事件来透传相应的信息。右图中的依赖注入模式节点c可以为节点a和节点b提供服务进行通信,节点a直接与节点c提供服务通信,节点b也直接与节点c提供的服务进行通信,最后通信为简化。节点也不耦合这部分内容,对上下层组件之间的通信没有明显的感知。1.2使用依赖注入实现控制反转依赖注入(DI)并不是Angular独有的。它是实现控制反转(IOC)设计模式的一种手段。DependencyInjection的出现解决了手动实例化过度耦合的问题,所有的资源都不能使用资源的两方管理,而不是由资源中心或第三方提供而不使用资源,可以带来很多好处。首先,资源集中管理,资源配置方便,管理方便。第二,降低了使用资源的双方之间的依赖程度,也就是我们所说的耦合度。类比现实世界就是,当我们去购买铅笔等产品时,只需要找一家商店购买铅笔类型的产品即可。我们不关心铅笔是哪里做的,木头和铅笔芯是怎么粘合的,我们只需要它来完成铅笔的书写功能,我们不会和具体的铅笔生产厂家或工厂接触。对于商店来说,可以自己去合适的渠道购买铅笔,实现资源的可配置性。结合编码场景,更具体地说,用户可以在不显式创建实例(新建操作)的情况下注入和使用实例,实例的创建由提供者决定。资源的管理是通过令牌(tokens)。由于提供者不关心实例的创建,用户可以通过一些本地注入的方式(令牌的二次配置)最终实现实例的替换,依赖注入模式应用和切面编程(AOP)相辅相成。2Angular中的依赖注入依赖注入是Angular框架最重要的核心模块之一。Angular不仅提供了Service类型的注入,组件树本身就是一个注入依赖树,函数和值也可以注入。也就是说,在Angular框架中,子组件可以通过父组件的token(通常是类名)来注入父组件实例。在组件库的开发中,有大量的情况是通过注入父组件来实现交互和通信,包括参数挂载、状态共享,甚至获取父组件所在节点的DOM。2.1解析依赖要使用Angular注入,首先要了解它的注入解析过程。类似于node_modules的解析过程,当没有找到依赖时,总会冒泡到父层去寻找依赖。Angular的旧版本(v6之前)将注入解析过程分为多级模块注入器、多级组件注入器和元素注入器。新版本(v9之后)简化为两级模型。第一条查询链是静态DOM级元素注入器、组件注入器等统称为元素注入器,另一条查询链是模块注入器。官方代码注释文档(provider_flag)中明确说明了解析顺序和解析失败后的默认值。图2两级注入器查找依赖过程(图片来源)也就是说组件/指令和组件/指令级提供的注入内容会先在组件视图的元素中寻找依赖,直到根元素,如果没有找到,则在该元素当前模块,引用(包括模块引用和路由懒加载引用)本模块的父模块,一次向上到根模块和平台模块。注意这里的injector是继承的。元素注入器可以创建并继承父元素注入器的查找函数,模块注入器类似。不断继承之后,有点像js对象的原型链。2.2配置提供者了解依赖解析的顺序优先级,我们可以提供合适级别的内容。我们已经知道它有两种类型:模块注入和元素注入。Moduleinjector:Provider可以在@NgModule的metadata属性中配置,v6之后提供的@Injectable声明也可以用来声明provideIn为模块名,'root'等(其实上面还有两个injector根模块的Platform和Null,这里不做讨论。)元素注入器:Providers,viewProviders可以配置在组件@Component的元数据属性中,也可以配置在指令的@Directive元数据中的providers。另外,其实@Injectable装饰器不仅可以使用声明的模块注入器,还可以声明为元素注入器。更多时候它会被声明为在根目录下提供,以实现单例。它通过类本身集成元数据,防止模块或组件显式声明提供者,这样如果类没有任何组件指令服务和其他类注入其中,将没有代码链接到类型声明,这可以被编译器忽略,从而实现Shakethetree。另一种提供方式是在声明InjectionToken时直接给定值。以下是这些方法的简写模板:@NgModule({providers:[//模块注入器]})exportclassMyModule{}@Component({providers:[//elementinjector-component],viewProviders:[//ElementInjector-组件视图]})exportclassMyComponent{}@Directive({providers:[//ElementInjector-Directive]})exportclassMyDirective{}@Injectable({providedIn:'root'})exportclassMyService{}exportconstMY_INJECT_TOKEN=newInjectionToken('my-inject-token',{providedIn:'root',factory:()=>{returnnewMyClass();}});提供依赖的位置不同的选择会带来一些差异,最终影响包的大小、可注入的依赖范围和依赖的生命周期。针对不同的场景,如单例(根)、服务隔离(模块)、多编辑窗口(组件)等,有不同的适用方案,应选择合理的位置,避免信息共享不当或代码冗余包装。2.3多样的值函数工具如果只提供实例注入,并不能体现出Angular框架依赖注入的灵活性。Angular提供了很多灵活的注入工具,useClass自动创建新实例,useValue使用静态值,useExisting可以复用现有实例,useFactory是通过函数构造的,指定deps和指定构造函数参数,这些组合可以非常棘手。您可以用您自己准备的另一个实例中途替换一个类的令牌。您可以先创建一个令牌来保存值或实例,然后在以后需要时将其替换回来。您甚至可以使用工厂函数将返回的实例的本地信息映射到另一个对象或属性值。这里的玩法会通过下面的案例来说明,这里就不展开了。官网上也有很多例子可以参考。2.4注入消费和装饰器Angular中的注入可以在构造函数中注入,也可以通过get方法获取注入器注入器获取已有的注入元素。Angular支持在注入时添加装饰器标记,@Host()限制冒泡@Self()限制元素本身@SkipSelf()限制元素本身@Optional()标记为可选@Inject()限制这里有一篇自定义Token令牌的文章《@Self or @Optional @Host? The visual guide to Angular DI decorators.》,形象地展示了如果父子组件之间使用不同的装饰器,最后会命中的实例有什么不同。图3不同注入装饰器的筛选结果2.4.1补充:在宿主视图和@Host装饰器中,@Host可能是最难理解的。以下是@Host的一些具体说明。@Host装饰器的官方解释是...从任何注入器中检索一个依赖关系,直到到达宿主元素Host这里的意思是宿主,@Host装饰器会将查询的范围限制在宿主元素(hostelement)内。什么是宿主元素?如果B组件是A组件模板使用的组件,那么A组件实例就是B组件实例的宿主元素。组件模板生成的内容称为View(视图),同一个View对于不同的组件可能是不同的视图。如果A组件在自己的模板范围内使用了B组件(见图4),A的模板内容(红框部分)形成的视图就是A对A组件的内嵌视图,B组件就在这个视图中,所以对于B来说,这个视图是B的主机视图。装饰器@Host是将搜索范围限制在宿主视图内,如果找不到则不会冒泡。图4嵌入式视图和宿主视图3案例与玩法下面我们通过真实的案例来看看依赖注入是如何工作的,如何排错,如何玩法。3.1情况一:Modal窗口创建了一个动态组件,但是找不到该组件。DevUI组件库的模态窗口组件提供了一个服务ModalService,可以弹出一个模态框,可以配置为自定义组件。业务同学在使用该组件时经常报错,打包找不到自定义组件。例如报如下错误:图5使用ModalService时,创建引用EditorX的组件时找不到错误。分析对应的服务提供者,分析ModalService是如何创建自定义组件的。ModalService源码的Open函数是第52行和第95行,可以看到,如果没有传入componentFactoryResolver,则使用ModalService注入的componentFactoryResolver。大多数情况下,业务会在根模块引入一次DevUIModule,而不会在当前模块引入ModalModule。也就是图6中的现状是这样的。根据图6,ModalService的注入器中没有EditorXModuleService。图6ModuleServiceProvider关系图根据注入器的继承,有四种解决方案:将EditorXModule放在ModalModule的声明中,这样注入器就可以找到EditorXModule提供的EditorModuleService——这是最糟糕的解决方案,本身就是懒加载loadChildren实现的目的是减少首页模块的加载。这样一来,子页面中需要用到的内容就放在了AppModule中。第一次加载加载富文本大模块,增加了FMP(FirstMeaningfulPaint)。不能接受的。在导入EditorXModule并使用ModalService的模块中引入ModalService-可取。只有一种情况是不可取的,那就是用另一个顶层的公共Service来调用ModalService,所以还是在上层加载了不需要的模块。在触发使用ModalService的组件时,注入当前模块的componentFactoryResolver,传递给ModalService的open函数参数——最好在实际使用的地方引入EditorXModule。在used模块中,手动提供一个ModalService-desirable,解决了注入搜索的问题。这四个方法其实都是在解决ModalService使用的componentFactoryResolver的injector内部链中的EditorXModuleService问题。保证在二层搜索链上,这个问题就可以解决。知识点总结:模块注入器继承和搜索范围。3.2案例2:CdkVirtualScrollFor找不到CdkVirtualScrollViewport通常我们在多个地方使用同一个模板时,我们会通过模板提取出公共的部分。之前开发DevUISelect组件的时候,开发者想把common的部分提取出来,报错了。图7Codemovementandinjectionerrornotfound这里是因为CdkVirtualScrollFor命令需要注入一个CdkVirtualScrollViewport,但是元素注入注入器继承系统继承了静态AST关系的DOM,动态的不行,所以下面的查询行为发生,查找后报告失败。图8Elementinjectorquerychain搜索范围最终解决方案:要么1)保持原代码位置不变,要么2)需要嵌入整个模板才能找到。图9嵌入整块使CdkVirtualScrollFo能够找到CdkVirtualScrollViewport(方案二)知识点总结:元素注入器的查询链是静态模板DOM元素的祖先。3.3案例三:表单验证组件被封装成子组件无法验证。这个案例来自这个博客《Angular: Nested template driven form》。我们在使用表单验证时也遇到了同样的问题。如图10所示,出于某种原因我们将这三个字段的地址封装成一个组件以供重用。图10将表单地址的三个字段封装到一个子组件中这时候我们会发现报错了。ngModelGroup在host内部需要一个ControlContainer,这是ngForm指令提供的内容。图11ngModelGroup找不到ControlContainer查看ngModelGroup代码,可以看到它只是增加了宿主装饰器的限制。图12ng_model_group.ts限制注入ControlContainer的范围这里可以使用viewProvider配合usingExisting为AddressComponent的宿主视图添加一个ControlContainer的Provider图13使用viewProviders为嵌套组件提供外部Provider知识点总结:viewProvider和usingExisting的组合。3.4案例四:拖拽模块提供的服务由于懒加载,不是单实例,导致无法互相拖拽。内部业务平台涉及跨多个模块拖放。由于loadChildren涉及懒加载,所以每个模块都会单独封装DevUI组件库的DragDropModule,它提供了一个DragDropService。拖拽命令分为拖拽命令Draggable和droppable命令,两个命令通过DragDropService进行通信。本来引入同一个模块,使用模块提供的Service是可以通信的,但是懒加载后DragDropModule模块被打包了两次,也产生了两个孤立的实例。此时,一个懒加载模块中的Draggable指令无法与另一个懒加载模块中的Droppable指令通信,因为此时DragDropService不是同一个实例。图14延迟加载模块导致服务不是同一个实例/单例。很明显我们的语句需要单例,而单例的方法通常是providerIn:'root'。然后让组件库的DragDropService不在模块级别提供,直接提供根扇区就好了。但是仔细一想,这里又会出现其他的问题。组件库本身是为各种业务提供的。万一有的商家在页面的两个地方有两组对应的拖拽,就不想被链接了。这时,单例破坏了这种天然的基于模块的隔离。那么由业务方来替换单例就更合理了。还记得我们前面提到的依赖查询链,先查找元素的注入器,找不到就启动模块注入器。所以替换的想法是我们可以提供一个元素级的提供者。图15使用扩展方法获取新的DragDropService并标记为在根级提供图16使用相同的选择器叠加重复指令,在组件库的Draggable指令和Droppable指令中添加附加指令并替换令牌的DragDropService如图15和16所示,在根部已经提供了单例的DragDropGlobalService,我们通过元素注入器叠加指令,将DragDropServicetoken替换为我们自己的全局单例实例。这时,在需要使用全局单例DragDropService的地方,我们只需要导入声明和导出这两条额外指令的模块,组件库的Draggable指令和Droppable指令就可以实现跨懒加载模块的通信了。知识点总结:元素注入器的优先级高于模块注入器。3.5案例5:局部主题功能场景如何制作附属于局部的下拉菜单问题DevUI组件库的主题是使用CSS自定义属性(css变量)声明:根css变量的值实现主题切换。如果我们想在一个界面中同时显示不同主题的预览,我们可以在DOM元素本地重新声明css变量来实现部分主题的功能。当我在做主题抖动生成器时,我使用了这样的方法在本地应用主题。图17本地主题功能但是仅仅在本地应用css变量值是不够的。有一些下拉弹出层默认是附着在body尾部的,也就是说它的附着层是在局部变量之外的,这会导致很尴尬的情况。问题。局部主题组件的下拉框是外部主题的样式。图18附加到局部主题的组件外的overlay的下拉框主题不正确,这时候怎么办?我们应该将附着点移回部分主题dom中。已知DevUI组件库的DatePickerPro组件的Overlay使用的是AngularCDK的Overlay。经过一轮分析,我们将其替换为注入如下:1)首先,我们继承OverlayContainer,实现自己的ElementOverlayContainer,如下图所示。图19自定义ElementOverlayContainer并替换_createContainer逻辑2)然后在预览的组件端,直接提供我们新的ElementOverlayContainer,并提供一个新的Overlay,这样新的Overlay就可以使用我们的OverlayContainer。本来在root上同时提供了Overlay和OverlayContainer,这里需要覆盖这两个。图20用自定义的ElementOverlayContainer替换OverlayContainer,提供一个新的Overlay,此时预览网站,弹出层的D??OM会成功附加到component-preview元素上。图21.cdk的Overlay容器附加到指定的dom。部分主题预览成功。DevUI组件库中还有一个自定义的OverlayContainerRef,用于一些组件和模态抽屉长凳,也需要相应地更换。最后可以实现对弹窗、弹层等本地主题的完美支持。知识点总结:好的抽象模式可以让模块可替换,实现优雅的切面编程。3.6案例六:CdkOverlay需要在滚动条上添加CdkScrollable指令,但是不能在入口组件的最外层添加该指令。最后一种情况如何处理?我想说一些非常规的方法,让大家了解provider的本质,配置provider本质上就是让它为你实例化或者映射到一个已经存在的实例。我们知道,如果使用cdkOverlay,如果我们想让弹出框在随着滚动条滚动的同时悬停在正确的位置,就需要给滚动条添加cdkScrollable命令。还是之前例子的场景。我们整个页面都是通过路由加载的。为了方便,我把滚动条写在了组件的宿主上。图22内容溢出滚动条在component:host中写了overflow:auto,于是我们遇到了一个棘手的问题,module是由router定义指定的,也就是没有地方显式调用,cdkScrollable命令应该怎么加?解决方法如下,隐藏部分代码,只留下核心代码。图23通过注入创建实例并手动调用生命周期这里通过注入生成一个cdkScrollable实例,在组件的生命周期阶段同步调用生命周期。这个解决方案不是正式的方法,但确实解决了问题。在此留作一种思考和探索的方式,供读者品味。知识点总结:依赖注入配置提供者可以创建实例,但是需要注意的是,实例会被当作普通的Service类,不能有完整的生命周期。3.7更多玩法:自定义更换平台,实现让Angular框架运行在Terminal终端的交互。可以参考这篇博文:《Rendering Angular applications in Terminal》图24替换RendererFactory2渲染器等内容,让Angular运行在终端上作者替换RendererFactory2等渲染器,使得Angular应用程序可以运行在终端上。这就是Angular设计的灵活性,强大到连平台都可以更换的灵活性。详细替换详情可查看原文,此处不再展开。知识点总结:依赖注入的强大之处在于provider可以自行配置,最终实现替换逻辑。4小结本文介绍了控制反转的依赖注入模式及其好处,介绍了Angular中的依赖注入如何找到依赖,如何配置provider,如何使用limited和filtered装饰器得到想要的实例,进一步通过N个案例分析如何结合依赖注入的知识点来解决开发编程中遇到的问题。通过正确理解依赖查找过程,我们可以在准确的位置配置提供者(案例1和2),用单例替换其他实例(案例4和5),甚至可以跨越嵌套组件包的限制进行连接提供的例子(Case3)或者使用提供的方法曲线实现指令实例化(Case6)。其中,案例5看似简单的替换,但是要能够写出可以替换的代码结构,需要对注入模式有深入的理解,对各个函数进行更好合理的抽象。如果抽象不合适,就无法依赖它。注射以获得最大效果。注入模式为模块的可插拔、插件化、部件化提供了更多可能的空间,降低了耦合度,增加了灵活性,使模块之间可以更优雅、更和谐地协同工作。强大的依赖注入功能,除了可以优化组件的通信路径,更重要的是还可以实现控制反转,将更多的切面编程面暴露给被封装的组件,一些业务特定逻辑的实现也可以成为灵活的。.前文推荐AngularCLI下自定义Webpack配置方式和自定义loader处理案例实战web界面暗黑模式和主题开发20行代码为你的项目添加DevUI主题切换能力