nodejs常用框架,nodejs运行机制

  nodejs常用框架,nodejs运行机制

  本文带你了解Node的后端框架Nest.js,介绍Nestjs模块机制的概念和实现原理。希望对你有帮助!

  node.js速度课程简介:进入学习

  Nest提供了模块机制,通过在模块装饰器中定义提供者、导入、导出和提供者构造函数来完成依赖注入,通过模块树来组织整个应用的开发。按照框架本身的约定直接实现一个应用程序是没有问题的。但就我个人而言,对框架声明的依赖注入、控制反转、模块、提供者、元数据、相关修饰等缺乏更清晰系统的认识。

  看起来我能理解也能理解,但让我从头解释清楚。我解释不清楚。于是经过一番探索,我得到了这篇文章。从现在开始,我们从新开始,进入正文。

  

1 两个阶段

  1.1 Express、Koa

  一种语言及其技术社区的发展过程,必然是从底层功能逐渐丰富发展起来的,就像树根慢慢长成树枝,然后长满叶子的过程一样。更早的时候,Nodejs出现了Express、Koa等基础Web服务框架。可以提供非常基本的服务能力。基于这个框架,社区中开始诞生大量的中间件和插件,为框架提供更丰富的服务。我们需要自己组织应用依赖,搭建应用脚手架,灵活繁琐,也有一定的工作量。

  经过发展,诞生了一些生产效率更高、规则更统一的框架,开始了一个新的阶段。

  1.2 EggJs、Nestjs

  为了更适合快速生产和应用,开发了EggJs、NestJs、Midway等统一标准化、开箱即用的框架。这种框架通过实现底层的生命周期,将一个应用的实现抽象成一个通用的、可扩展的过程,我们只需要按照框架提供的配置方式就可以简单地实现应用。框架实现了程序的过程控制,我们只需要把自己的零件组装到合适的位置,看起来更像流水线作业。每个流程划分明确,也节省了大量的实施成本。

  1.3 小结

  以上两个阶段只是一个铺垫。我们大致可以知道,框架的升级提高了生产效率。为了实现框架的升级,将引入一些设计思想和模式。Nest中出现了控制反转、依赖注入、元编程等概念。大家说说吧。

  

