IoC控制反转和DI依赖注入

MVC

MVC(Model-View-Controller)是一种常用的软件架构模式,用于设计和组织应用程序的代码。它将应用程序分为三个主要组件:模型(Model)、视图(View)和控制器(Controller),各自负责不同的职责。

  1. 模型(Model):模型表示应用程序的数据和业务逻辑。它负责处理数据,包含数据的增删改查等操作。模型通常包含与数据库、文件系统或外部服务进行交互的代码。
  2. 视图(View):视图负责将模型的数据以可视化的形式呈现给用户。它负责用户界面的展示,包括各种图形元素、页面布局和用户交互组件等。视图通常是根据模型的状态来动态生成和更新的。(如果是前后端分离就没有视图层)
  3. 控制器(Controller):控制器充当模型和视图之间的中间人,负责协调两者之间的交互。它接收用户输入(例如按钮点击、表单提交等),并根据用户的输入更新模型的状态或调用相应的模型方法。控制器还可以根据模型的变化来更新视图的显示。(路由)

前端路由:路由的映射函数通常是加载不同的页面,在页面中对网页中的元素进行一些DOM操作。后端路由:服务端接收到客户端发来的HTTP请求会解析请求的URL,将URL中的pathname解析出后根据pathname来找到相应的映射函数,然后执行该函数,该函数会调用模型层处理数据的函数,最后根据需求返回处理的数据给前端)

MVC 的主要目标是将应用程序的逻辑、数据和界面分离,以提高代码的可维护性、可扩展性和可重用性。通过将不同的职责分配给不同的组件,MVC 提供了一种清晰的结构,使开发人员能够更好地管理和修改应用程序的各个部分。

IoC和DI

控制反转(Inversion of Control,IoC)和依赖注入(Dependency Injection,DI)是软件开发中常用的设计模式和技术,用于解耦和管理组件之间的依赖关系。虽然它们经常一起使用,但它们是不同的概念。

IoC

控制反转(IoC)是一种设计思想而不是技术。设计思想是指将设计好的对象交给容器控制,而不是传统的在对象内部直接控制。常规情况下,对象都是开发者手动创建的,而 IoC 则使用框架来自动创建项目所需的对象。

控制反转核心思想就是实现对象与对象之间的解耦,更好的进行模块化开发,有利于维护项目和快速的开发项目。

传统创建依赖对象的方法:

js 复制代码
// A类中使用了B类,A类依赖于B类
class A {
    constructor(){
        this.b = new B()
    }
}

class B {
    constructor(){}
}

使用容器管理对象:

js 复制代码
// 在容器Container类中将A类和B类进行解耦
class A {
    constructor(b){
        this.b = b
    }
}

class B {
    constructor(){}
}

class Container {
    constructor(){
        let b = new B()
        new A(b)
    }
}

当实例化A类时,会运行A类的constructor函数,就会实例化B。如此A类和B类就造成了耦合的关系。在容器Container类中进行实例化,就将A类和B类之间进行了解耦。

DI

依赖注入(DI)是实现控制反转的一种具体技术。在容器运行期间,由容器动态地将某个依赖对象注入到当前对象的技术,而不是对象本身来完成依赖对象的注入。组件不再负责创建或管理它所依赖的其他组件,而是通过构造函数、属性或方法参数等方式将依赖关系注入到组件中。

依赖注入可以通过构造函数注入(Constructor Injection)、属性注入(Property Injection)或方法注入(Method Injection)等方式实现。

装饰器

装饰器是在不改变原对象的基础上,为对象动态的添加和设置各种功能。装饰器就是函数的语法糖。

装饰器分为类装饰器,成员装饰器,参数装饰器。

  • 类装饰器:放在一个类的前面
  • 成员装饰器:放在类的某个属性前面或者某个方法的前面
  • 参数装饰器:放在方法的某个参数的前面

下列代码是nest.js的一个控制器(nest是一个以express为基础的typescript服务器框架):

js 复制代码
//类装饰器
@Controller('user') 
export class UserController {
    // 成员装饰器
    @Get('/userInfo')
    @HttpCode(200)
    // 参数装饰器
    recall(@Req()request: Request): string {
        console.log(request) // 将会输出请求的内容
        return 'hello world'
    }
}

@Controller('user'),它是一个类装饰器,要放在class的前面,作用是定义一个基本的路由控制器,即添加了一个路由。如此,这个类成为了一个控制器,具有了一个/user的路由

@Get('/userInfo')@HttpCode(200)都是成员装饰器,它们放在类的成员前面,作用是让这个类具有了更多的功能,@Get('/userInfo')是给/user路由增加了一个/userInfo的子路由,@HttpCode(200)是返回200的状态码。

@Req()是一个参数装饰器,放在类中函数的参数前面,作用是让这个参数具有了请求的参数的意义。

js 复制代码
class UserController {
    recall(request: Request):string {
        console.log(request)
        return 'hello nestjs'
    }
}

取掉所有的装饰器,其实这个类只有一个方法。装饰器则给这个类增加了很多功能,从而达到了一个路由控制器的作用,可以处理传入的请求参数和向客户端返回响应。这就是装饰器在实际开发中的用法,通过高度的封装,更加快速的实现功能,同时也具备了面向对象开发的特点。

inversify和Reflect Metadata

InversifyJs是基于TS利用依赖注入实现IOC的轻量工具库。通过装饰器+反射实现元数据的注入和读取。

在实现依赖注入时,为class添加一些元数据metadata,并可以方便的读取这些数据,而在这个过程中不会对class的结构产生任何影响。就需要用到装饰器+反射来注入元数据metadata,然后通过反射来获取元数据metadataReflect Metadata是利用了WeakMap来实现的元数据存储,把对象作为key,元数据集合作为value。

InversifyJs是基于Reflect Metadata的

InversifyJs使用

Scope

