TypeScript依赖注入框架Typedi的使用、原理、源码解读

简介

typedi是一个基于TS的装饰器和reflect-metadata的依赖注入轻量级框架,使用简单易懂,方便拓展。

使用typedi的前提是安装reflect-metadata,并在项目的入口文件的第一行中声明import ‘reflect-metadata’,这样就会在原生的Reflect API上挂载metadata操作相关的API。

前提

  1. 项目中引入reflect-metadata依赖
pnpm add reflect-metadata
  1. 在项目的入口文件的第一行声明
import 'reflect-metadata'
  1. 在tsconfig.json中开启装饰器语法和装饰器元数据
{"compilerOptions": {"experimentalDecorators": true,"emitDecoratorMetadata": true}
}

当开启了emitDecoratorMetadata后,TS会自动为装饰器生成metadata,有3个metadata:

  • design:type 被装饰器修饰的目标的类型,即成员的类型
  • design:paramtypes 方法的参数的类型集合,是一个数组,只有被修饰是方法时,此metadata才有效,否则就是undefined
  • design:returntype 方法的返回值类型,只有方法独有的metadata

安装typedi

pnpm add typedi

注册实例到容器中

typedi是基于IOC和DI的思想,因此需要有一个容器来容纳所有的bean

有三种方式注册你的实例到容器Container中:

  • 使用@Service()修饰的类(声明式)
  • Token注册一个实例(手动式)
  • 用字符串来注册一个实例(手动式)

Token和字符串标识符可以用来注册类以外的其他值。Token和字符串标识符都可以注册任何类型的值,包括除undefined之外的原始值。它们必须在容器上用Container.set()函数设置,然后才能通过Container.get()请求它们。

使用@Service注入:

import 'reflect-metadata'
import { Service, Container } from 'typedi'@Service()
class Person {name: string = 'John'age: number = 30
}const obj = Container.get(Person) as Personconsole.log(obj)
// Person { name: 'John', age: 30 }

使用Token或字符串注入:

import 'reflect-metadata'
import { Service, Container, Token } from 'typedi'Container.set('message', 'Hello World')
console.log(Container.get('message')) // Hello Worldconst token = new Token('TOKEN_INDEX')
Container.set(token, 'Nice to meet you!')
console.log(Container.get(token)) // Nice to meet you!

依赖注入

三种方式注入依赖的实例:

  • 通过类的构造函数参数自动注入
  • 使用@Inject()装饰器来标注需要注入的属性
  • 直接使用Container.get()来获取实例,手动注入

构造函数参数注入

import 'reflect-metadata'
import { Service, Container, Token } from 'typedi'@Service()
class A {say(){console.log('A ...')}
}@Service()
class B {constructor(public a: A){}say(){console.log('B ...')this.a.say()}
}const b = Container.get(B) as B
b.say()
// B ...
// A ...

@Inject()属性注入

import 'reflect-metadata'
import { Service, Container, Token, Inject } from 'typedi'@Service()
class A {say(){console.log('A ...')}
}@Service()
class B {@Inject()a: Asay(){console.log('B ...')this.a.say()}
}const b = Container.get(B) as B
b.say()
// B ...
// A ...

bean的作用域

默认注入到容器中的实例都是单例的,即每次从容器中获取的对象都是同一个对象

import 'reflect-metadata'
import { Service, Container, Token, Inject } from 'typedi'@Service()
class Person {name = 'John Doe'age = 21
}const obj1 = Container.get(Person)
const obj2 = Container.get(Person)
// 判断两个对象是否是同一个对象
console.log(obj1 === obj2) // true

如果想要每次请求容器时,都会得到一个新的对象,可以这样做

import 'reflect-metadata'
import { Service, Container, Token, Inject } from 'typedi'@Service({ transient: true })
class Person {name = 'John Doe'age = 21
}const obj1 = Container.get(Person)
const obj2 = Container.get(Person)
// 判断两个对象是否是同一个对象
console.log(obj1 === obj2) // false

@Inject

