如今,nodejs的框架也是层出不穷,偏向向底层的有 expresskoaFastify,偏向于上层有阿里的 Eggthinkjs 、还有国外的 nestjs

在这里我更喜欢 nestjs,主要是其用了不同于其他框架的思想,采用分层,AOP(面向切面编程),OOP(面向对象编程)的设计思想。

如果想要自己写一个类似的框架,该如何入手呢,下面我将从0开始,带大家看看如何利用这种思想写一个属于nodejs框架,在此之前,先了解什么是AOP编程,还有 Ioc 和 Di 是什么东西 (如果了解的可以跳过,如果不对的话可以留言指正,谢谢大神)

分两部分: 概念篇和实践篇

概念:

Ioc: 控制反转(Inversion of Control) 的缩写,开发者不需要关心对象的过程,交给容器处理

Di: 依赖注入(Dependency Injection) 的缩写,容器创建对象实例时,同时为这个对象注入它所依赖的属性

1、本质:

  • 是面向对象编程中的一种设计原则,最常见的方式叫做依赖注入,依赖注入(DI)和控制反转(IoC)是从不同的角度描述的同一件事情,就是指通过引入 IoC 容器,利用依赖关系注入的方式,实现对象之间的解耦

2、图解

虚线表示可以注入, 实线指向容器可以反转控制

1、 class A ,classB,ClassC 实线 都指向容器,由容器处理实例化操作

2、 class A 虚线指向 classB,代表 class B 需要注入 classA 作为实例化的参数; class B 指向 class C 同理

一句话理解: 将所有类的实例化交给容器,类实例化要的参数由容器提供

3、 npm 代表库

  • inversify:node 端 ioc 框架
  • nestjs:node 端 web 框架
  • Angular:前端框架

实践:

前提: 需要安装 reflect-metadata 依赖库,

核心: 两个装饰器,一个容器,

  • Inject: 是装饰器,是构造函数参数的注入器
  • Injectable : 是装饰器, 用于注入相关类构造函数的依赖项的元数据
  • Container: 管理对象实例化的容器

重点 api:

  • Reflect.defineMetadata(metadataKey, metadataValue, target, propertyKey):定义对象或属性的元数据
  • Reflect.getMetadata(metadataKey, metadataValue, target, propertyKey):获取对象或属性的原型链上的元数据键的元数据值
  • design:paramtypes:内置的元数据键 metadataKey;获取构造函数的参数

执行流程:

  • 注册:首先将所有的要实例化的类和类实例化所需要的参数交给容器
  • 分类:将容器中添加的类和普通参数进行分类
  • 实例化:对类进行实例化,当实例化过程需要的参数,也是需要类的时候,判断是否已经实例过了,否则进行递归实例化处理

1、先声明一些常量、类型和工具方法

util.ts

/*************** 常量声明************************* */

// 依赖注入(DI)的元数据key
export const InjectKey = "INJECT_METADATA_KEY"; // 类进行控制反转的元数据key
export const InjectableKey = "INJECTABLE_METADATA_KEY"; // 内置的获取构造函数的元数据key
export const DesignParamtypes = "design:paramtypes"; /******************ts类型声明********************** */ /**
* 类声明
*/
export interface Type<T> extends Function {
new (...args: any[]): T;
} //第一种入参类型,需要容器处理实例化的数据
export interface ClassProvider<T> {
provide: string | Type<T>;
useClass: Type<T>;
} //第二种入参类型,不需要容器处理实例化的数据
export interface ValueProvider<T> {
provide: string | Type<T>;
useValue: any;
} /**
* 三种类型的写法
*/
export type Provider<T> = Type<T> | ValueProvider<T> | ClassProvider<T>; /*************** 工具方法************************* */
/**
* 判定是控制反转的提供者(类)
* @param target
* @returns
*/
export const isInjectable = (target: any) => {
return (
typeof target === "function" && Reflect.getMetadata(InjectableKey, target)
);
}; /**
* 判断是否是 { provide,useClass }类型的写法
* @param arg
* @returns
*/
export function isClassProvider<T>(arg: unknown): arg is ClassProvider<T> {
return (arg as any).useClass !== undefined;
} /**
*判断是否是 { provide,useValue } 类型的写法
* @param arg
* @returns
*/
export function isValueProvider<T>(arg: unknown): arg is ValueProvider<T> {
return (arg as any).useValue !== undefined;
}

