依赖注入DI(DependencyInjection)是编程领域中非常常见的一种设计模式,指的是应用程序(如服务或其他组件)所需的依赖通过构造函数参数或自动注入的过程属性注入。这样做的好处是可以降低组件之间的耦合度,更容易测试和维护。我们先举一个简单的例子。我们有两个简单的类A和B。B类依赖于A类。我们在B类中实例化它并调用它的方法:classA{constructor(name){this.name=name;}log(){console.log("name:",this.name);}}B类{a=newA("ConardLi");开始(){这个。一个日志();}}constb=newB();b.start();但是这种写法非常不灵活,因为A类是一个依赖,它的初始化逻辑硬编码到B类中,如果我们要添加或修改其他依赖,就必须不断地修改B类。按照设计思路依赖注入,我们可以改写代码如下:classA{constructor(name){this.name=name;}log(){console.log("name:",this.name);}}classB{constructor(a){this.a=a;}start(){this.a.log();}}consta=newA();constb=newB(a);b.start();代码只做了一点改动。最核心的变化是我们把A类和B类的实现完全分离了,他们不再需要关心依赖的实例化,因为我们在外面提到了最重要的依赖注入。这也是为什么我们经常把依赖注入和控制反转IoC(InversionofControl)放在一起说。控制反转是关于转移创建对象的控制权。过去,创建对象的主动权和时机都是由我们自己掌握的,现在这种权力已经转移到第三方。我们可能看不到这么简单的代码有什么好处,但是在大型代码库中,这种设计可以显着帮助我们减少样板代码,创建和连接依赖项的工作由单个程序处理,我们不需要担心创建特定类所需的类的实例。在JavaScript的各大框架中,依赖注入的设计模式也扮演着非常重要的角色。Angular、Vue.js、Next.js等框架都使用了依赖注入的设计模式。JavaScript框架Angular中的依赖注入大量应用了Angular中依赖注入的设计思想。Angular使用依赖注入来管理应用程序各个部分之间的依赖关系,以及如何将这些依赖注入到应用程序中,例如,您可以使用依赖注入来注入服务、组件、指令、管道等。例如,我们现在有一个日志管理工具类,我们可以使用Injectable来指定它为一个可注入对象。//logger.service.tsimport{Injectable}from'@angular/core';@Injectable({providedIn:'root'})exportclassLogger{writeCount(count:number){console.warn(count);}}then在组件中使用时,无需实例化,直接从构造函数参数中取出自动注入的对象即可://hello-world-di.component.tsimport{Component}from'@angular/core';import{Logger}from'../logger.service';@Component({selector:'hello-world-di',templateUrl:'./hello-world-di.component.html'})导出类HelloWorldDependencyInjectionComponent{count=0;构造函数(私人记录器:记录器){}onLogMe(){this.logger.writeCount(this.count);这个。计数++;}}Vue.js在Vue.js中,提供和注入实际上使用依赖注入设计模式。provide属性可以用来在父组件中提供一个值,这个值可以注入到父组件的所有子组件中。exportdefault{name:'Parent',provide(){return{user:this.user};},data(){return{user:{name:'John',age:30}};}};inject属性可以用来在子组件中注入父组件提供的值。//子组件exportdefault{name:'Child',inject:['user'],computed:{userName(){returnthis.user.name;}}};React.js在React.js中,并没有直接使用依赖注入,但是我们还是可以使用一些第三方库来实现的。例如,我们可以使用InversifyJS提供的可注入装饰器将类标记为可注入。import{injectable}from"inversify";exportinterfaceIProvider{provide():T;}@injectable()exportclassNameProviderimplementsIProvider{provide(){return"World";}}}在component中,我们可以直接调用注入的provide方法,组件不需要关心它的实现。import*asReactfrom"react";import{IProvider}from"./providers";exportclassHelloextendsReact.Component{privatereadonlynameProvider:IProvider;render(){return你好{this.nameProvider.provide()}!
;}}手动实现依赖注入前面我们提到的InversifyJS其实就是一个专门用来实现依赖注入的工具库。主要由injectable、inject等几个装饰器组成。是啊,这么神奇的功能是怎么实现的呢?让我们手动实现它。首先,让我们澄清一个需求场景。假设我们要使用Koa框架开发一个简单的Node.js服务。在Koa中,Controller用于处理用户的请求和响应。它负责接收用户请求,然后调用相应的服务或业务逻辑进行处理,最后将处理结果返回给用户。Service用于封装业务逻辑和数据处理,负责实现应用的核心功能。Service通常由多个Controller调用,它们是松耦合的。我们希望使用两个装饰器来实现Service的自动依赖注入:exportdefaultclassUserControllerextendsController{@Injectuser:UserService;@UseService异步列表(ctx:ThriftContext):Promise{constuser=awaitthis.user.findAll({id:1000});控制台日志(1,用户);}}我们在实现的时候可能会用到两个非常重要的API,MetadataReflectionAPI和DecoratorAPI,我们先分别回顾一下它们的基础知识。DecoratorAPI装饰器模式是一种经典的设计模式,其目的是在不修改装饰器源代码(如函数、类等)的情况下为装饰器添加/删除某些功能。一些现代编程语言在语法层面提供了对装饰器模式的支持,每种语言中的现代框架都广泛使用装饰器。主要用途分为两类:收集用户自定义类/函数的信息(例如,用于生成路由表,用于实现依赖注入等)以增强用户自定义类/函数,以及添加额外的功能。我们目前比较常用的装饰器是TypeScript的实验性装饰器,还有ECMAScript中的DecoratorAPI还处于遗留阶段。下面是它的用法:装饰类时,装饰器方法一般会接收一个目标类作为参数,如下以给类添加静态属性和原型方法为例:constaddField=target=>{target.age=17;target.prototype.speak=function(){console.log('xxx');};};@addFieldclassPeople{}console.log(People.age);consta=newPeople();a.speak();类属性装饰器可以用于类,一般接收三个参数:target:被修饰的类名:类成员的名称descriptor:属性描述符,对象会把这个参数传给Object.defineProperty下面是一个例子,可以修改类的属性只读:functionreadonly(target,name,descriptor){descriptor.writable=false;返回描述符;}classPerson{@readonlyname='person'}constperson=newPerson();person.name='tom';元数据反射APIReflect是一个内置的JavaScript对象,它提供了一组用于操作对象的方法。它类似于其他内置对象,但其目的是提供一组通用的操作对象的方法。ReflectMetadata是ES7的一个提案,主要用于声明时添加和读取元数据。reflect.getMetadata('design:type',target,key)可以用来获取类target中属性key的类型信息:functionInject(){returnfunction(target:any,key:string,descriptor:PropertyDescriptor){consttype=Reflect.getMetadata('design:type',target,key);控制台日志(类型);//[类服务]返回描述符;};}exportdefaultclassWebsiteControllerextendsController{@Inject()service:Service//...}Reflect.getMetadata('design:paramtypes',target,key)可以用来获取属性key的函数参数类型在班级目标中;Reflect.getMetadata('design:returntype',target,key)可用于获取类target中属性key的函数返回值类型。除了获取固定的类型信息,还可以自定义MetaData,在合适的时候获取其值。示例如下:functionclassDecorator():ClassDecorator{returntarget=>{//在类上定义元数据,键为`classMetaData`,值为`a`Reflect.defineMetadata('classMetaData','a',目标);};}@classDecorator()classSomeClass{}Reflect.getMetadata('classMetaData',SomeClass);//'a'那么,有了这些知识,我们就可以手动实现一个依赖注入装饰器了。实现依赖注入,再次明确我们的需求:在不同服务的Controllers中共享Service,在使用Service时,可以自动获取注入的Service实例,同时可以在Service中获取请求的Context信息。首先我们来实现,Injectdecorator:需要使用哪些Service注册到Controller中通过design:type获取Service的类型信息通过自定义元数据存储在Controller中使用了哪些ServiceFunctionInject(target:any,key:string){console.log(`注册控制器:${target}服务:${key}`);//获取当前Service的类型constserviceClass=Reflect.getMetadata('design:type',target,key);//获取当前Controller注册的服务列表constserviceList=Reflect.getMetadata(META_KEY_CONTROLLER_SERVICE,target)||[];//添加当前ServiceReflect.defineMetadata(META_KEY_CONTROLLER_SERVICE,[...serviceList,{serviceClass,serviceName:key}],target);}然后UseService装饰器:取出存储在Controller和Service的对应信息请求进来时的元数据,实例化Service,将Context传给Servicedescriptor.value=asyncfunction(...args:any){//获取当前请求的Contextconst[ctx]=args;//获取当前Controller绑定的ServiceconstserviceList=Reflect.getMetadata(META_KEY_CONTROLLER_SERVICE,目标)||[];控制台日志(服务列表);for(leti=0;i{constuser=awaitthis.用户。找到所有();安慰。日志(1,用户);}}