@Inject是一个属性和参数装饰器,用于解决一个类的属性或构造函数参数的依赖

默认情况下,他能推断出属性或参数的类型,并初始化一个检测到的类型的实例,然而这种行为可以通过指定一个自定义的可构造类型、Token或已命名的Service作为第一个参数 来覆盖

属性注入

属性的类型是自动推断出来的,所以不需要定义所需的值来作为装饰器的参数

import 'reflect-metadata';
import { Container, Inject, Service } from 'typedi';@Service()
class InjectedExampleClass {print() {console.log('I am alive!');}
}@Service()
class ExampleClass {@Inject()withDecorator: InjectedExampleClass;withoutDecorator: InjectedExampleClass;
}const instance = Container.get(ExampleClass);/*** `instance`变量是一个ExampleClass实例* 其`withDecorator`属性包含一个InjectedExampleClass实例* 而`withoutDecorator`属性undefined*/
console.log(instance);instance.withDecorator.print();
// "I am alive!" (InjectedExampleClass.print 方法)
console.log(instance.withoutDecorator);
// undefined, 因为这个属性没有用@Inject装饰器标记

构造函数注入

构造函数注入,当一个类被@Service装饰器标注时,在构造器注入中不需要@Inject装饰器,TS会自动推断并为每个构造参数注入正确的类实例。

但是注意,@Inject可以用来覆盖注入的类型

import 'reflect-metadata';
import { Container, Inject, Service } from 'typedi';@Service()
class InjectedExampleClass {print() {console.log('I am alive!');}
}@Service()
class ExampleClass {constructor(@Inject()public withDecorator: InjectedExampleClass,public withoutDecorator: InjectedExampleClass) {}
}const instance = Container.get(ExampleClass);/*** `instance'变量是一个ExampleClass实例* 它同时具有
`withDecorator`和`withoutDecorator`属性* 都包含一个
InjectedExampleClass实例。*/
console.log(instance);instance.withDecorator.print();
// 输出 "I am alive!" (InjectedExampleClass.print function)
instance.withoutDecorator.print();
// 输出 "I am alive!" (InjectedExampleClass.print function)

明确请求目标类型

默认情况下,TypeDI将尝试推断属性和参数的类型并注入适当的类实例。当必要时,可以覆盖注入值的类型:

  • 通过@Inject( () => type),其中type是一个可构造的值(例如,一个类的定义)
  • 通过@Inject(myToken),其中myToken是一个Token类的实例
  • 通过@Inject(serviceName),其中serviceName是一个字符串,已经通过Container.set(serviceName, value)注册过了
import 'reflect-metadata';
import { Container, Inject, Service } from 'typedi';@Service()
class InjectedExampleClass {print() {console.log('I am alive!');}
}@Service()
class BetterInjectedClass {print() {console.log('I am a different class!');}
}@Service()
class ExampleClass {@Inject()inferredPropertyInjection: InjectedExampleClass;/*** 我们告诉TypeDI,用`BetterInjectedClass`类初始化。* 不管推断的类型是什么。*/@Inject(() => BetterInjectedClass)explicitPropertyInjection: InjectedExampleClass;constructor(public inferredArgumentInjection: InjectedExampleClass,/*** 我们告诉TypeDI,用`BetterInjectedClass`类初始化。* 不管推断的类型是什么。*/@Inject(() => BetterInjectedClass)public explicitArgumentInjection: InjectedExampleClass) {}
}/*** `instance`变量是一个 ExampleClass 的实例,同时具有* - `inferredPropertyInjection` 和 `inferredArgumentInjection` 属性* 都包含一个`InjectedExampleClass`实例* - `explicitPropertyInjection`和`explicitArgumentInjection`属性* 都包含一个`BetterInjectedClass'实例。*/
const instance = Container.get(ExampleClass);instance.inferredPropertyInjection.print();
// "I am alive!" (InjectedExampleClass.print function)
instance.explicitPropertyInjection.print();
// "I am a different class!" (BetterInjectedClass.print function)
instance.inferredArgumentInjection.print();
// "I am alive!" (InjectedExampleClass.print function)
instance.explicitArgumentInjection.print();
// "I am a different class!" (BetterInjectedClass.print function)