在 inversify.js 中,或者说是在 IoC 的概念中存在一个叫做 scope 的单词,它是和 class 的注入关联在一起的。一个类的注入 scope可以支持以下三种模式:

Transient:每次从容器中获取的时候(也就是每次请求)都是一个新的实例
Singleton:每次从容器中获取的时候(也就是每次请求)都是同一个实例
Request:也称为Scoped模式,每次请求的时候都会获取新的实例,如果在这次请求中该类被require多次,那么依然还是用同一个实例返回。

Scope可以全局配置,通过defaultScope参数传参进去,也可以针对每个类进行区别配置,使用方法是:

js 复制代码
container.bind<Shuriken>("Shuriken").to(Shuriken).inTransientScope(); // Default
container.bind<Shuriken>("Shuriken").to(Shuriken).inSingletonScope();
container.bind<Shuriken>("Shuriken").to(Shuriken).inRequestScope();

容器对象和inversify的绑定过程

容器本身就是一个类实例,inversify 做的就是利用这么一个类实例来管理诸多别的类实例。在InversifyJs中,Container类可以看作是整个依赖注入链路的入口,它可以创建容器对象。

创建的容器对象通过bind方法,会根据传入的标志符号或者类生成一个新的binding对象,用于指定class也就是类绑定到容器内的类型。在这个时候还没有真正绑定内容,bind方法会返回一个链式调用对象用于处理绑定相关操作,通过链式调用to方法去绑定一个类,并将类添加到容器中。除了to语法,其余的语法其实都是在往Binding这个类实例的属性赋值。

Binding这个类实例属性如下:

js 复制代码
interface Binding<T> extends Clonable<Binding<T>> {
        id: number;
        moduleId: string;
        activated: boolean;
        serviceIdentifier: ServiceIdentifier<T>;
        constraint: ConstraintFunction;
        dynamicValue: ((context: interfaces.Context) => T) | null;
        scope: BindingScope;
        type: BindingType;
        implementationType: Newable<T> | null;
        factory: FactoryCreator<any> | null;
        provider: ProviderCreator<any> | null;
        onActivation: ((context: interfaces.Context, injectable: T) => T) | null;
        cache: T | null;
    }

container.bind(A).toXXX(),可以理解成绑定A类为XXX,这里的XXX可以有以下几种:

  • to():必须传入一个构造器,定义的类型是BindingTypeEnum.Instance,后续在使用的时候会new这个构造器来创建实例对象
  • toSelf():to写法的一种简写方式,内部最后还是调用to
  • toConstantValue():绑定为常量,传入的是一个初始化后的实例
  • toDynamicValue():绑定为动态值,在获取的时候会去执行对应的函数
  • toConstructor():绑定为构造函数,在获取之后需要自己实例化
  • toFactory():绑定为工厂函数,与刚才的动态值不一样,动态值会执行完动态函数返回值,而工厂函数则会返回一个高阶函数,允许进一步定制值
  • toFunction():绑定为函数,其实就是toConstantValue的别名
  • toAutoFactory():绑定为自动工厂函数,此时的工厂函数不用开发者提供,内部自己实现
  • toProvider():绑定为一个异步的工厂函数,称之为Provider,对于需要一些异步操作的时候这种方式非常有用
  • toService():绑定为一个服务,让其解析为以前声明过的别的类型绑定,这个绑定很特殊,没有别的任何后续操作,因为它没有返回值。

容器模块化

也可以通过ContainerModule将容器模块化有助于管理众多的绑定。ContainerModule的构造函数只有一个回调函数作为参数,回调函数的参数如下:bind(绑定)、unbind(解除绑定)、isbound(判断是否绑定)、rebind(重新绑定)

同步容器模块
js 复制代码
let warriors = new ContainerModule((bind, unbind) => {
    bind("Ninja").to(Ninja);
});

let weapons = new ContainerModule((bind,unbind,isBound,rebind) => {
        bind("Katana").to(Katana);
        bind("Shuriken").to(Shuriken);
    }
);

let container = new Container();
container.load(warriors, weapons);
container.unload(warriors);
异步容器模块
js 复制代码
let weapons = new AsyncContainerModule((bind,unbind,isBound,rebind) => {
        bind("Katana").to(Katana);
        bind("Shuriken").to(Shuriken);
    }
);

let container = new Container();
await container.loadAsync(warriors, weapons);
container.unload(warriors);

类对类的依赖

在InversifyJs中,使用@injectable装饰器来标志一个类是否是可依赖注入的类,使用@inject装饰器来标志要注入依赖的类,在解析阶段的时候会根据@inject装饰器的参数在容器中查找依赖并注入。即@injectable装饰器修饰被依赖的类,@inject装饰器修饰依赖类,其装饰器参数是被依赖的类。

InversifyJS 允许类对其他类的直接依赖。这样做时,需要使用装饰器 @injectable,但不需要使用装饰器 @inject。使用类作为服务标识时不需要 @inject 装饰器,因为编译器能生成元数据。但是需要进行如下配置:

  • 导入 reflect-metadata
  • tsconfig.json 文件中设置 emitDecoratorMetadata 为 true
js 复制代码
import { Container, injectable, inject } from "inversify";

@injectable()
class Katana {
    public hit() {
        return "cut!";
    }
}

@injectable()
class Shuriken {
    public throw() {
        return "hit!";
    }
}

@injectable()
class Ninja implements Ninja {

    private _katana: Katana;
    private _shuriken: Shuriken;

    public constructor(katana: Katana, shuriken: Shuriken) {
        this._katana = katana;
        this._shuriken = shuriken;
    }

    public fight() { return this._katana.hit(); };
    public sneak() { return this._shuriken.throw(); };

}

var container = new Container();
container.bind<Ninja>(Ninja).to(Ninja);
container.bind<Katana>(Katana).to(Katana);
container.bind<Shuriken>(Shuriken).to(Shuriken);

