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

news2024/12/26 11:35:07

简介

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 Person

console.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 World

const 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: A

  say(){
    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: Person

  say(){
    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()标注的是类中实例属性,则此属性为undefined
  index?: 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.coloradmin.cn/o/1395314.html

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

相关文章

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

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

Stability AI发布全新代码模型Stable Code 3B

Stable Code 3B: Coding on the Edge 要点&#xff1a; Stable Code 3B 是一个包含 30 亿个参数的大型语言模型 (LLM)&#xff0c;可实现准确且响应灵敏的代码补全&#xff0c;其水平与大 2.5 倍的 CodeLLaMA 7b 等模型相当。即使在 MacBook Air 等普通笔记本电脑上没有 GPU&…

4D毫米波雷达——RADIal数据集、格式、可视化 CVPR2022

前言 本文介绍RADIal数据集&#xff0c;来着CVPR2022的。 它是一个收集了 2 小时车辆行驶数据的数据集&#xff0c;采集场景包括&#xff1a;城市街道、高速公路和乡村道路。采集设备包括&#xff1a;摄像头、激光雷达和高清雷达等&#xff0c;并且还包括了车辆的 GPS 位置和…

【Docker】contos7安装 Nacos容器部署单个部署集群

&#x1f389;&#x1f389;欢迎来到我的CSDN主页&#xff01;&#x1f389;&#x1f389; &#x1f3c5;我是平顶山大师&#xff0c;一个在CSDN分享笔记的博主。&#x1f4da;&#x1f4da; &#x1f31f;推荐给大家我的博客专栏《Docker】contos7安装 Nacos容器部署单个&…

基于springboot+vue的社区团购系统(前后端分离)

博主主页&#xff1a;猫头鹰源码 博主简介&#xff1a;Java领域优质创作者、CSDN博客专家、公司架构师、全网粉丝5万、专注Java技术领域和毕业设计项目实战 主要内容&#xff1a;毕业设计(Javaweb项目|小程序等)、简历模板、学习资料、面试题库、技术咨询 文末联系获取 项目背景…

安卓Android studio读写EM4305卡源码

本示例使用的发卡器&#xff1a; https://item.taobao.com/item.htm?id718720660087&spma1z10.5-c.w4002-21818769070.15.57dc6f89txUhXE <?xml version"1.0" encoding"utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xml…

蓝桥杯备赛 day 2 —— 二分算法(C/C++,零基础,配图)

目录 &#x1f308;前言&#xff1a; &#x1f4c1; 二分的概念 &#x1f4c1; 整数二分 &#x1f4c1; 二分的模板 &#x1f4c1; 习题 &#x1f4c1; 总结 &#x1f308;前言&#xff1a; 这篇文章主要是准备蓝桥杯竞赛同学所写&#xff0c;为你更好准备蓝桥杯比赛涉及…

【音视频原理】图像相关概念 ② ( 帧率 | 常见帧率标准 | 码率 | 码率单位 )

文章目录 一、帧率1、帧率简介2、常见帧率标准3、帧率 刷新率 二、码率1、码率简介2、码率单位 一、帧率 1、帧率简介 帧率 Frame Rate , 帧 指的是 是 画面帧 , 帧率 是 画面帧 的 速率 ; 帧率 的 单位是 FPS , Frames Per Second , 是 每秒钟 的 画面帧 个数 ; 帧率 是 动画…

弗洛伊德循环查找算法-原理

本文灵感来自哔哩哔哩视频 视频链接: 弗洛伊德循环查找算法 算法代码(java) package rain;class ListNode {int value;ListNode next;public ListNode(int value) {this.value value;this.next null;}Overridepublic String toString() {return "ListNode{" &q…

Kotlin 移动端多平台

支持多平台编程是 Kotlin 的主要优势之一。它减少了为不同平台编写和维护相同代码所花费的时间&#xff0c;同时保留了本机编程的灵活性和优势。 1. 基本概念 KMM&#xff1a;Kotlin Multiplatform for mobile&#xff08;移动设备的 Kotlin 多平台&#xff09; KMM 多平台的主…

使用的uview 微信高版本 头像昵称填写能力

<template><view><button class"cu-btn block bg-blue margin-tb-sm lg" tap"wxGetUserInfo">一键登录</button><view><!-- 提示窗示例 --><u-popup :show"show" background-color"#fff">&…

pygame里实现导弹追踪效果,同时对python的指针机制有一点点思考

最近,儿子一直缠着让我把之前给他编写的游戏重做一下,要加一些功能.但是因为之前写代码的时候刚学会python,当时的想法就是能跑就行,现在回头看来,代码的可维护性几乎为零.所以没办法只能冲头再来,重构了几乎所有代码.在编写的时候遇到了一个有意思的问题,儿子让我给游戏添加一…

Pyside6入门教学——编写一个UI界面并显示

1、安装Pyside6 输入下列命令安装Pyside6。 pip install Pyside6 2、设计UI 打开Qt设计工具&#xff08;在安装Pyside6包的目录下&#xff09;。 【注】我这用的是anaconda虚拟环境&#xff0c;所以我的路径是D:\App\Anaconda3\envs\snake\Lib\site-packages\PySide6。设计…

【Python学习】Python学习19- 异常处理

目录 【Python学习】Python学习19- 异常处理 前言python标准异常异常处理带异常类型语法不带异常类型语法使用except而带多种异常类型try-finally 语句触发异常 参考 文章所属专区 Python学习 前言 本章节主要说明Python的异常处理。 python标准异常 BaseException 所有异常…

GNU Radio简介及流程图搭建

文章目录 前言一、GNU Radio 是什么&#xff1f;二、GNU Radio 安装三、搭建第一个流程图1、创建 GRC 文件2、添加块3、运行流程图 前言 欢迎来到无线通信的世界&#xff0c;初步接触 GNU Radio&#xff0c;对其学习进行一个记录。 一、GNU Radio 是什么&#xff1f; GNU Rad…

【C语言编程之旅 4】刷题篇-关键字

第一题 解析 C语言关键字&#xff1a;C语言定义的&#xff0c;具有特定含义、专门用于特殊用途的C语言标识符&#xff0c;也称为保留字 A&#xff1a;错误&#xff0c;关键字是语言自身定义的 B&#xff1a;正确 C&#xff1a;错误&#xff0c;关键字具有特殊含义&#xff…

docker部署项目,/var/lib/docker/overlay2目录满了如何清理?

docker部署项目&#xff0c;/var/lib/docker/overlay2目录满了如何清理&#xff1f; 一、问题二、解决1、查看 /var/lib/docker 目录&#xff08;1&#xff09;、containers 目录&#xff08;2&#xff09;、volumes 目录&#xff08;3&#xff09;、overlay2 目录 2、清理&…

Java开发分析 JProfiler 14 中文

JProfiler 14是一款强大的Java分析工具&#xff0c;专为帮助Java开发者优化应用性能而设计。它提供了实时监控、内存分析、线程分析、CPU分析等多种功能&#xff0c;帮助开发者快速定位和解决性能问题。JProfiler 14具有直观的用户界面&#xff0c;使用户能够轻松上手。此外&am…

苹果Find My可查找添加32件物品,伦茨科技ST17H6x芯片加速产品赋能

苹果最近更新的支持文档证实&#xff0c;从 iOS 16 开始&#xff0c;"Find My"可查找添加物品从16件增加到32件&#xff0c;AirTag 和“查找”网络中的物品利用“查找”网络的强大功能来发挥作用&#xff0c;这个网络由数亿台加密的匿名 Apple 设备构成。“查找”网络…

Kafka-多线程消费及分区设置

目录 一、Kafka是什么&#xff1f;消息系统&#xff1a;Publish/subscribe&#xff08;发布/订阅者&#xff09;模式相关术语 二、初步使用1.yml文件配置2.生产者类3.消费者类4.发送消息 三、减少分区数量1.停止业务服务进程2.停止kafka服务进程3.重新启动kafka服务4.重新启动业…