从0开发属于自己的nestjs框架的mini 版 —— ioc篇
如今,nodejs的框架也是层出不穷,偏向向底层的有 express、koa、 Fastify,偏向于上层有阿里的 Egg、thinkjs 、还有国外的 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篇的更多相关文章
- [开源]CSharpFlink(NET 5.0开发)分布式实时计算框架,PC机10万数据点秒级计算测试说明
github地址:https://github.com/wxzz/CSharpFlinkgitee地址:https://gitee.com/wxzz/CSharpFlink 1 计算 ...
- 解决.VS2012+EF5.0开发的网站在window server2003上无法部署的问题
(一)前 言 最近一个月使用VS2012(默认框架是.net f ...
- 解决.VS2012+EF5.0开发的网站在window server2003上无法部署的问题(转载)
转载:http://www.cnblogs.com/eggTwo/p/3653825.html (一)前 言 ...
- 用SignalR 2.0开发客服系统[系列2:实现聊天室]
前言 交流群:195866844 上周发表了 用SignalR 2.0开发客服系统[系列1:实现群发通讯] 这篇文章,得到了很多帮助和鼓励,小弟在此真心的感谢大家的支持.. 这周继续系列2,实现聊天室 ...
- iOS开发系列—Objective-C之Foundation框架
概述 我们前面的章节中就一直新建Cocoa Class,那么Cocoa到底是什么,它和我们前面以及后面要讲的内容到底有什么关系呢?Objective-C开发中经常用到NSObject,那么这个对象到底 ...
- qooxdoo 3.0 发布,JavaScript 的 GUI 框架
qooxdoo 3.0 是一个主要的版本,包含很多新特性和内部的改动,qooxdoo 3.0 是一个通用的 JS 框架,主要改进体现在 qx.Desktop, qx.Mobile, 和 qx.Webs ...
- Winform开发框架之客户关系管理系统(CRM)的开发总结系列2-基于框架的开发过程
在上篇随笔<Winform开发框架之客户关系管理系统(CRM)的开发总结系列1-界面功能展示>中介绍了我的整个CRM系统的概貌,本篇继续本系列的文章,介绍如何基于我的<winform ...
- JAVA开发Web Service几种框架介绍
郑重声明:此文为转载来的,出处已不知了,侵告删. 在讲Web Service开发服务时,需要介绍一个目前开发Web Service的几个框架,分别为Axis,axis2,Xfire,CXF以及JWS( ...
- C# 6 与 .NET Core 1.0 高级编程 - 38 章 实体框架核心(上)
译文,个人原创,转载请注明出处(C# 6 与 .NET Core 1.0 高级编程 - 38 章 实体框架核心(上)),不对的地方欢迎指出与交流. 章节出自<Professional C# 6 ...
- 用函数式编程,从0开发3D引擎和编辑器(一)
介绍 大家好,欢迎你踏上3D编程之旅- 本系列的素材来自我们的产品:Wonder-WebGL 3D引擎和编辑器 的整个开发过程,探讨了在从0开始构建3D引擎和编辑器的过程中,每一个重要的功能点.设计方 ...
随机推荐
- Sql Server 数据库事务与锁,同一事务更新又查询锁?期望大家来解惑
我有一个People表,有三行数据: 如果我们没详细了解数据库事务执行加锁的过程中,会不会有这样一个疑问:如下的这段 SQL 开启了事务,并且在事务中进行了更新和查询操作. BEGIN TRAN up ...
- 微擎删除分类无法删除解决-select in效率低解决办法
今天朋友微擎后台微网站里的分类要删除,可是怎么删除也不能删除,同样的系统另一套却可以迅速删除. 后来查询到是查询语句的问题,朋友的平台用户量太大,数据太大,用了以下语句,造成效率太低: SELECT ...
- 代码打包的可视化数据分析图: webpack-bundle-analyzer 的使用
先看webpack-bundle-analyzer的效果图(官方效果图): 通过使用webpack-bundle-analyzer可以看到项目各模块的大小,可以按需优化 1.先安装 npm insta ...
- 2023-02-12:给定正数N,表示用户数量,用户编号从0~N-1, 给定正数M,表示实验数量,实验编号从0~M-1, 给定长度为N的二维数组A, A[i] = { a, b, c }表示,用户i报
2023-02-12:给定正数N,表示用户数量,用户编号从0~N-1, 给定正数M,表示实验数量,实验编号从0~M-1, 给定长度为N的二维数组A, A[i] = { a, b, c }表示,用户i报 ...
- 2020-08-27:OpenStack与Docker的区别?
福哥答案2020-08-27: 此答案来自qq群巨佬:openstack是个体系一个完整的方案系统 基于虚拟机 docker只是一个内核上特殊模式跑与其他系统进程隔离的进程的软件方案真正与docker ...
- 2021-02-01:Redis 集群会有写操作丢失吗?
福哥答案2021-02-01: 以下情况可能导致写操作丢失:1.过期 key 被清理.2.最大内存不足,导致 Redis 自动清理部分 key 以节省空间.3.主库故障后自动重启,从库自动同步.4.单 ...
- 洛谷P3374 【模板】树状数组 1-(单点修改,区间查询)
题目描述 如题,已知一个数列,你需要进行下面两种操作: 将某一个数加上 x 求出某区间每一个数的和 输入格式 第一行包含两个正整数 n,m,分别表示该数列数字的个数和操作的总个数. 第二行包含 n 个 ...
- Python基础 - 第一个python程序
Python程序是什么? Python源程序就是一个特殊格式的文本文件,可以使用任意文本编辑器软件做python的开发,python的文件扩展名为 .py 执行python程序的三种方式 直接调用解释 ...
- 在asp.net core web api中添加efcore使用codefirst
首先创建webapi项目,我这里使用的版本是.net6 在nuget中添加对应的工具包 红框标出来的是对应的数据库扩展包,mysql用mysql版,sqlserver用sqlserver版,选择正确的 ...
- 终于把 7 年前的 Docker Hub 账号恢复了
折腾 docker,向 Docker Hub 提交镜像的时候发现原来自己在 2014 年就已经注册过 Docker Hub 的账号了,而且在 https://hub.docker.com/u/shen ...