但是如果在循环依赖中使用类作为标识符会被抛出异常:

js 复制代码
import "reflect-metadata";
import { Container, injectable } from "inversify";
import getDecorators from "inversify-inject-decorators";

let container = new Container();
let { lazyInject } = getDecorators(container);

@injectable()
class Dom {
    public domUi: DomUi;
    constructor (domUi: DomUi) {
        this.domUi = domUi;
    }
}

@injectable()
class DomUi {
    @lazyInject(Dom) public dom: Dom;
}

@injectable()
class Test {
    constructor(dom: Dom) {
        console.log(dom);
    }
}

container.bind<Dom>(Dom).toSelf().inSingletonScope();
container.bind<DomUi>(DomUi).toSelf().inSingletonScope();
const dom = container.resolve(Test); // Error!

Error: Missing required @Inject or @multiinject annotation in: argument 0 in class Dom.

当使用类作为服务标识,@inject 等注解可以不需要使用。如果我们添加 @inject(Dom)@inject(DomUi) 的注解,依然会抛出相同的异常。因为装饰器被调用的时候,类还没有被声明,所以装饰器被调用为 @inject(undefined),导致 InversifyJS 认为对应的注解没有被添加。

解决办法是使用 Symbol,比如 Symbol.for("Dom") 作为服务标识而不是 Dom 这样的类。

js 复制代码
import "reflect-metadata";
import { Container, injectable, injectable } from "inversify";
import getDecorators from "inversify-inject-decorators";

const container = new Container();
const { lazyInject } = getDecorators(container);

const TYPE = {
    Dom: Symbol.for("Dom"),
    DomUi: Symbol.for("DomUi")
};

@injectable()
class DomUi {
    public dom: Dom;
    public name: string;
    constructor (
        @inject(TYPE.Dom) dom: Dom
    ) {
        this.dom = dom;
        this.name = "DomUi";
    }
}

@injectable()
class Dom {
    public name: string;
    @lazyInject(TYPE.DomUi) public domUi: DomUi;
    public constructor() {
        this.name = "Dom";
    }
}

@injectable()
class Test {
    public dom: Dom;
    constructor(
        @inject(TYPE.Dom) dom: Dom
    ) {
        this.dom = dom;
    }
}

container.bind<Dom>(TYPE.Dom).to(Dom).inSingletonScope();
container.bind<DomUi>(TYPE.DomUi).to(DomUi).inSingletonScope();

const test = container.resolve(Test); // Works!

具体类型绑定自身

如果要解析的类型是具体类型,绑定注册会很重复和冗长:container.bind<Samurai>(Samurai).to(Samurai)

可以采用toSelf方法:container.bind<Samurai>(Samurai).toSelf()

标志符号

字符串作为类型标识被 InversifyJS 注入,会导致命名冲突。 InversifyJS 支持和推荐使用 Symbol 而不是字符串字面量。

Symbol 是一种特殊的、不可变的数据类型,可以作为对象属性的标识符使用。Symbol 对象是一个 symbol 基本数据类型的隐式对象包装器。

js 复制代码
import { Container, injectable, inject } from "inversify";

let Symbols = {
    Ninja : Symbol.for("Ninja"),
    Katana : Symbol.for("Katana"),
    Shuriken : Symbol.for("Shuriken")
};
// TS中,implements表示实现,是一个新的类,从父类或者接口实现所有的属性和方法,同时可以重写属性和方法,包含一些新的功能
// extends表示继承,是一个新的接口或者类,从父类或者接口继承所有的属性和方法,不可以重写属性,但可以重写方法
@injectable()
class Katana implements Katana {
    public hit() {
        return "cut!";
    }
}

@injectable()
class Shuriken implements Shuriken {
    public throw() {
        return "hit!";
    }
}

@injectable()
class Ninja implements Ninja {

    private _katana: Katana;
    private _shuriken: Shuriken;

    public constructor(
        @inject(Symbols.Katana) katana: Katana,
        @inject(Symbols.Shuriken) shuriken: Shuriken
    ) {
        this._katana = katana;
        this._shuriken = shuriken;
    }

    public fight() { return this._katana.hit(); };
    public sneak() { return this._shuriken.throw(); };

}

var container = new Container();
container.bind<Ninja>(Symbols.Ninja).to(Ninja);
container.bind<Katana>(Symbols.Katana).to(Katana);
container.bind<Shuriken>(Symbols.Shuriken).to(Shuriken);

构造器注入

将类的构造函数绑定到容器中,需要的时候从容器中获取类的构造器,再进行实例化。这样我们就可以在容器中统一管理这些注入到容器对象的类的实例对象了。

js 复制代码
@injectable()
class Ninja implements Ninja {

    private _katana: Katana;
    private _shuriken: Shuriken;

    public constructor(
        @inject("Newable<Katana>") Katana: Newable<Katana>, 
        @inject("Shuriken") shuriken: Shuriken
    ) {
        this._katana = new Katana();
        this._shuriken = shuriken;
    }

    public fight() { return this._katana.hit(); };
    public sneak() { return this._shuriken.throw(); };

}
container.bind<interfaces.Newable<Katana>>("Newable<Katana>").toConstructor<Katana>(Katana);

实例会跟随类的生命周期而存在,且该类能纳入容器中进行管理。但是这样做,实际上仍然无法在容器中统一管理这些实例的生命周期。如果需要在 dispose 时销毁这些实例,那么需要在类中手动实现 dispose 方法,并在 dispose 时手动销毁这些实例。

这样改造的好处是简单,但是很多时候并不是一个最优解,因为希望该实例本身能在注入框架的管理下,避免去手动的控制与销毁。

注入工厂

绑定一个类到用户自定义的工厂,将这些类的实例化过程封装到工厂中。而这样的工厂类一定是单例的,那么就可以通过管理工厂类来管理这些实例化对象的生命周期了。