2、Inject 实现

/**
* 这是一个装饰器
* @Inject 是构造函数参数的注入器
* @param token
* @returns
*/
export function Inject(token: any) {
return function (target: any, perperity: string, index: number) {
Reflect.defineMetadata(InjectKey, token, target, `index-${index}`);
};
}

3、Injectable 实现

/**
* 这是一个类装饰器
* @Injectable 标注该类是可以交给容器进行实例化,控制反转的
* @returns
*/
export const Injectable = () => {
return function (target: any) {
Reflect.defineMetadata(InjectableKey, true, target);
};
};

4、Container 实现

/**
* 控制反转(Ioc)和依赖注入(DI)
* 一个依赖注入的容器
*/
export class Container {
/**
* 缓存已经完成提供者在容器中实例化的创建
*/
private instanceMap = new Map<string | Type<any>, any>();
/**
* 缓存要加入的依赖类(提供者)
*/
private providerMap = new Map<string | Type<any>, Type<any>>(); constructor(providers: Array<Provider<any>> = []) {
this.init(providers);
}
/**
* 初始化
* @param providers
* @returns
*/
private init(providers: Array<Provider<any>> = []) {
providers.forEach((item) => this.add(item));
this.loading();
return this;
} /**
* 获取构造函数的参数
*/
private getConstructorParam<T>(target: Type<T>) {
let args = Reflect.getMetadata(DesignParamtypes, target) || [];
return args.map((item: any, index: number) => {
const injectMedate = Reflect.getMetadata(
InjectKey,
target,
`index-${index}`
);
//如果不是inject注入就是其他类型的注入,要考虑原始类型: [Function: String]、[Function: Number]...
let paramsToken = injectMedate == undefined ? item : injectMedate;
if (paramsToken === undefined) return paramsToken;
return this.get(paramsToken);
});
}
/**
* 对容器中 类(提供者)实例化
* @param provider
* @returns
*/
private injectWidthClassProvider(key: string | Type<any>, target: Type<any>) {
let args = this.getConstructorParam(target);
let instance = Reflect.construct(target, args);
this.instanceMap.set(key, instance);
return instance;
} /**
* 根据 注入容器的 类型获取对应的数据
* @param key
* @returns
*/ /**
* 加载容器中的对象(提供者)
* @returns
*/
public loading() {
this.providerMap.forEach((_, key) => this.get(key));
this.providerMap.clear();
return this;
} /**
* 添加要创建实例化的对象(提供者)
* @param value
*/
public add<T>(value: Provider<T>) {
if (isValueProvider(value)) {
this.instanceMap.set(value.provide, value.useValue);
} else if (isInjectable(value)) {
this.providerMap.set(value as Type<T>, value as Type<T>);
} else if (isClassProvider(value)) {
this.providerMap.set(value.provide, value.useClass);
}
return this;
} public get<T>(key: string | Type<T>) {
if (this.instanceMap.has(key)) {
return this.instanceMap.get(key);
}
if (this.providerMap.has(key) && isInjectable(this.providerMap.get(key))) {
return this.injectWidthClassProvider(key, this.providerMap.get(key));
} const errlog = `cannot Provider ${key} is not injectable`;
throw new Error(errlog);
}
/**
* 获取所有的实例
* @returns
*/
public getInstance() {
return this.instanceMap;
}
}

5、 测试用法