2 控制反转和依赖注入

  2.1 依赖注入

  一个应用其实就是很多抽象类,它们互相调用实现应用的所有功能。随着应用程序代码和功能越来越复杂,项目的维护会越来越困难,因为类越来越多,它们之间的关系也越来越复杂。

  比如我们用Koa来开发我们的应用,Koa本身主要实现一套基本的Web服务能力。在实现应用的过程中,我们会定义很多类,这些类的实例化和相互依赖关系将由我们在代码逻辑中自由组织和控制。每个类的实例化都是我们手动新建的,我们可以控制一个类是只实例化一次然后共享,还是每次都实例化。下面的类B依赖于A,每次B被实例化,A就会被实例化一次,所以对于每个实例B,A都是一个非共享的实例。

  A级{}

  //B

  B类{

  构造器(){

  this . A=new A();

  }

  }下面的C是获取的外部实例,所以多个C实例是共享的app.a实例。

  A级{}

  //C

  const app={ };

  app . A=new A();

  C类{

  构造器(){

  this . a=app . a;

  }

  }下面的D是通过构造函数参数传入的,可以一次传入一个非共享的实例,也可以是共享的app.a实例(D和F共享app.a),由于现在是作为参数传入,所以我也可以传入一个X类实例。

  A级{}

  X类{}

  //D

  const app={ };

  app . A=new A();

  D级

  建筑商(a){

  this.a=a

  }

  }

  F级

  建筑商(a){

  this.a=a

  }

  }

  新D(应用程序a)

  新F(应用程序a)

  新D(新X())这边是依赖注入。B所依赖的a通过传递值注入到B中。通过构造函数注入(传值)只是一种实现方式,也可以通过调用set方法传入,或者其他任何方式,只要内部能传入一个外部依赖即可。就这么简单。

  A级{}

  //D

  D级

  setDep(a){

  this.a=a

  }

  }

  const d=新D()

  新A())2.2 All in 依赖注入?

  随着迭代的进行,似乎B将根据不同的前提条件而变化。比如前置条件一,this.a,需要传入A的一个实例,前置条件二,this.a,需要传入x的一个实例,这个时候我们就要开始做实际的抽象了。我们将把它转换成类似上面D的依赖注入的方式。

  当初我们在实现应用的时候,会在满足当时需求的情况下实现B类和C类的编写,这本身是没有问题的。经过几年的项目迭代,我们可能不会移动这部分代码。如果考虑后期扩展,会影响开发效率,不一定能派上用场。所以,很多时候,我们遇到需要抽象的场景,然后抽象出一些代码。

  //转换前

  B类{

  构造器(){

  this . A=new A();

  }

  }

  新B()

  //转换后

  D级

  建筑商(a){

  this.a=a

  }

  }

  新D(新A())

  New D(new X())按照现在的开发模式,会有三种类型的CBD存在,B和C有一定几率发展成D,我们每次升级D的抽象过程,都会需要重构代码,这是一个实现成本。

  这里有一个例子来说明在没有任何约束或规定的开发模式中。我们可以自由编写代码来实现各种类之间的依赖控制。在完全开放的环境下,是非常自由的。这是一个刀耕火种的原始时代。由于没有固定的代码开发模式和最高的行动纲领,随着不同开发人员的介入或者同一开发人员在不同时间段编写代码的差异,在代码增长的过程中依赖关系会变得非常不清晰,共享实例可能会被多次实例化,浪费内存。从代码来看,很难理解一个完整的依赖结构,代码可能会变得非常难以维护。

  然后我们每次定义一个类,都是按照依赖注入的方式来写,都是像D一样写,这样就把C和B的抽象过程提前了,这样后期扩展更方便,转换成本也降低了。所以这叫All in依赖注入,也就是我们所有的依赖都是通过依赖注入来实现的。

  但是前期的实施成本又变高,团队协作很难做到团结和坚持,最终可能会失败。这也可以定义为过度设计,因为额外的实现成本不一定带来收益。

  2.3 控制反转

  既然我们已经约定了使用依赖注入的统一方式,是否可以通过框架的底层封装实现一个底层控制器,约定一个依赖配置规则,让控制器根据我们定义的依赖配置来控制实例化过程和依赖共享,帮助我们实现类管理。这种设计模式叫做控制反转

  当你第一次听说反向控制时,可能很难理解。控制是什么意思?逆转什么?

  前面我们提到过,Koa应用的实现,所有类完全由我们自由控制,所以可以看作是一种常规的程序控制方式,也就是所谓的“控制前移”。我们使用Nest,它在底层实现了一组控制器。我们只需要在实际开发过程中按照协议写配置代码,框架程序会帮我们管理类的依赖注入,所以我们称之为:控制反转。

  本质上是把程序的实现过程交给框架程序统一管理,控制权由开发者交给框架程序。

  举个现实的例子,一个人本来自己开车上班,目的是为了到公司。它自己开车,自己控制路线。如果他交出开车的控制权,也就是去赶公交车,他只需要选择相应的班车就可以到达公司。单从控制的角度来说,人是解放的,只要记得坐那趟车就行了,出错的几率也小了,人也放松了很多。总线系统是控制器,总线线路是约定的配置。

  通过上面的实际对比,我想我可以稍微理解一下操控反转了。

  2.4 小结

  从Koa到Nest,从前端的JQuery到Vue React。其实都是通过框架一步步封装起来,解决上个时代效率低下的问题。

  上面的Koa应用开发用非常原始的方式控制依赖和实例化,类似于前端的JQuery操作dom。这种原语方式叫做控制正向,而Vue React就像Nest提供了一层程序控制器,都可以叫做控制反向。这也是个人理解。如果有问题,希望上帝指出来。

  先说一下Nest中的module @Module。依赖注入和控制反转都需要它作为媒介。

  

3 Nestjs的模块(@Module)

   Nestjs实现了控制反转,规定了配置模块(@module)的导入、导出和提供者的管理提供者是类的依赖注入。

  提供者可以理解为在当前模块中注册并实例化一个类,下面的A和B在当前模块中实例化。如果B引用构造函数中的A,则它是当前模块化A的被引用实例。

  从“@nestjs/common”导入{ Module };

  从“”导入{ ModuleX }。/moduleX ;

  从“”导入{ A }。/A ;

  从导入{ B }。/B ;

  @模块({

  进口:[ModuleX],

  提供商:[A,B],

  出口:[A]

  })

  导出模块化的类{}

  //B

  B类{

  构造函数(a:A){

  this.a=a

  }

  }exports是指在当前模块中的提供者中实例化的类,作为可以被外部模块共享的类。比如现在实例化ModuleF的C类,我想直接注入ModuleD的A类实例。在模块中设置exports)A,通过模块中的imports导入模块。

  按照下面的写法,控制反转程序会自动扫描依赖关系。首先,查看模块的提供者中是否有提供者A。如果没有提供者A,请在导入的模块中查找实例A。如果有,获取模块化的A实例并将其注入C实例。

  从“@nestjs/common”导入{ Module };

  从“”导入{ ModuleD}。/moduleD ;

  从导入{ C }。/C ;

  @模块({

  进口:[模块化],

  提供商:[C],

  })

  导出类ModuleF {}

  //C

  C类{

  构造函数(a:A){

  this.a=a

  }

  }所以如果想让外部模块使用当前模块的类实例,必须先在当前模块的提供者中定义实例化的类,然后再定义导出这个类,否则会报错。

  //正确

  @模块({

  提供商:[A],

  出口:[A]

  })

  //错误

  @模块({

  提供商:[],

  出口:[A]

  })这里还是提一嘴ts的知识点

  出口C类{

  构造函数(私有a: A) {

  }

  }因为TypeScript支持将构造函数参数(private、protected、public、readonly)隐式自动定义为class属性,所以没有必要使用this.a=a .在Nest中是这样写的。

  