在很多的地方都需要类的实例化创建对象时,就不用直接引入类每次都进行实例化,而是通过引入工厂类来获取在工厂类中创建的实例化对象,这样就可以在工厂中控制多个实例化对象的生命周期了。通过工厂注入的方式,可以将对其他类的依赖项明确地声明在构造函数中,从而使得类的职责更加清晰,保持了类的纯粹性。

在InversifyJs中,通过container.bind().Factory().toFactory()的链式调用将工厂类绑定在容器对象中。.toFactory()方法需要接受一个工厂创建器也就是一个函数,该函数接受一个参数并且会返回一个函数。参数context是一个上下文对象,它会传递给工厂函数;返回的函数就是工厂函数,工厂函数用于创建工厂类的实例化对象。

js 复制代码
// 设置工厂函数,可以根据不同逻辑实现不同的对象实例化
const instanceFactory = (name: string)  => {
// 返回不同的实例化对象
  if (name === "Instance") {
    return context.container.get<InstanceClass>("Instance");
  }
  return context.container.get<DefaultClass>("Default");
};

 // 工厂创建器,将 context 传递给工厂函数,方便获取容器
const instanceFactoryCreator = (context: interfaces.Context) => {
  return instanceFactory;
};

// 在容器中注入工厂创建器函数,容器会执行工厂创建器函数,然后在容器中就绑定了工厂函数
container
  .bind<interfaces.Factory<InstanceClass>>("Factory<InstanceClass>")
  .toFactory<InstanceClass>(instanceFactoryCreator);
js 复制代码
@injectable()
class Ninja implements Ninja {
  private _katana: Katana;
  private _shuriken: Shuriken;

  public constructor(
    @inject("Factory<Katana>") katanaFactory: () => Katana,
    @inject("Shuriken") shuriken: Shuriken
  ) {
    this._katana = katanaFactory(); // 在容器对象中存储的是工厂函数,运行绑定在容器中的工厂函数,运行工厂函数后就会返回实例化对象
    this._shuriken = shuriken;
  }

  public fight() {
    return this._katana.hit();
  }
  public sneak() {
    return this._shuriken.throw();
  }
}
// 注入的是工厂创建器函数
container
  .bind<interfaces.Factory<Katana>>("Factory<Katana>")
  .toFactory<Katana>((context: interfaces.Context) => {
  // 工厂创建器,将 context 传递给工厂函数,方便获取容器
    return () => {
    // 返回实例化对象
      return context.container.get<Katana>("Katana");
    };
  });