循环依赖

依赖注入最常见的问题就是循环依赖,因此为了避免循环依赖,我们需要为属性指明类型
在循环依赖的情况下,TS无法推断出属性的类型,就导致design:type为undefined,typedi就无法实例化,因此我们需要强制给出类型。

// Car.ts
@Service()
export class Car {@Inject(type => Engine)engine: Engine;
}// Engine.ts
@Service()
export class Engine {@Inject(type => Car)car: Car;
}

注意这种方式只能解决属性注入,不能解决构造参数的注入。

需要注意的是,通常循环依赖意味着:

  1. 模块间的指责分工不明
  2. 单个模块的指责过多(不满足单一职责原则)
  3. 缺少合理的抽象层

Service Token

在使用@Service()来注入一个实例到Container中时,我们可以给出@Service()参数,用来唯一标识这个实例,参数类型通常是字符串或Token类型。

使用字符串

import 'reflect-metadata'
import { Service, Token, Container, Inject } from 'typedi'@Service('userComponet')
class Person {name = 'john'
}@Service('userIndex')
class PersonController {@Inject('userComponet')obj: Personsay(){console.log('userIndex ... ',this.obj.name)}
}console.log((Container.get('userComponet') as Person).name); // john(Container.get('userIndex') as PersonController).say() // userIndex ... john

使用Token

Service Token 可以用来标识Container中唯一的一个实例,可以安全地从Container中获取Bean

import 'reflect-metadata';
import { Container, Token } from 'typedi';export const JWT_SECRET_TOKEN = new Token<string>('MY_SECRET');Container.set(JWT_SECRET_TOKEN, 'wow-such-secure-much-encryption');/*** 这个值是类型安全的,因为Token是类型化的。*/
const JWT_SECRET = Container.get(JWT_SECRET_TOKEN);
console.log(JWT_SECRET)

可以与@Inject()搭配使用,覆盖属性或参数的推断类型

import 'reflect-metadata';
import { Container, Token, Inject, Service } from 'typedi';export const JWT_SECRET_TOKEN = new Token<string>('MY_SECRET');Container.set(JWT_SECRET_TOKEN, 'wow-such-secure-much-encryption');@Service()
class Example {@Inject(JWT_SECRET_TOKEN)myProp: string;
}const instance = Container.get(Example);
// instance.myProp属性有为Token分配的值。

同名的Token

两个具有相同名称的Token是不同的Token,一个Token实例是唯一的,类似于Symbol类型。

import 'reflect-metadata';
import { Container, Token } from 'typedi';const tokenA = new Token('TOKEN');
const tokenB = new Token('TOKEN');Container.set(tokenA, 'value-A');
Container.set(tokenB, 'value-B');const tokenValueA = Container.get(tokenA);
// tokenValueA 是 "value-A"
const tokenValueB = Container.get(tokenB);
// tokenValueB 是 "value-B"console.log(tokenValueA === tokenValueB);
// false

Token和字符串的对比

Token和字符串都可以用来标识一个Service实例,但是推荐使用Token,因为Token是类型安全的,而同一个string的名称真的就是唯一表示Service实例。

继承性

当基类和继承类都被标记为@Service()后,属性是支持继承性的。

在创建时,继承有装饰属性的类将收到这些属性上的初始化实例。

即当子类继承了父类的依赖注入的属性时,子类中的此属性也是可以直接使用的

import 'reflect-metadata';
import { Container, Token, Inject, Service } from 'typedi';@Service()
class InjectedClass {name: string = 'InjectedClass';
}@Service()
class BaseClass {name: string = 'BaseClass';@Inject()injectedClass: InjectedClass;
}@Service()
class ExtendedClass extends BaseClass {name: string = 'ExtendedClass';
}const instance = Container.get(ExtendedClass);console.log(instance.injectedClass.name);
// 输出"InjectedClass"
console.log(instance.name);
// 输出 "ExtendedClass"