4 Nest 元编程

  元编程的概念体现在Nest框架中,其中的控件反转和装饰器是元编程的实现。大致可以理解为,元编程的本质还是编程,只是中间有一些抽象的程序。这个抽象程序可以识别元数据(比如@Module中的对象数据),这其实是一种把其他程序当作数据的扩展能力。我们正在编写这样一个抽象程序,这就是元编程。

  4.1 元数据

  嵌套文档中经常提到元数据。如果第一次看到元数据的概念,会比较混乱。你需要习惯用接触的时间去理解,不必太纠结。

  元数据的定义是:描述数据的数据,主要是描述数据属性的信息,或者是描述程序的数据。

  Nest中@Module配置的导出、提供者、导入、控制器都是元数据,因为是用来描述程序关系的数据。这些数据信息并不是显示给最终用户的实际数据,而是由框架程序读取和识别的。

  4.2 Nest 装饰器

  如果你看一下Nest中的装饰器源代码,你会发现几乎每个装饰器本身都只通过reflect-metadata定义了一个元数据。

  @可注射装饰

  导出函数可注入(选项?injectable options):class decorator {

  return (target: object)={

  reflect . definemetadata(injectible _ WATERMARK,true,target);

  reflect . definemetadata(SCOPE _ OPTIONS _ METADATA,OPTIONS,target);

  };

  }这里有反射的概念,反射很好理解。以@Module decorator为例定义元数据提供者,但是只有类被传入providers数组。程序实际运行时,提供者中的类会被框架程序自动实例化成为提供者,不需要开发者展示的实例化和依赖注入。类只有在模块中实例化后才成为提供者。提供者中的类被反射为提供者,控件反转是使用的反射技术。

  再比如数据库中的ORM(对象关系映射)。使用ORM时,只需要定义表字段,ORM库会自动将对象数据转换成SQL语句。

  const data=table model . build();

  data . time=1;

  data.browser= chrome

  data . save();

  //SQL: insert into tablename (time,browser) [{time: 1, browser: chrome}] ORM库正好使用了反射技术,用户只需要关注字段数据本身。对象通过ORM库反映到SQL执行语句中,开发人员只需要关注数据字段就可以了,不用写SQL。

  4.3 reflect-metadata

  Reflect-metadata是一个反射库,Nest用它来管理元数据。Reflect-metadata使用WeakMap创建全局单个实例,并设置和获取修饰对象(类、方法等)的元数据。)通过set和get方法。

  //随便看看。

  var _WeakMap=!使用WeakMap===function 的多填充类型?weak map:CreateWeakMapPolyfill();

  var Metadata=new _ weak map();

  函数定义元数据(){

  OrdinaryDefineOwnMetadata(){

  getorcreatematedatamap(){

  var target metadata=metadata . get(O);

  if(is undefined(target metadata)){

  如果(!创建)

  返回未定义的;

  target metadata=new _ Map();

  Metadata.set(O,target metadata);

  }

  var metadata map=target metadata . get(P);

  if (IsUndefined(metadataMap)) {

  如果(!创建)

  返回未定义的;

  meta data Map=new _ Map();

  targetMetadata.set(P,metadata map);

  }

  返回元数据映射;

  }

  }

  }reflect-metadata将被装饰人的元数据存储在全局singleton对象中,进行统一管理。Reflect-metadata不实现具体的反射,而是提供一个工具库来辅助反射。

  

5 最后

  现在我们来看看前面的问题。

  1和2我觉得从正面看很清楚。如果还是有点模糊,建议再读一遍,查阅一些其他的文章和资料,通过不同作者的思考来帮助你理解知识。

  5.1 问题 [3 4] 总述:

  Nest使用反射技术,实现控件反转,提供元编程能力。开发者使用@Module decorator来修饰类和定义元数据(providers\imports\exports),元数据存储在全局对象中(使用reflect-metadata library)。程序运行后,嵌套框架内的控制程序读取并注册模块树,扫描元数据并实例化类以成为提供者。它根据模块元数据中providers\imports\exports的定义,在所有模块提供者中搜索当前类的其他依赖类的实例(提供者),然后通过构造函数注入。

  本文概念较多,也并没有做太详细的解析,概念需要时间慢慢理解,如果一时理解不透彻,也不必太过着急。好吧,就到这里,这篇文章还是花费不少精力,喜欢的朋友期望你能一键三连

  更多关于node的信息,请访问:nodejs教程!以上是了解Node.js Nestjs框架的模块机制,说说实现原理的细节。更多请关注我们的其他相关文章!

郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。

留言与评论(共有 条评论)
   
验证码: