如何在TypeScript中使方法装饰器实例化对象

huangapple go评论79阅读模式
英文:

How make method decorators instance the object in Typescript

问题

I have been developing a lambda project and we are using lambda-api package. Then I have defined some decorators called Get and Post to map a route into the lambda API object. With these decorators, I have defined a class called ProductApi to hold methods that can be configured using those decorators and passing a route path. It works fine.

The problem is that when I have a class like ProductApi, the constructor is never called, and if I want to add some dependencies (like a Service or a Repository), they will never be defined. In this example, the /health route works fine because it does not use anything from the object instance, but other routes do not.

How can I make sure that the constructor will be called and define the service instance?

const api = createAPI();

function Get(path: string) {
    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        api.get(path, descriptor.value.bind(target));
    };
}

function Post(path: string) {
    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        api.post(path, descriptor.value.bind(target));
    };
}

class ProductApi {
    private someValue: string;

    constructor(private readonly productService: IProductService = new ProductService()) {
        // this scope does not execute
        this.someValue = "some value";
    }

    @Get('/health')
    async healthCheck(req: Request, res: Response) {
        console.log(`Executing -- GET /health`);
        // this.someValue does not exist here
        return res.status(200).json({ ok: true });
    }

    @Get('/products')
    async getProducts(req: Request, res: Response) {
        console.log(`Executing -- GET /products`);
        const data = this.productService.getProductsFromService(); // error: Cannot read properties of undefined (reading 'getProductsFromService')
        return res.status(200).json(data);
    }

    @Post('/products')
    async postProducts(req: Request, res: Response) {
        console.log(`Executing -- POST /products`);
        const product = this.productService.saveProduct('Drums', 1200); // error: Cannot read properties of undefined (reading 'saveProduct')
        return res.status(201).json(product);
    }
}

export const lambdaHandler = async (event: APIGatewayProxyEvent, context: Context): Promise<APIGatewayProxyResult> => {
    console.log('SCOPE lambda');
    return await api.run(event, context);
};

Note: I don't want to use frameworks; I just want an easy way to configure routes on the lambda API instance.

英文:

I have been developing a lambda project and we are using lambda-api package. Then I have defined some decorators called Get and Post to map a route into lambda api object. With these decorators I have defined a class called ProductApi to hold methods that can be configured using those decorators and passing a route path. It works fine.

The problem is that when I have a class like ProductApi the constructor is never called and if I want to add some dependencies (like a Service or a Repository) it will never be defined. In this example, the /health route works fine because it does not use anything from the object instance, but other routes does not.

How can I make sure that the constructor will be called and define the service instance?