参考文章

https://static.kancloud.cn/czkme/dependency-inject/2511047

源码解读

原理说明

依赖注入的核心就是容器Container
Container.set(id, value)向容器中push一个实例
Container.get(id)从容器中get一个实例

Container容器对象中的核心概念:

  • ServiceMetadata,被容器接管的每个类都叫做一个Service,ServiceMetadata就是这个类对应的信息,每个类都有一个与之对应的ServiceMetadata实例
  • metadataMap,是一个map,保存的是某一个类的配置信息
  • handlers,是一个数组,所有@Inject()的属性都是一个handler,表示待注入的属性。此容器接管的所有类中的@Inject()标注的属性都在这个数组中

按照装饰器的执行顺序,一个类中的@Inject()先执行,然后是@Service()
@Inject可以用在属性和构造参数上。

看一下ServiceMetadata的结构

export interface ServiceMetadata<Type = unknown> {// service的唯一标识,id: string | Token | Constructable | AbstractConstructor | CallableFunction; /*** 实例的作用域*  singleton 单例模式, 单例的实例会被放在default容器中*  container 从指定容器中创建实例,从此容器中也是单例的*  transient 瞬时的,每次从容器请求都会创建一个新的实例*/scope: 'singleton' | 'container' | 'transient'; // Service的类型,就是构造函数类型type: Constructable<Type> | null; // 创建此类型实例的工厂,factory: [Constructable<unknown>, string] | CallableFunction | undefined; // 此实例的工厂方法// 目标类的实例value: unknown | Symbol;// 是否允许在同一个service id下注册多个实例multiple: boolean;/*** 是否立即实例化,当为true,容器创建完成后就会实例化此类的bean;* 当为false,只有当用时才会实例化*/eager: boolean;/*** 引用此类的 metadata 的容器*/referencedBy: Map<ContainerIdentifier, ContainerInstance>;
}

@Inject的原理

首先要清楚@Inject()的用法:

  • @Inject()中可以给Service的id,用来指定注入特定的实例
  • @Inject()中的参数还可以是一个函数,用来强制修改要注入的类型

一个Handler的结构

export interface Handler<T = unknown> {// 属性所在的类(构造函数),即此属性需要注入的目标类object: Constructable<T>; // 成员名称propertyName?: string;// 成员在构造函数参数中的索引,// 若@Inject()标注的是类中实例属性,则此属性为undefinedindex?: number;// 一个方法,在@Inject()中已经实现了,从指定的容器中获取此属性的实例value: (container: ContainerInstance) => any;
}

当在一个属性上标注了@Inject()后,实际上就发生了一件事情,向容器中注入一个此属性的handler

20240117154925

@Service发生了什么

@Service()发生了两件事:

  1. 初始化此class的ServiceMetadata
  2. 向Container.metadataMap()中push这个ServiceMetadata
    看图
    20240117155904

Container.get(id)

Container.get(id)是从容器中获取实例,在这个步骤中完成了类的实例化。
这个框架的核心就是get()方法
20240117163134

总结

基于Container的依赖注入,无非就是两件事,向Container中push实例和从Container中get实例。
typedi采用了惰性加载的方式,初始只保存类的metadata(类的配置信息),
Container.get()时才会对类进行实例化,而在类实例化的过程中,如果检测都有需要注入的属性,则会继续调用Container.get()来实例化属性,经典的递归形式;后续如果如果要获取某个实例,判断已经实例化了直接返回,就不需要继续实例化了

typedi这个框架设计非常小巧强悍,代码简洁,支持自定义拓展。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/633187.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【图解数据结构】深度解析时间复杂度与空间复杂度的典型问题

&#x1f308;个人主页&#xff1a;聆风吟 &#x1f525;系列专栏&#xff1a;图解数据结构、算法模板 &#x1f516;少年有梦不应止于心动&#xff0c;更要付诸行动。 文章目录 一. ⛳️上期回顾二. ⛳️常见时间复杂度计算举例1️⃣实例一2️⃣实例二3️⃣实例三4️⃣实例四5…

FPGA引脚选择(Select IO)--认知1

主要考虑功能角度&#xff08;速度&#xff0c;电平匹配&#xff0c;内部程序编写&#xff09;去找研究芯片内部资源 1. 关键字 HP I/O Banks, High performance The HP I/O banks are deisgned to meet the performance requirements of high-speed memory and other chip-to-…

参照oracle按名称排序,用js在前端对附件封装排序方法

此前因客户需求需要附件按照名称排序 而后台无法对单个文件夹做单独处理。虽可以在每次点击之后重新调用接口&#xff0c;再组装数据&#xff0c;但效率太低&#xff0c;且无须存储&#xff0c;而存储在当前文件夹的排序方法也需要更新。索性自己写了一个通用的方法。经测试排序…

彩超框架EchoSight开发日志记录

EchoSight开发记录 蒋志强 我会不定期的更新 开发进展。最近更新进展于2024年1月15日 1.背景 由于某些不可抗逆的原因&#xff0c;离开了以前的彩超大厂&#xff0c;竞业在家&#xff0c;难得有空闲的时间。我计划利用这段时间 自己独立 从零开始 搭建一套 彩超系统的软件工…

【陈老板赠书活动 - 22期】- 人工智能(第三版)

陈老老老板&#x1f9d9;‍♂️ &#x1f46e;‍♂️本文专栏&#xff1a;赠书活动专栏&#xff08;为大家争取的福利&#xff0c;免费送书&#xff09; &#x1f934;本文简述&#xff1a;活就像海洋,只有意志坚强的人,才能到达彼岸。 &#x1f473;‍♂️上一篇文章&#xff…

浅谈CPU进入保护模式的方法

看程序要想思路不乱&#xff0c;最重要的就是要抓到程序的主线&#xff0c;不要被一些只是用来保护的代码打乱。如何抓到主线呢&#xff1f;比较法学习代码是比较有效的&#xff0c;比如对于CPU如何进入保护模式的理解。 不同的操作系统作者有自己的方法&#xff0c;代码看起来…

高级编程JavaScript中的数据类型?存储上能有什么差别?

在JavaScript中&#xff0c;我们可以分成两种类型&#xff1a; 基本类型复杂类型 两种类型的区别是&#xff1a;存储位置不同 一、基本类型 基本类型主要为以下6种&#xff1a; NumberStringBooleanUndefinednullsymbol Number 数值最常见的整数类型格式则为十进制&…

Liunx:线程控制

目录 创建线程&#xff1a;pthread_create(); 线程等待&#xff1a;pthread_join(); 线程退出&#xff1a;pthread_exit(); 线程取消&#xff1a;pthread_cancel() 说线程的时候说过&#xff0c;liunx没有选择单独定义线程的数据结构和适配算法&#xff0c;而是用轻量级进程…

【计算机网络】OSI七层模型与TCP/IP四层模型的对应与各层介绍

1 OSI七层模型与TCP/IP四层模型对应 2 OSI七层模型介绍 OSI&#xff08;Open Systems Interconnection&#xff09;模型是一个由国际标准化组织&#xff08;ISO&#xff09;定义的七层网络体系结构&#xff0c;用于描述计算机网络中的通信协议。每一层都有特定的功能&#xff…

基于arcgis js api 4.x开发点聚合效果

一、代码 <html> <head><meta charset"utf-8" /><meta name"viewport"content"initial-scale1,maximum-scale1,user-scalableno" /><title>Build a custom layer view using deck.gl | Sample | ArcGIS API fo…

启动低轨道卫星LEO通讯产业与6G 3GPP NTN标准

通讯技术10年一个大跃进&#xff0c;从1990年的2G至2000年的3G网路&#xff0c;2010年的4G到近期2020年蓬勃发展的5G&#xff0c;当通讯技术迈入融合网路&#xff0c;当前的 5G 技术不仅可提供高频宽、低延迟&#xff0c;同时可针对企业与特殊需求以 5G 专网的模式提供各式服务…