@Injectable()
class A {
constructor(@Inject("api") private api: string /** b:number **/) {
console.log("----实例化A:");
console.log("a-api", this.api);
}
} @Injectable()
class B {
constructor(@Inject("AA") private a: A, @Inject("api") private api: string) {
console.log("----实例化B:");
console.log("B:insA", this.a);
console.log("B:api", this.api);
}
}
@Injectable()
class C {
constructor(private b: B, @Inject("api") private api: string) {
console.log("----实例化C:");
console.log("C:insB", this.b);
console.log("C:api", this.api);
}
} let contaner = new Container([
C,
B,
{ provide: "AA", useClass: A },
{ provide: "api", useValue: 123 },
]); contaner.add({ provide: "a", useValue: "12345" }).loading();
/**
* log:
* ----实例化A:
a-api 123
----实例化B:
B:insA A { api: 123 }
B:api 123
----实例化C:
C:insB B { a: A { api: 123 }, api: 123 }
C:api 123
contaner: Container {
instanceMap: Map(5) {
'api' => 123,
'AA' => A { api: 123 },
[class B] => B { a: [A], api: 123 },
[class C] => C { b: [B], api: 123 },
'a' => '12345'
},
providerMap: Map(0) {}
}
*/
console.log("contaner:", contaner);
console.log("AA:", contaner.get("AA"));
console.log("A:", contaner.get(A));

总结:

1、以上就是关于nestjs 框架核心的设计思想AOP 的实现,一个mini 版本的ioc 框架的

2、这个只是阐述其核心思想的实现的

