TypeScript/Angular2 中的 DTO 设计

DTO Design in TypeScript/Angular2

我目前正在开发 Angular 2 应用程序。在开发过程中,我开始使用 TypeScript classes 从 JSON 创建对象,我通过 HTTP 接收或在表单中创建新对象时。

class 可能看起来像这样。

export class Product {
    public id: number;
    public name: string;
    public description: string;
    public price: number;
    private _imageId: number;
    private _imageUrl: string;

    constructor(obj: Object = {}) {
        Object.assign(this, obj);
    }

    get imageId(): number {
        return this._imageId;
    }
    set imageId(id: number) {
        this._imageId = id;
        this._imageUrl = `//www.example.org/images/${id}`;
    }

    get imageUrl(): string {
        return this._imageUrl;
    }

    public getDTO() {
        return {
            name: this.name,
            description: this.description,
            imageId: this.imageId,
            price: this.price
        }
    }
}

到目前为止,上面显示的这个解决方案效果很好。但是现在让我们假设对象中有更多的属性,我想要一个干净的 DTO(例如没有私有属性)来通过 POST 将这个对象发送到我的服务器。更通用的 getDTO() 函数会是什么样子?我想避免有一长串 属性 赋值。我正在考虑为属性使用装饰器。但我真的不知道如何使用它们来过滤 DTO 的属性。

您可以为此使用 property decorator

const DOT_INCLUDES = {};

function DtoInclude(proto, name) {
    const key = proto.constructor.name;
    if (DOT_INCLUDES[key]) {
        DOT_INCLUDES[key].push(name);
    } else {
        DOT_INCLUDES[key] = [name];
    }
}

class A {
    @DtoInclude
    public x: number;
    public y: number;

    @DtoInclude
    private str: string;

    constructor(x: number, y: number, str: string) {
        this.x = x;
        this.y = y;
        this.str = str;
    }

    toDTO(): any {
        const includes: string[] = DOT_INCLUDES[(this.constructor as any).name];
        const dto = {};

        for (let key in this) {
            if (includes.indexOf(key) >= 0) {
                dto[key] = this[key];
            }
        }

        return dto;
    }
}

let a = new A(1, 2, "string");
console.log(a.toDTO()); // Object {x: 1, str: "string"}

(code in playground)

如果你愿意,你可以使用他们示例中使用的 the reflect-metadata,我用 DOT_INCLUDES 注册表实现了它,这样它就可以在 playground 中很好地工作,而不需要额外的依赖。


编辑

正如@Bergi 评论的那样,您可以遍历 includes 而不是 this:

toDTO(): any {
    const includes: string[] = DOT_INCLUDES[(this.constructor as any).name];
    const dto = {};

    for (let ket of includes) {
        dto[key] = this[key];
    }

    return dto;
}

哪个确实更有效率也更有意义。

我使用 class-transformer 进行 DTO 设计。它完成所有肮脏的工作并提供 @Expose()@Exclude()@Transform()@Type() 以及其他几个有用的 属性 注释。只需阅读文档。

这是一个例子:

  • 基本 DTO 自动处理序列化和反序列化。
  • @Transform 转换 SQL 日期 to/from 字符串。您可以使用自己的自定义转换器。
import {
  classToPlain,
  plainToClass,
  Transform,
  TransformationType,
  TransformFnParams
} from 'class-transformer';
import { DateTime } from 'luxon';

/**
 * Base DTO class.
 */
export class Dto {
  constructor(data?: Partial<Dto>) {
    if (data) {
      Object.assign(this, data);
    }
  }

  /**
   * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#tojson_behavior
   */
  toJSON(): Record<string, any> {
    return classToPlain(this);
  }

  static fromJSON<T extends typeof Dto>(this: T, json: string): InstanceType<T> {
    return this.fromPlain(JSON.parse(json));
  }

  /**
   * @see https://github.com/Microsoft/TypeScript/issues/5863#issuecomment-528305043
   */
  static fromPlain<T extends typeof Dto>(this: T, plain: Object): InstanceType<T> {
    return plainToClass(this, plain) as InstanceType<T>;
  }
}

/**
 * SQL date transformer for JSON serialization.
 */
export function sqlDateTransformer({type, value}: TransformFnParams): Date | string {
  if (!value) {
    return value;
  }

  switch (type) {
    case TransformationType.PLAIN_TO_CLASS:
      return DateTime.fromSQL(value as string).toJSDate();

    case TransformationType.CLASS_TO_PLAIN:
      return DateTime.fromJSDate(value as Date).toFormat('yyyy-MM-dd HH:mm:ss');

    default:
      return value;
  }
}

/**
 * Example DTO.
 */
export class SomethingDto extends Dto {
  id?: string;
  name?: string;

  /**
   * Date is serialized into SQL format.
   */
  @Transform(sqlDateTransformer)
  date?: Date;

  constructor(data?: Partial<SomethingDto>) {
    super(data);
  }
}

// Create new DTO
const somethingDto = new SomethingDto({
  id: '1a8b5b9a-4681-4868-bde5-95f023ba1706',
  name: 'It is a thing',
  date: new Date()
});

// Convert to JSON
const jsonString = JSON.stringify(somethingDto);
console.log('JSON string:', jsonString);

// Parse from JSON
const parsed = SomethingDto.fromJSON(jsonString);
console.log('Parsed:', parsed);