【.NET Core】 多线程之(Thread)详解

【.NET Core】 多线程之&#xff08;Thread&#xff09;详解 文章目录 【.NET Core】 多线程之&#xff08;Thread&#xff09;详解一、概述二、线程的创建和使用2.1 ThreadStart用于无返回值&#xff0c;无参数的方法2.2 ParameterizedThreadStart:用于带参数的方法 三、线程的…

使用 Python 第三方库 xlwt 写入数据到 Excel 工作表

1. 安装 xlwt 库 Python 写入数据到 Excel 工作簿中可以使用第三方库 xlwt. xlwt 拆分下来看就是 excel 和 write 的简化拼接&#xff0c;意思就是写数据到 Excel. 这个第三方库的 pip 安装命令如下所示&#xff1a; pip install xlwt -i https://mirrors.aliyun.com/pypi/si…

FairGuard游戏安全2023年度报告

导 读&#xff1a;2023年&#xff0c;游戏行业摆脱了疫情带来诸多负面影响&#xff0c;国内游戏市场收入与用户规模双双实现突破&#xff0c;迎来了历史新高点。但游戏黑灰产规模也在迅速扩大&#xff0c;不少游戏饱受其侵扰&#xff0c;游戏厂商愈发重视游戏安全问题。 为帮助…

WordPress怎么禁用文章和页面古腾堡块编辑器?如何恢复经典小工具?

现在下载WordPress最新版来搭建网站&#xff0c;默认的文章和页面编辑器&#xff0c;以及小工具都是使用古腾堡编辑器&#xff08;Gutenberg块编辑器&#xff09;。虽然有很多站长说这个编辑器很好用&#xff0c;但是仍然有很多站长用不习惯&#xff0c;觉得操作太难了&#xf…

C/C++ BM5 合并K个已排序的链表

文章目录 前言题目1 解决方案一1.1 思路阐述1.2 源码 2 解决方案二2.1 思路阐述2.2 源码 总结 前言 在接触了BM4的两个链表合并的情况&#xff0c;对于k个已排序列表&#xff0c;其实可以用合并的方法来看待问题。 这里第一种方法就是借用BM4的操作&#xff0c;只不过是多个合…

怎么处理vue项目中的错误详解

文章目录 一、错误类型二、如何处理后端接口错误代码逻辑问题全局设置错误处理生命周期钩子 三、源码分析小结参考文献 一、错误类型 任何一个框架&#xff0c;对于错误的处理都是一种必备的能力 在 Vue 中&#xff0c;则是定义了一套对应的错误处理规则给到使用者&#xff0…

【MATLAB源码-第117期】基于matlab的蜘蛛猴优化算法(SMO)机器人栅格路径规划,输出做短路径图和适应度曲线。

操作环境&#xff1a; MATLAB 2022a 1、算法描述 蜘蛛猴优化算法&#xff08;Spider Monkey Optimization, SMO&#xff09;是一种灵感来源于蜘蛛猴觅食行为的群体智能优化算法。蜘蛛猴是一种生活在南美洲热带雨林中的灵长类动物&#xff0c;它们在寻找食物时展现出的社会行…

深入探究 JavaScript 中的 String:常用方法和属性全解析(上)

&#x1f90d; 前端开发工程师&#xff08;主业&#xff09;、技术博主&#xff08;副业&#xff09;、已过CET6 &#x1f368; 阿珊和她的猫_CSDN个人主页 &#x1f560; 牛客高级专题作者、在牛客打造高质量专栏《前端面试必备》 &#x1f35a; 蓝桥云课签约作者、已在蓝桥云…

比特币狂人引爆达沃斯论坛

点击查看TechubNews原文链接&#xff1a;比特币狂人引爆达沃斯论坛 比特币狂人、自称无政府资本主义者的阿根廷总统米莱在达沃斯的最新演讲引爆社交网络大讨论。 1 月 15 日&#xff0c;第 54 届世界经济论坛在瑞士阿尔卑斯山的达沃斯开幕。来自约 60 个国家首脑和跨国公司的领…