(context: interfaces.Context) => {return () => {return context.container.get<Katana>("Katana");}这个是工厂创建器函数,该函数注入到了容器对象中。() => {return context.container.get<Katana>("Katana");是工厂函数,容器对象会运行工厂创建器函数并将该函数返回的工厂函数存储在对象中。这样当@inject使用依赖时就会得到容器中存储的工厂函数,运行工厂函数就会得到工厂函数返回的依赖类的实例化对象。

带参数的工厂

依赖注入框架完美解决了在使用依赖类时需要传入依赖类并且创建依赖实例。但对于在执行过程中依赖于外部变量的类,在InversifyJS 的工厂注入中,是在获取实例后为实例进行属性注入。例如:

js 复制代码
// 设置工厂函数
const instanceFactory = (payload: Record<string, any>) => {
// 从容器对象中获取实例对象
  const instance = context.container.get<InstanceClass>("Instance");
  instance.payload = payload;
  return instance;
};

// 工厂创建器,将 context 传递给工厂函数,方便获取容器
const instanceFactoryCreator = (context: interfaces.Context) => {
  return instanceFactory;
};

// 绑定工厂
container
  .bind<interfaces.Factory<InstanceClass>>("Factory<InstanceClass>")
  .toFactory<InstanceClass>(instanceFactoryCreator);

在实例化后改变实例的属性,从而使实例中对属性的依赖得以满足。但这样的实现方式,会使得原有类发生改变,也会改变类中属性的访问方式,例如类的属性是readonly 或是 private ,在实例化对象时是无法对其进行赋值。

当这个类继承于外部需要传入参数的类,或者是需要在首次实例化时根据传入的变量依赖执行部分操作时,这种实例化的方式是行不通的。可以使用带参数实例化的工厂注入。

js 复制代码
// 设置工厂函数
const instanceFactory = (payload: Record<string, any>) => {
  const InstanceClass = context.container.get<Newable<InstanceClass>>(
    "Newable<InstanceClass>"
  );

  const instance = new InstanceClass(payload);

  return instance;
};
// 工厂创建器,将 context 传递给工厂函数,方便获取容器
const instanceFactoryCreator = (context: interfaces.Context) => {
  return instanceFactory;
};

// 绑定工厂,注入到容器中的是工厂创建器这个函数
container
  .bind<interfaces.Factory<InstanceClass>>("Factory<InstanceClass>")
  .toFactory<InstanceClass>(instanceFactoryCreator);

使用步骤

  1. 声明接口和类型
  2. @Injectable@Inject装饰器声明依赖
  3. 创建并配置一个 Container类的实例化的容器对象
  4. 获取并使用
js 复制代码
// 声明依赖
@injectable()
class Ninja implements Warrior {

    private _katana: Weapon;
    private _shuriken: ThrowableWeapon;

    public constructor(
	    @inject(TYPES.Weapon) katana: Weapon,
	    @inject(TYPES.ThrowableWeapon) shuriken: ThrowableWeapon
    ) {
        this._katana = katana;
        this._shuriken = shuriken;
    }

    public fight() { return this._katana.hit(); }
    public sneak() { return this._shuriken.throw(); }

}
// 创建和配置容器
const myContainer = new Container();
myContainer.bind<Warrior>(TYPES.Warrior).to(Ninja);
myContainer.bind<Weapon>(TYPES.Weapon).to(Katana);
myContainer.bind<ThrowableWeapon>(TYPES.ThrowableWeapon).to(Shuriken);
// 使用
const ninja = myContainer.get<Warrior>(TYPES.Warrior);
expect(ninja.fight()).eql("cut!"); // true

inversify-express-utils使用

inversify-express-utils是一个基于 InversifyJS 实现的专门为 Express 框架提供依赖注入的工具库。

配置容器和服务器

InversifyExpressServer可以快速的创建express应用程序,将创建的inversify容器对象传递给InversifyExpressServer构造函数。这将允许InversifyExpressServer构造函数创建的对象从容器对象中注册所有路由控制器及其依赖项,并将它们添加加到express应用程序。

  • .setConfig(configFn):可以给express应用程序注册项目所需的中间件
  • .setErrorConfig(errorConfigFn):类似于.setConfig(),但此函数是在注册所有应用中间件和控制器路由之后应用的
  • .build():将所有注册的控制器和中间件添加加到express应用程序并返回应用程序实例,之后可以启动服务器
js 复制代码
let server = new InversifyExpressServer(container);
server.setConfig((app) => { 
    app.use(express.json()) // 用于解析前端请求所接受的json数据
});
server.setErrorConfig((app) => { 
    app.use((err, req, res, next) => { 
        console.error(err.stack); 
        res.status(500).send('Something broke!'); 
        }); 
});

装饰器

  • @controller(path,[middleware,...]):作用是将类注册为具有根路径的路由控制器,并且可以选择为此控制器注册任何全局中间件。
  • @httpMethod(method, path, [middleware, ...]):作用是将修饰的控制器的方法注册为特定路径的映射函数,其中method是请求的方法,path是前端请求URL的pathname。包括@httpGet@httpPost@httpPut@httpPatch@httpHead@httpDelete,和@All@httpGet就是Next.js的@Get、spring boot的@GetMapping,其他同理。

class-validator和class-transformer

class-validator是一个简化验证的依赖库,class-transformer是一个将对象进行序列化和反序列化的依赖库。

class-validator官方提供的方式还不能直接对一个请求的json值进行校验,它要求必须要是类的一个对象,所以需要class-transformer依赖库做一些处理,将一个json转成指定的类的对象。

class-validator使用方法

类型校验

装饰器 含义
@IsBoolean() 检查值是布尔型
@IsDate() 检查值是日期
@IsString() 检查值是字符串
@IsNumber(options: IsNumberOptions) 检查值是数字
@IsInt() 检查值是整数
@IsArray() 检查值是数组
@IsEnum(entity: object) 检查值是有效枚举
js 复制代码
// codes是数组类型每一项是字符串,且数组长度最小为1
  @ArrayMinSize(1)
  @IsString({each:true}) //每项验证,验证数组的每一项是否是字符串
  codes: string[];

  // 是对象类型,尽量不是用 any 而是具体到各种 类型
  // 对于键值对的类型直接使用
  @IsObject()
  value : KeyValuePair
  
  @IsOptional()   // 选填参数,可以加上 IsOptional 方法
  @IsString()
  @MinLength(1).  // 最小长度
  name: string;

  @IsNotEmpty()
  userId: string;

  @IsNotEmpty({ message: 'appId 不能为空' }) // 支持自定义错误信息,目前不支持显示给前端
  appId: string;
  
  @IsNotEmpty()
  @IsArry()     
  codes: string[];
  
  @IsEnum(targetTtype) // 支持枚举
  type: targetTtype;

公共校验

装饰器 含义
@IsOptional() 选填字段校验如,需要再加上另外校验 如果参数值是空的 (null 或 undefined),将忽略该字段的其他校验方法
@Equals(comparison: any) 检查值是等于对比值
@NotEquals(comparison: any) 检查值是不等于对比值
@IsEmpty() 检查值是空的 ( '', null , undefined).
@IsNotEmpty() 检查值不能为空 (!== '', !== null, !== undefined).
@IsIn(values: any[]) 检查值是在允许的值的数组内
@IsNotIn(values: any[]) 检查值是不在数组内
js 复制代码
  // namespace 是选填字段namespace的类型是字符串,如果namespace有字段,值至少长度为1 ,
  // MinLength(1) 用于非空传递,也可以使用 IsNotEmpty()
  @IsOptional()
  @MinLength(1)
  namespace: string;

数组校验

装饰器 含义
@ArrayContains(values: any[]) 检查数组中包含了所有的值从给定的值的数组
@ArrayNotContains(values: any[]) 检查数组中不包含任何给定的值。
@ArrayNotEmpty() 检查数据中的元素不为空
@ArrayMinSize(min: number) 检查数组的最小长度为指定的数字
@ArrayMaxSize(max: number) 检查数组的最大长度为指定的数字
@ArrayUnique(identifier?: (o) => any) 检查所有数组的值是否唯一,对象的比较是基于引用的,可选函数可以指定用于比较的返回值

nest class-validator验证修饰器中文文档 - 密蒙 - 博客园 (cnblogs.com)

class-validator基本使用

首先需要创建一个类,并在类的每个属性上添加不同的验证装饰器,这里用PostDto作为演示。

js 复制代码
import { Contains, IsInt, Length, IsEmail, IsFQDN, IsDate, Min, Max} from "class-validator";
// 导出PostDto类
export class PostDto {  
@Length(10, 20)  
title: string;  
  
@Contains("hello")  
text: string;  
  
@IsInt()  
@Min(0)  
@Max(10)  
rating: number;  
  
@IsEmail()  
email: string;  
  
@IsFQDN()  
site: string;  
  
@IsDate()  
createDate: Date;  
  
}

然后使用validate方法验证值,validate方法是要验证类的实例对象而不是一个js对象。

js 复制代码
import {validate, validateOrReject} from "class-validator";
import PostDto from './PostDto.dto.ts
// 实例化PostDto类的实例对象
let post = new PostDto();
// 给示例对象添加属性
post.title = "Hello"; // 不通过检验
post.text = "this is a great post about hell world"; // 通过检验
post.rating = 11; // 不通过检验
post.email = "google.com"; // 通过检验
post.site = "googlecom"; // 不通过检验

// 如果验证失败不会停止运行程序
validate(post).then(errors => { 
    if (errors.length > 0) {
    // 返回的错误信息是数组,数组中有元素则表示验证没有通过
        console.log("validation failed. errors: ", errors);
    } else {
        console.log("validation succeed");
    }
});

// 验证失败就停止运行程序
validateOrReject(post).catch(errors => {
    console.log("Promise rejected (validation failed). Errors: ", errors);
});
// 或者
async function validateOrRejectExample(input) {
    try {
        await validateOrReject(input);
    } catch (errors) {
        console.log("Caught promise rejection (validation failed). Errors: ", errors)
    }
}

返回的错误信息数组:

js 复制代码
[{
    target: /* 实例化的post对象 */,
    property: "title",
    value: "Hello",
    constraints: {
        length: "property must be longer than or equal to 10 characters"
    }
}, {
    target: /* 实例化的post对象 */,
    property: "text",
    value: "this is a great post about hell world",
    constraints: {
        contains: "text must contain a hello string"
    }
},
// and other errors
]

validate方法的第二个参数是一个选项对象,尽量设置forbidNonWhitelistedtrue以避免unkown对象的输入验证

js 复制代码
export interface ValidatorOptions {

    skipMissingProperties?: boolean;
    whitelist?: boolean;
    forbidNonWhitelisted?: boolean;
    groups?: string[];
    dismissDefaultMessages?: boolean;
    validationError?: {
        target?: boolean;
        value?: boolean;
    };

    forbidUnknownValues?: boolean;
}

验证失败返回的错误数组是ValidationError类的对象的数组,格式如下

js 复制代码
{
    target: Object; // 被验证的对象
    property: string; // 未通过验证的对象的属性
    value: any; // 未通过验证的值
    constraints?: { // 错误消息验证失败的约束以及约束提示
        [type: string]: string;
    };
    children?: ValidationError[]; // 包含属性的所有嵌套验证错误
}

在http响应中我们一般不想在错误中暴露target,那么就可以如下方式禁用它

js 复制代码
validator.validate(post, { validationError: { target: false } });

class-transformer方法

类转换器的作用是将普通的javascript对象转换成类的实例对象。当通过api或者json文件访问所得的是普通的json文本,而通过JSON.parse把其转换成普通的javascript对象,但是有时候需要让它变成一个类的实例对象而不是普通的javascript对象。比如用class-validatorvalidate方法来验证从后端api获取的json字符串或者前端传入的json字符串时,就需要自动把json转为待验证的类的实例对象而不是一个js对象。

plainToClass

plainToClass这个方法会把一个普通的js对象转换成指定类的实例对象。

js 复制代码
import {plainToClass} from "class-transformer";
// 把 user这个普通对象 转换成 User类的实例对象,数组也是支持的 
let users = plainToClass(User, user); 

plainToClassFromExist

plainToClassFromExist方法将普通对象合并已经创建的类实例

js 复制代码
const defaultUser = new User();
defaultUser.role = 'user';
// 如果user对象没有设置role的值,则混合之后,对象中role 值为 user
let mixedUser = plainToClassFromExist(defaultUser, user); 

classToPlain

classToPlain方法将类的实例对象转换为普通对象

js 复制代码
import {classToPlain} from "class-transformer";
let photo = classToPlain(photo);

转换后可以使用JSON.stringify再转成普通的json文本

classToClass

classToClass方法会克隆类的实例对象

js 复制代码
import {classToClass} from "class-transformer";
let photo = classToClass(photo);

可以使用ignoreDecorators选项去除所有原实例中的装饰器

serialize

serialize方法直接把类的实例对象转换为json文本,是不是数组都可以转换

js 复制代码
import {serialize} from "class-transformer";
let photo = serialize(photo);

deserialize 和 deserializeArray

js 复制代码
import {deserialize,deserializeArray} from "class-transformer";
// 直接把json文本转换为类的实例对象
let photo = deserialize(Photo, photo);
// 如果json文本是个对象数组使用deserializeArray方法
let photos = deserializeArray(Photo, photos);

强制类型安全

plainToClass会把被转换对象的所有属性全部转换为类的实例对象的属性,即使类中并不存在某些属性。

js 复制代码
import {plainToClass} from "class-transformer";

class User {
  id: number
  firstName: string
  lastName: string
}

const fromPlainUser = {
  unkownProp: 'hello there',
  firstName: 'Umed',
  lastName: 'Khudoiberdiev',
}

console.log(plainToClass(User, fromPlainUser))

// 转换结果
User {
    unkownProp: 'hello there',
    firstName: 'Umed',
    lastName: 'Khudoiberdiev',
}

可以使用excludeExtraneousValues选项结合Expose装饰器来指定需要公开哪些类的属性

js 复制代码
import {Expose, plainToClass} from "class-transformer";

class User {
    @Expose() id: number;
    @Expose() firstName: string;
    @Expose() lastName: string;
}

const fromPlainUser = {
  unkownProp: 'hello there',
  firstName: 'Umed',
  lastName: 'Khudoiberdiev',
}

console.log(plainToClass(User, fromPlainUser, { excludeExtraneousValues: true }))

// 转换结果
User {
    id: undefined,
    firstName: 'Umed',
    lastName: 'Khudoiberdiev'
}

自定义实现Nest.js

可以使用express框架可以编写接口,mysql数据库读写数据,prisma ORM框架操作数据库。把这些组合到一起,就可以实现一个类似于Nest.js或者java的SpringBoot的架构,来开发node.js项目。

安装项目依赖

采用TypeScript编写,在安装项目依赖时先安装TypeScript执行环境

js 复制代码
npm install -g typescript
npm install -g ts-node
// 创建tsconfig.json配置文件 
tsc --init
js 复制代码
// 实现依赖注入(inversify基于reflect-metadata)
npm install inversify
yarn add inversify
npm i reflect-metadata
yarn add reflect-metadata
// 编写接口所需的express
npm insatll express
yarn add express
// 连接express和inversify所需的依赖
npm i inversify-express-utils
yarn add inversify-express-utils
// ORM框架prisma
npm i prisma
yarn add prisma
// express的TypeScript声明文件包
npm i @types/express -D
yarn add @types/express -D
// 验证Dto层对象
npm i class-validator
npm i class-transformer

在生成的tsconfig.json文件中设置配置,以便支持装饰器和反射,和关闭严格模式。

js 复制代码
{
  "compilerOptions": {
   
    /* Language and Environment */
    "target": "es2016",                 
    "experimentalDecorators": true,                   /* Enable experimental support for legacy experimental decorators. */
    "emitDecoratorMetadata": true,                    /* Emit design-type metadata for decorated declarations in source files. */

    /* Modules */
    "module": "commonjs",    

    /* Interop Constraints */
    "esModuleInterop": true,                             /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
    "forceConsistentCasingInFileNames": true,            /* Ensure that casing is correct in imports. */

    /* Type Checking */
    "strict": false,    
    "skipLibCheck": true                                 /* Skip type checking all .d.ts files. */
  }
}

项目架构

  • prisma目录是数据库的配置和创建模型(模型就是MySQL关系型数据库中的数据表)
  • .env是prisma生成的,在这儿配置数据库连接的端口以及密码等信息
  • src目录是根据创建的模型又生成的子目录以及db子目录,也就是用于处理每个数据表的操作
  • src中每个子目录包含controller层(路由控制层,接受和响应前端请求)、services层(模型层,用于对数据进行逻辑处理,该层会连接数据库)、dto层(用于验证对象)
  • src中的db子目录是封装prisma对数据库的操作。封装prisma,注入到实例化的容器对象中,在容器中去创建prisma对象。如此一来,就不用在每个模块的services.ts文件中使用prisma时都去new一个新对象。
  • main.ts是项目主入口,在这儿创建和配置容器对象,并在容器对象中注入依赖的类。

实现代码

schema.prisma:

js 复制代码
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "mysql"
  url      = env("DATABASE_URL")
}

model Post {
  id         Int       @id @default(autoincrement()) // 表示id字段是整数且自增,主键
  title      String // 表示title字段是字符串类型
  content    String
  publish    Boolean   @default(false) // 表示发布字段是布尔值默认false
  createdAt  DateTime  @default(now())
  updatedAt  DateTime  @updatedAt
  author     User      @relation(fields: [authorId], references: [id]) // 关联User用户表,关联关系:authorId 关联User表的id,必选
  authorId   Int // 外键,必选
  categories Category[] //  关系拥有者,和Category模型关联,且与Category模型互为关系拥有者,多对多关系,必选
}

model User {
  id    Int    @id @default(autoincrement()) // 主键
  name  String
  email String @unique // 表示email字段是字符串且是唯一的
  posts Post[] // 关系拥有者,和Post模型关联,一对多关系,必选
  profile Profile? // 关系拥有者,和Post模型关联,一对多关系,可选
}

model Profile {
  id        Int     @id @default(autoincrement()) // 主键
  gender    String?
  birthday  String?
  realeName String?
  user User? @relation(fields: [userId],references: [id]) // 关联User用户表,关联关系:userId 关联User表的id,可选
  userId Int? @unique // 外键,可选
}

model Category {
  id    Int    @id @default(autoincrement())
  name  String
  posts Post[] // 关系拥有者,和Post模型关联,且与Post模型互为关系拥有者,多对多关系,必选
}

main.ts

ts 复制代码
// 作为装饰器的基础,支持反射,放在最顶层
import 'reflect-metadata';
// 引入搭建服务端的express的类,InversifyExpressServer可以快速的创建express应用程序
import { InversifyExpressServer } from 'inversify-express-utils';
// 配置inversify容器,实现依赖注入
import { Container } from 'inversify';
// 引入express
import express from 'express';
// 引入prisma ORM框架
import { PrismaClient } from '@prisma/client';
// 引入User模块的controller
import { UserController } from './src/user/controller';
// 引入User模块的services
import { UserService } from './src/user/service';
// 引入封装好的数据库PrismaDatabase
import { PrismaDatabase } from './src/db';
// 创建容器对象,这个对象会自动创建项目所需的对象
const container = new Container();
// 在容器对象中绑定封装的prisma工厂类,注入的实际是工厂创建器这个函数,容器对象会执行注入的函数,返回工厂函数
container.bind<PrismaClient>('PrismaClient').toFactory(() => {
    // 工厂创建器函数,不需要使用context上下文对象
    return () => {
        // 工厂函数返回PrismaClient的实例化对象
        return new PrismaClient(); // 创建prisma实例化对象
    }
});
// 具体类型绑定自身
// 将User模块的controller和User模块的services添加到容器中,就可以实现services和controller之间的依赖注入,controller依赖于services
container.bind<UserController>(UserController).toSelf();
container.bind<UserService>(UserService).toSelf();
container.bind<PrismaDatabase>(PrismaDatabase).toSelf();
// 创建 Express 服务器,需要接受一个容器对象
const sever = new InversifyExpressServer(container);
// 在.setConfig方法中进行express的中间件注册
sever.setConfig(app => {
    // 注册express中间件,以便接受和解析前端请求时传入的json数据
    app.use(express.json());
});
// 所有注册的控制器和中间件添加到express应用程序并返回应用程序实例
const app = sever.build();
// 启动服务
app.listen(8089, () => {
    console.log('8089 is running');
})

db/index.ts

js 复制代码
// 封装数据库
// 引入装饰类,injectable修饰器将PrismaDatabase类标注为可依赖类
import { injectable, inject } from 'inversify'
// 引入依赖类PrismaClient类,用于变量的类型定义
import type { PrismaClient } from '@prisma/client';

@injectable()
export class PrismaDatabase {
    readonly prisma: PrismaClient
    constructor(@inject('PrismaClient') PrismaClient: () => PrismaClient) {
        // 在容器中注入的是prisma的工厂创建器,也就是注入了一个返回工厂函数的函数,容器运行工厂创建器后,会将工厂函数存储在容器中
        // PrismaClient: () => PrismaClient  参数名PrismaClient,其类型是一个函数类型,() => PrismaClient是TS的函数类型定义
        this.prisma = PrismaClient(); // 执行容器对象中存储的工厂函数,得到工厂函数中实例化的对象
    }
}

user/controller.ts

ts 复制代码
// 引入装饰器将类设置注册为路由控制器
import { controller,httpGet,httpPost as Post} from 'inversify-express-utils'
// 引入inject装饰器,将controller层标注为依赖类,其参数是被依赖的services层
import {inject} from 'inversify'
// 引入services层
import { UserService } from './service';
// 引入类,用于变量的类型定义
import type {Request,Response} from 'express'
@controller('/user') //路由
export class UserController {
    // 属性的修饰符是private readonly,属性名是userService,属性类型是UserService
    private readonly userService:UserService
    constructor(@inject(UserService) userService:UserService){
        // User类的构造函数中实例化依赖的services层的UserService类
        this.userService = userService;
    }

     // get请求接口,获取所有用户的信息
     @httpGet('/userInfo')
     public async getUserInfo (req:Request,res:Response) {
        let result = await this.userService.getUserList();
        res.send({
         code:200,
         msg:'查询成功',
         data:result
        })
     }
     // post请求接口,添加一条用户信息
     @Post('/create')
     public async createUser(req:Request,res:Response){
         // req用于接受前端出入的参数
         const info = req.body;
         let result = await this.userService.addUser(info)
         // res用于给前端返回数据
         res.send(result);
     }
}

user/service.ts

ts 复制代码
// 引入注入装饰器,作用是将修饰的类变成一个可以注入类的可依赖类
import { injectable, inject } from 'inversify'
import { UserDto } from './user.dto'
import { plainToClass } from 'class-transformer' //dto验证
import { validate } from 'class-validator' //dto验证
import { PrismaDatabase } from '../db'
@injectable()
export class UserService {

    private readonly prismaDataBase: PrismaDatabase
    constructor(@inject(PrismaDatabase) prismaDataBase: PrismaDatabase) {
        this.prismaDataBase = prismaDataBase;
    }

    public async getUserList() {
        // 逻辑处理后的数据,将处理后的结果返回
        return await this.prismaDataBase.prisma.user.findMany()
    }

    public async addUser(userData: UserDto) {
        // userData接受前端传入的参数
        const user = plainToClass(UserDto, userData);
        const errors = await validate(user);
        if (errors.length != 0) {
            // 错误信息数组有值,验证未通过
            return errors
        } else {
            // 逻辑处理后的数据
            let result = await this.prismaDataBase.prisma.user.create({
                data: user
            });
            // 将处理后的结果返回
            return result
        }

    }
}

user/user.dto.ts

ts 复制代码
// DTO层,用于验证对象,结合class-validator和class-transformer使用
import {IsNotEmpty,IsEmail} from 'class-validator'
import {Transform} from 'class-transformer'
export class UserDto {
    @IsNotEmpty({message:'用户名必填,不能为空'})
    @Transform(user => user.value.trim()) // 用于去除该值前后的两边空格
    name:string
    @IsNotEmpty({message:'邮箱必填,不能为空'})
    @IsEmail({},{message:'邮箱格式不正确'}) // 第一个参数对象可以设置邮箱是否允许包含域名等格式
    @Transform(({value}) => value.trim()) // 解构value属性,用于去除该值前后的两边空格
    email:string;
}

request.http:测试接口代码

js 复制代码
POST http://localhost:8089/user/create HTTP/1.1
Content-Type: application/json

{
    "name":"gloria"
}
js 复制代码
POST http://localhost:8089/user/create HTTP/1.1
Content-Type: application/json

{
    "name":"gloria",
    "email":"info@gnation.hk"
}
js 复制代码
GET http://localhost:8089/user/userInfo HTTP/1.1
相关推荐
王码码20354 小时前
Go语言的测试:从单元测试到集成测试
后端·golang·go·接口
王码码20354 小时前
Go语言中的测试:从单元测试到集成测试
后端·golang·go·接口
嵌入式×边缘AI:打怪升级日志5 小时前
使用JsonRPC实现前后台
前端·后端
小码哥_常5 小时前
从0到1:Spring Boot 中WebSocket实战揭秘,开启实时通信新时代
后端
lolo大魔王6 小时前
Go语言的异常处理
开发语言·后端·golang
IT_陈寒8 小时前
Python多进程共享变量那个坑,我差点没爬出来
前端·人工智能·后端
码事漫谈8 小时前
2026软考高级·系统架构设计师备考指南
后端
AI茶水间管理员9 小时前
如何让LLM稳定输出 JSON 格式结果?
前端·人工智能·后端
其实是白羊9 小时前
我用 Vibe Coding 搓了一个 IDEA 插件,复制URI 再也不用手动拼了
后端·intellij idea
用户8356290780519 小时前
Python 操作 Word 文档节与页面设置
后端·python