const api = createAPI();
function Get(path: string) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
api.get(path, descriptor.value.bind(target));
};
}
function Post(path: string) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
api.post(path, descriptor.value.bind(target));
};
}
class ProductApi {
private someValue: string;
constructor(private readonly productService: IProductService = new ProductService()) {
// this scope does not execute
this.someValue = &quot;some value&quot;;
}
@Get(&#39;/health&#39;)
async healthCheckr(req: Request, res: Response) {
console.log(`Executing -- GET /health`);
// this.someValue does not exists here
return res.status(200).json({ ok: true });
}
@Get(&#39;/products&#39;)
async getProducts(req: Request, res: Response) {
console.log(`Executing -- GET /products`);
const data = this.productService.getProductsFromService(); // error: Cannot read properties of undefined (reading &#39;getProductsFromService&#39;)
return res.status(200).json(data);
}
@Post(&#39;/products&#39;)
async postProducts(req: Request, res: Response) {
console.log(`Executing -- POST /products`);
const product = this.productService.saveProduct(&#39;Drums&#39;, 1200); // erro: Cannot read properties of undefined (reading &#39;saveProduct&#39;)
return res.status(201).json(product);
}
}
export const lambdaHandler = async (event: APIGatewayProxyEvent, context: Context): Promise&lt;APIGatewayProxyResult&gt; =&gt; {
console.log(&#39;SCOPE lambda&#39;);
return await api.run(event, context);
};

Note: I don't want to use frameworks, I just want a easy way to configure routes on the lamda api instanmce.

答案1

得分: 1

你需要首先存储有关如何绑定路由的元数据,然后在类创建时应用它。

https://tsplay.dev/mbvA4m(可运行)

import type { Request, Response } from 'lambda-api'

type LambdifiedProto = {
  '_lambdifiedGetters'?: Record< /* 路径 */ string, /* 属性键 */ string>
}

function Get(path: string) {
  return function <K extends string, T extends (req: Request, res: Response) => Promise<any>>(
    proto: Record<K, T>, propertyKey: K, descriptor: TypedPropertyDescriptor<T>
  ): void {
    let lproto = proto as LambdifiedProto;
    if (!Object.hasOwn(lproto, '_lambdifiedGetters')) {
      // 创建或从原型克隆
      lproto._lambdifiedGetters = { ...lproto._lambdifiedGetters }
    }
    lproto._lambdifiedGetters[path] = propertyKey
    console.log(`在路径 ${path} 上注册 getter ${propertyKey}`)
  }
}

function Lambda<C extends new (...a: any[]) => any>(klass: C) {
  return class Lambdified extends klass {
    constructor(...a: any[]) {
      super(...a);
      let getters = (klass.prototype as Lambdified)._lambdifiedGetters
      for (let [path, propertyKey] of Object.entries(getters)) {
        console.log('注册 API:', { path, propertyKey, this: this })
        // api.register(path, (q, s) => this[propertyKey](q, s))
      }
    }
  }
}

@Lambda
class ProductApi {
  me = 'ProductApi'
  @Get('./health')
  @Get('./v1/health')
  async healthCheckr(req: Request, res: Response) {
    console.log(`执行 -- GET /health`);
    // 这里不存在this.someValue
    return res.status(200).json({ ok: true });
  }
}

console.log('...创建开始...')

new ProductApi()

(注意:代码中可能存在一些拼写错误,需要根据需要进行修复。)

英文:

You need to first store metadata about how to bind routes, and then apply that on class creation

https://tsplay.dev/mbvA4m (runnable)

import type { Request, Response } from &#39;lambda-api&#39;

type LambdifiedProto = {
  &#39;_lambdifiedGetters&#39;?: Record&lt; /* path */ string, /* propertyKey */ string&gt;
}

function Get(path: string) {
  return function &lt;K extends string, T extends (req: Request, res: Response) =&gt; Promise&lt;any&gt;&gt;(
    proto: Record&lt;K, T&gt;, propertyKey: K, descriptor: TypedPropertyDescriptor&lt;T&gt;
  ): void {
    let lproto = proto as LambdifiedProto;
    if (!Object.hasOwn(lproto, &#39;_lambdifiedGetters&#39;)) {
      // create or clone from protoproto
      lproto._lambdifiedGetters = { ...lproto._lambdifiedGetters }
    }
    lproto._lambdifiedGetters![path] = propertyKey
    console.log(`registered getter ${propertyKey} on path ${path}`)
  }
}

function Lambda&lt;C extends new (...a: any[]) =&gt; any&gt;(klass: C) {
  return class Lambdified extends klass {
    constructor(...a: any[]) {
      super(...a);
      let getters = (klass.prototype as Lambdified)._lambdifiedGetters
      for (let [path, propertyKey] of Object.entries(getters)) {
        console.log(&#39;register api: &#39;, { path, propertyKey, this: this })
        // api.register(path, (q, s) =&gt; this[propertyKey](q, s))
      }
    }
  }
}

@Lambda
class ProductApi {
  me = &#39;ProductApi&#39;
  @Get(&#39;./health&#39;)
  @Get(&#39;./v1/health&#39;)
  async healthCheckr(req: Request, res: Response) {
    console.log(`Executing -- GET /health`);
    // this.someValue does not exists here
    return res.status(200).json({ ok: true });
  }
}

console.log(&#39;...create start...&#39;)

new ProductApi()

答案2

得分: 1

不同于C#,在JS中,一个"方法"只是附加到对象的函数。你可以轻松将它放在一个变量中或附加到另一个对象上。这基本上定义了在那个"方法"内部的this是什么。

而一个"类"的构造函数只是一个创建新对象并告诉它的函数,"如果有人查找你没有的属性,将它们转发到我的prototype对象这里去"。然后它执行构造函数内部的代码,将该对象作为this

这就是JS的原型继承的要点,即使JS在此期间引入了class关键字,背后仍然发生的就是这个。

为什么我要解释这个?

因为装饰器是在原型对象上工作的。这一行代码api.get(path, descriptor.value.bind(target));从原型中获取方法,永久绑定原型对象作为this(因此生成的函数将只知道原型对象,永远不会看到任何真实实例),并使用绑定的函数作为该路由的回调函数。

所以,即使那个类会在某种方式下被实例化(由谁;我不知道),你传递给路由的函数将不会知道这一点。

在我看来,你的装饰器应该更像这样:

function Get(path: string) {
    return function (target: any, methodName: string) {
        if(typeof target[methodName] !== "function"){
          throw new Error("你需要将这个装饰器与一个方法一起使用。");
        }

        const Class = target.constructor;

        api.get(path, (req: Request, res: Response) => {
          const instance = diContainer.getInstance(Class); // 或者 new Class();
          return instance[methodName](req, res);
        });
    };
}

附注:Dimava提出了这个话题;这些是旧的装饰器。TS在JS有装饰器规范之前就已经采用了它们。现在已经有了一个规范,并且与这些旧装饰器有很大不同,TS终于在V5中实现了该规范。你(和我)应该了解新的语法并采用它,因为这个语法可能很快就会被弃用。

英文:

Unlike in C#, in JS a "method" is just a function stuck to an object. You can easily put it in a variable or stick it to another object. This basically defines what this is inside that "method".
And a "class" constructor is just a function that creates a new object and tells it, "if someone's looking for some property that you don't have, forward them to my prototype object over here." Then it executes the code inside the constructor with that object as this.

That is JS' prototypal inheritance in a nutshell, and even if JS has received a class keyword in the meantime, that's what still happens behind the scenes.

Why am I explaining this?

Because decorator are working on that prototype object. This line here api.get(path, descriptor.value.bind(target)); takes the method from that prototype, permanently binds the prototype object as this (so the resulting function will only know the prototype object and never ever see any real instance) and uses the bound function as a callback for that route.

So currently, even if that class would magically be instantiated (by whom; I don't know) the function that you've passed to the route will have no knowledge of that.

imo. Your decorator should look more like this:

function Get(path: string) {
return function (target: any, methodName: string) {
if(typeof target[methodName] !== &quot;function&quot;){
throw new Error(&quot;you need to use this decorator with a method.&quot;);
}
const Class = target.constructor;
api.get(path, (req: Request, res: Response) =&gt; {
const instance = diContainer.getInstance(Class); // or new Class();
return instance[methodName](req, res);
});
};
}

Sidenote: Dimava brought up this topic; these are legacy decorators. TS adapted them long before there was a spec for decorator in JS. Now there is one and it significantly differs from these legacy decorators and TS has finally implemented the spec in V5. You (and me) should get updated on the new syntax and adopt it, because this syntax will probably be deprecated pretty soon.

huangapple
  • 本文由 发表于 2023年3月31日 21:30:38
  • 转载请务必保留本文链接:https://go.coder-hub.com/75899115.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定