从0开发属于自己的nestjs框架的mini 版 —— ioc篇的更多相关文章

  1. [开源]CSharpFlink(NET 5.0开发)分布式实时计算框架,PC机10万数据点秒级计算测试说明

    github地址:https://github.com/wxzz/CSharpFlinkgitee地址:https://gitee.com/wxzz/CSharpFlink  1         计算 ...

  2. 解决.VS2012+EF5.0开发的网站在window server2003上无法部署的问题

    (一)前  言                                                                    最近一个月使用VS2012(默认框架是.net f ...

  3. 解决.VS2012+EF5.0开发的网站在window server2003上无法部署的问题(转载)

    转载:http://www.cnblogs.com/eggTwo/p/3653825.html (一)前  言                                             ...

  4. 用SignalR 2.0开发客服系统[系列2:实现聊天室]

    前言 交流群:195866844 上周发表了 用SignalR 2.0开发客服系统[系列1:实现群发通讯] 这篇文章,得到了很多帮助和鼓励,小弟在此真心的感谢大家的支持.. 这周继续系列2,实现聊天室 ...

  5. iOS开发系列—Objective-C之Foundation框架

    概述 我们前面的章节中就一直新建Cocoa Class,那么Cocoa到底是什么,它和我们前面以及后面要讲的内容到底有什么关系呢?Objective-C开发中经常用到NSObject,那么这个对象到底 ...

  6. qooxdoo 3.0 发布,JavaScript 的 GUI 框架

    qooxdoo 3.0 是一个主要的版本,包含很多新特性和内部的改动,qooxdoo 3.0 是一个通用的 JS 框架,主要改进体现在 qx.Desktop, qx.Mobile, 和 qx.Webs ...

  7. Winform开发框架之客户关系管理系统(CRM)的开发总结系列2-基于框架的开发过程

    在上篇随笔<Winform开发框架之客户关系管理系统(CRM)的开发总结系列1-界面功能展示>中介绍了我的整个CRM系统的概貌,本篇继续本系列的文章,介绍如何基于我的<winform ...

  8. JAVA开发Web Service几种框架介绍

    郑重声明:此文为转载来的,出处已不知了,侵告删. 在讲Web Service开发服务时,需要介绍一个目前开发Web Service的几个框架,分别为Axis,axis2,Xfire,CXF以及JWS( ...

  9. C# 6 与 .NET Core 1.0 高级编程 - 38 章 实体框架核心(上)

    译文,个人原创,转载请注明出处(C# 6 与 .NET Core 1.0 高级编程 - 38 章 实体框架核心(上)),不对的地方欢迎指出与交流. 章节出自<Professional C# 6 ...

  10. 用函数式编程,从0开发3D引擎和编辑器(一)

    介绍 大家好,欢迎你踏上3D编程之旅- 本系列的素材来自我们的产品:Wonder-WebGL 3D引擎和编辑器 的整个开发过程,探讨了在从0开始构建3D引擎和编辑器的过程中,每一个重要的功能点.设计方 ...

随机推荐

  1. C# 手写识别方案整理

    书写识别,网上的大佬们都有输出. 书写识别存在的2个问题: 直接拿官网的案例(将 Windows Ink 笔划识别为文本和形状 - Windows apps | Microsoft Learn),会发 ...

  2. 一文搞懂 Promise 新 Api allSettled 的用法和 all 区别,以及如何在不支持新特性的环境下实现一个 Polyfill

    开始 一文搞懂 Promise 新 Api allSettled 的用法和 all 区别,以及如何在不支持新特性的环境下实现一个 Polyfill allSettled 的用法 const runAl ...

  3. Stream方法的介绍

    文章目录 前言 Lambda表达式 格式 函数式接口 Stream的方法介绍 forEach filter collect count sum limit 和skip groupingBy reduc ...

  4. CS144 计算机网络 Lab4:TCP Connection

    前言 经过前面几个实验的铺垫,终于到了将他们组合起来的时候了.Lab4 将实现 TCP Connection 功能,内部含有 TCPReceiver 和 TCPSender,可以与 TCP 连接的另一 ...

  5. 2022-12-02:有a块草莓蛋糕,有b块芝士蛋糕,两人轮流拿蛋糕, 每次不管是谁只能选择在草莓蛋糕和芝士蛋糕中拿一种, 拿的数量在1~m之间随意, 谁先拿完最后的蛋糕谁赢。 返回先手赢还是后手赢。

    2022-12-02:有a块草莓蛋糕,有b块芝士蛋糕,两人轮流拿蛋糕, 每次不管是谁只能选择在草莓蛋糕和芝士蛋糕中拿一种, 拿的数量在1~m之间随意, 谁先拿完最后的蛋糕谁赢. 返回先手赢还是后手赢. ...

  6. 2022-06-25:给定一个正数n, 表示有0~n-1号任务, 给定一个长度为n的数组time,time[i]表示i号任务做完的时间, 给定一个二维数组matrix, matrix[j] = {a,

    2022-06-25:给定一个正数n, 表示有0~n-1号任务, 给定一个长度为n的数组time,time[i]表示i号任务做完的时间, 给定一个二维数组matrix, matrix[j] = {a, ...

  7. 2021-01-18:java中,HashMap的创建流程是什么?

    福哥答案2021-01-18: jdk1.7创建流程:三种构造器.1.初始容量不能为负数,默认16.2.初始容量大于最大容量时,初始容量等于最大容量.3.负载因子必须大于0,默认0.75.4.根据初始 ...

  8. 2021-12-09:二叉树展开为链表。 给你二叉树的根结点 root ,请你将它展开为一个单链表: 展开后的单链表应该同样使用 TreeNode ,其中 right 子指针指向链表中下一个结点,而左

    2021-12-09:二叉树展开为链表. 给你二叉树的根结点 root ,请你将它展开为一个单链表: 展开后的单链表应该同样使用 TreeNode ,其中 right 子指针指向链表中下一个结点,而左 ...

  9. 【Qt 6】读写剪贴板

    剪贴板是个啥就不用多介绍了,最直观的功能是实现应用程序之间数据共享.就是咱们常说的"复制"."粘贴"功能. 在 Qt 中,QClipboard 类提供了相关 A ...

  10. .Net8顶级技术:边界检查之IR解析(慎入)

    前言 C#这种语言之所以号称安全的,面向对象的语言.这个安全两个字可不是瞎叫的哦.因为JIT会检查任何可能超出分配范围的数值,以便使其保持在安全边界内.这里有两个概念,其一边界检查,其二IR解析.后者 ...